From 206252308da7e8deebd1ab3bb3e10eb74c3b08d0 Mon Sep 17 00:00:00 2001 From: ysaskia Date: Wed, 8 Mar 2023 22:04:52 +0100 Subject: [PATCH 01/10] fix: svelte files now prepocessed during indexing --- src/preset/indexer.js | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/preset/indexer.js b/src/preset/indexer.js index 08a956e9..287a844f 100644 --- a/src/preset/indexer.js +++ b/src/preset/indexer.js @@ -1,8 +1,18 @@ -import { extractStories } from '../parser/extract-stories'; +import * as svelte from 'svelte/compiler'; +import path from 'path'; import fs from 'fs-extra'; +import { extractStories } from '../parser/extract-stories'; export async function svelteIndexer(fileName, { makeTitle }) { let code = (await fs.readFile(fileName, 'utf-8')).toString(); + const optionsPath = await findUp('svelte.config.js'); + + if (optionsPath) { + const { default: svelteOptions } = await import(`file:///${optionsPath}`); + if (svelteOptions && svelteOptions.preprocess) { + code = (await svelte.preprocess(code, svelteOptions.preprocess, { filename: fileName })).code; + } + } const defs = extractStories(code); @@ -16,3 +26,20 @@ export async function svelteIndexer(fileName, { makeTitle }) { })), }; } + +async function findUp(name) { + const chunks = __dirname.split(path.sep); + + while (chunks.length) { + const filePath = path.resolve(chunks.join(path.posix), `../${name}`); + const pathExist = fs.pathExists(filePath, name); + + if (pathExist) { + return filePath; + } + + chunks.pop(); + } + + return ''; +} From 695ac825471d1da50fbff928ff81f2e40184f074 Mon Sep 17 00:00:00 2001 From: ysaskia Date: Wed, 8 Mar 2023 22:25:31 +0100 Subject: [PATCH 02/10] chore: remove quick and dirty usage of __dirname --- src/preset/indexer.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/preset/indexer.js b/src/preset/indexer.js index 287a844f..0d9b0b7a 100644 --- a/src/preset/indexer.js +++ b/src/preset/indexer.js @@ -1,5 +1,6 @@ import * as svelte from 'svelte/compiler'; import path from 'path'; +import { fileURLToPath } from 'url'; import fs from 'fs-extra'; import { extractStories } from '../parser/extract-stories'; @@ -28,7 +29,8 @@ export async function svelteIndexer(fileName, { makeTitle }) { } async function findUp(name) { - const chunks = __dirname.split(path.sep); + const importPath = fileURLToPath(import.meta.url); + const chunks = path.dirname(importPath).split(path.sep); while (chunks.length) { const filePath = path.resolve(chunks.join(path.posix), `../${name}`); From ba0f22045dc5220643f6884a50146cf34f8124e7 Mon Sep 17 00:00:00 2001 From: ysaskia Date: Wed, 8 Mar 2023 22:32:00 +0100 Subject: [PATCH 03/10] chore: remove hardcoded file protocol --- src/preset/indexer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/preset/indexer.js b/src/preset/indexer.js index 0d9b0b7a..ef0b08b4 100644 --- a/src/preset/indexer.js +++ b/src/preset/indexer.js @@ -1,6 +1,6 @@ import * as svelte from 'svelte/compiler'; import path from 'path'; -import { fileURLToPath } from 'url'; +import { fileURLToPath, pathToFileURL } from 'url'; import fs from 'fs-extra'; import { extractStories } from '../parser/extract-stories'; @@ -9,7 +9,7 @@ export async function svelteIndexer(fileName, { makeTitle }) { const optionsPath = await findUp('svelte.config.js'); if (optionsPath) { - const { default: svelteOptions } = await import(`file:///${optionsPath}`); + const { default: svelteOptions } = await import(pathToFileURL(optionsPath)); if (svelteOptions && svelteOptions.preprocess) { code = (await svelte.preprocess(code, svelteOptions.preprocess, { filename: fileName })).code; } From a47dfdb645278b665d6a45c21c9fbaa987eae03b Mon Sep 17 00:00:00 2001 From: ysaskia Date: Wed, 8 Mar 2023 22:43:10 +0100 Subject: [PATCH 04/10] chore: use promise signature of pathExists --- src/preset/indexer.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/preset/indexer.js b/src/preset/indexer.js index ef0b08b4..f612a314 100644 --- a/src/preset/indexer.js +++ b/src/preset/indexer.js @@ -34,7 +34,8 @@ async function findUp(name) { while (chunks.length) { const filePath = path.resolve(chunks.join(path.posix), `../${name}`); - const pathExist = fs.pathExists(filePath, name); + // eslint-disable-next-line no-await-in-loop + const pathExist = await fs.pathExists(filePath); if (pathExist) { return filePath; From 182d111b8165c7cf7a3e1756d4f673a5244d65e0 Mon Sep 17 00:00:00 2001 From: ysaskia Date: Wed, 8 Mar 2023 22:48:56 +0100 Subject: [PATCH 05/10] chore: join with posix path sep --- src/preset/indexer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/preset/indexer.js b/src/preset/indexer.js index f612a314..794fbeb4 100644 --- a/src/preset/indexer.js +++ b/src/preset/indexer.js @@ -33,7 +33,7 @@ async function findUp(name) { const chunks = path.dirname(importPath).split(path.sep); while (chunks.length) { - const filePath = path.resolve(chunks.join(path.posix), `../${name}`); + const filePath = path.resolve(chunks.join(path.posix.sep), `../${name}`); // eslint-disable-next-line no-await-in-loop const pathExist = await fs.pathExists(filePath); From 9340d8661c91c12ced50009d91a431fcb051ccda Mon Sep 17 00:00:00 2001 From: ysaskia Date: Thu, 9 Mar 2023 21:51:45 +0100 Subject: [PATCH 06/10] chore: add svelte config loader --- src/config-loader.js | 171 ++++++++++++++++++++++++++++++++++++++++++ src/preset/indexer.js | 33 ++------ 2 files changed, 176 insertions(+), 28 deletions(-) create mode 100644 src/config-loader.js diff --git a/src/config-loader.js b/src/config-loader.js new file mode 100644 index 00000000..f6e5d8ae --- /dev/null +++ b/src/config-loader.js @@ -0,0 +1,171 @@ +// This file is a rewrite of `@sveltejs/vite-plugin-svelte` without the `Vite` +// parts: https://github.com/sveltejs/vite-plugin-svelte/blob/e8e52deef93948da735c4ab69c54aced914926cf/packages/vite-plugin-svelte/src/utils/load-svelte-config.ts +import { logger } from '@storybook/client-logger'; +import { createRequire } from 'module'; +import path from 'path'; +import fs from 'fs'; +import { pathToFileURL, fileURLToPath } from 'url'; + +/** + * Try find svelte config and then load it. + * + * @returns {import('@sveltejs/kit').Config} + * Returns the svelte configuration object. + */ +export async function loadSvelteConfig() { + const configFile = findSvelteConfig(); + let err; + + // try to use dynamic import for svelte.config.js first + if (configFile.endsWith('.js') || configFile.endsWith('.mjs')) { + try { + return importSvelteOptions(configFile); + } catch (e) { + logger.error(`failed to import config ${configFile}`, e); + err = e; + } + } + // cjs or error with dynamic import + if (configFile.endsWith('.js') || configFile.endsWith('.cjs')) { + try { + return requireSvelteOptions(configFile); + } catch (e) { + logger.error(`failed to require config ${configFile}`, e); + if (!err) { + err = e; + } + } + } + // failed to load existing config file + throw new Error(`failed to load config ${configFile}`, { cause: err }); +} + +const importSvelteOptions = (() => { + // hide dynamic import from ts transform to prevent it turning into a require + // see https://github.com/microsoft/TypeScript/issues/43329#issuecomment-811606238 + // also use timestamp query to avoid caching on reload + const dynamicImportDefault = new Function( + 'path', + 'timestamp', + 'return import(path + "?t=" + timestamp).then(m => m.default)' + ); + + /** + * Try import specified svelte configuration. + * + * @param {string} configFile + * Absolute path of the svelte config file to import. + * + * @returns {import('@sveltejs/kit').Config} + * Returns the svelte configuration object. + */ + return async (configFile) => { + const result = await dynamicImportDefault( + pathToFileURL(configFile).href, + fs.statSync(configFile).mtimeMs + ); + + if (result != null) { + return { ...result, configFile }; + } + throw new Error(`invalid export in ${configFile}`); + }; +})(); + +const requireSvelteOptions = (() => { + /** @type {NodeRequire} */ + let esmRequire; + + /** + * Try import specified svelte configuration. + * + * @param {string} configFile + * Absolute path of the svelte config file to require. + * + * @returns {import('@sveltejs/kit').Config} + * Returns the svelte configuration object. + */ + return (configFile) => { + // identify which require function to use (esm and cjs mode) + const requireFn = import.meta.url + ? (esmRequire = esmRequire ?? createRequire(import.meta.url)) + : require; + + // avoid loading cached version on reload + delete requireFn.cache[requireFn.resolve(configFile)]; + const result = requireFn(configFile); + + if (result != null) { + return { ...result, configFile }; + } + throw new Error(`invalid export in ${configFile}`); + }; +})(); + +/** + * Try find svelte config. First in current working dir otherwise try to + * founding it by climbing up the directory tree. + * + * @returns {string} + * Returns an array containing all available config files. + */ +function findSvelteConfig() { + const lookupDir = process.cwd(); + let configFiles = getConfigFiles(lookupDir); + + if (configFiles.length === 0) { + configFiles = getConfigFilesUp(); + } + if (configFiles.length === 0) { + throw new Error('no svelte config found'); + } else if (configFiles.length > 1) { + logger.warn( + `found more than one svelte config file, using ${configFiles[0]}. ` + + 'you should only have one!', + configFiles + ); + } + return configFiles[0]; +} + +/** + * Gets the file path of the svelte config by walking up the tree. + * Returning the first found. Should solves most of monorepos with + * only one config at workspace root. + * + * @returns {string[]} + * Returns an array containing all available config files. + */ +async function getConfigFilesUp() { + const importPath = fileURLToPath(import.meta.url); + const pathChunks = path.dirname(importPath).split(path.sep); + + while (pathChunks.length) { + pathChunks.pop(); + + const parentDir = pathChunks.join(path.posix.sep); + const configFiles = getConfigFiles(parentDir); + + if (configFiles.length !== 0) { + return configFiles; + } + } + return []; +} + +/** + * Gets all svelte config from a specified `lookupDir`. + * + * @param {string} lookupDir + * Directory in which to look for svelte files. + * + * @returns {string[]} + * Returns an array containing all available config files. + */ +function getConfigFiles(lookupDir) { + return knownConfigFiles + .map((candidate) => path.resolve(lookupDir, candidate)) + .filter((file) => fs.existsSync(file)); +} + +const knownConfigFiles = ['js', 'cjs', 'mjs'].map((ext) => `svelte.config.${ext}`); diff --git a/src/preset/indexer.js b/src/preset/indexer.js index 794fbeb4..1a23dbe2 100644 --- a/src/preset/indexer.js +++ b/src/preset/indexer.js @@ -1,18 +1,14 @@ -import * as svelte from 'svelte/compiler'; -import path from 'path'; -import { fileURLToPath, pathToFileURL } from 'url'; import fs from 'fs-extra'; +import * as svelte from 'svelte/compiler'; import { extractStories } from '../parser/extract-stories'; +import { loadSvelteConfig } from '../config-loader'; export async function svelteIndexer(fileName, { makeTitle }) { let code = (await fs.readFile(fileName, 'utf-8')).toString(); - const optionsPath = await findUp('svelte.config.js'); + const svelteOptions = loadSvelteConfig(); - if (optionsPath) { - const { default: svelteOptions } = await import(pathToFileURL(optionsPath)); - if (svelteOptions && svelteOptions.preprocess) { - code = (await svelte.preprocess(code, svelteOptions.preprocess, { filename: fileName })).code; - } + if (svelteOptions && svelteOptions.preprocess) { + code = (await svelte.preprocess(code, svelteOptions.preprocess, { filename: fileName })).code; } const defs = extractStories(code); @@ -27,22 +23,3 @@ export async function svelteIndexer(fileName, { makeTitle }) { })), }; } - -async function findUp(name) { - const importPath = fileURLToPath(import.meta.url); - const chunks = path.dirname(importPath).split(path.sep); - - while (chunks.length) { - const filePath = path.resolve(chunks.join(path.posix.sep), `../${name}`); - // eslint-disable-next-line no-await-in-loop - const pathExist = await fs.pathExists(filePath); - - if (pathExist) { - return filePath; - } - - chunks.pop(); - } - - return ''; -} From 71b3707565ca19bf845a4705508d57c0ec74ebf0 Mon Sep 17 00:00:00 2001 From: ysaskia Date: Thu, 9 Mar 2023 22:13:20 +0100 Subject: [PATCH 07/10] chore: remove useless async directive --- src/config-loader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config-loader.js b/src/config-loader.js index f6e5d8ae..a0965579 100644 --- a/src/config-loader.js +++ b/src/config-loader.js @@ -136,7 +136,7 @@ function findSvelteConfig() { * @returns {string[]} * Returns an array containing all available config files. */ -async function getConfigFilesUp() { +function getConfigFilesUp() { const importPath = fileURLToPath(import.meta.url); const pathChunks = path.dirname(importPath).split(path.sep); From 8638edd9332c0cbcccd329afec9c7c75c90e1a8d Mon Sep 17 00:00:00 2001 From: ysaskia Date: Wed, 15 Mar 2023 10:45:12 +0100 Subject: [PATCH 08/10] chore: fix review errors and remarks --- src/config-loader.js | 57 ++++++++++++++++++++++++++++--------------- src/preset/indexer.js | 2 +- 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/src/config-loader.js b/src/config-loader.js index a0965579..cca23ac6 100644 --- a/src/config-loader.js +++ b/src/config-loader.js @@ -4,16 +4,23 @@ import { logger } from '@storybook/client-logger'; import { createRequire } from 'module'; import path from 'path'; import fs from 'fs'; +import { pathExists } from "fs-extra"; import { pathToFileURL, fileURLToPath } from 'url'; /** * Try find svelte config and then load it. * - * @returns {import('@sveltejs/kit').Config} + * @returns {import('@sveltejs/kit').Config | undefined} * Returns the svelte configuration object. */ export async function loadSvelteConfig() { - const configFile = findSvelteConfig(); + const configFile = await findSvelteConfig(); + + // no need to throw error since we handle projects without config files + if (configFile !== undefined) { + return undefined; + } + let err; // try to use dynamic import for svelte.config.js first @@ -104,24 +111,24 @@ const requireSvelteOptions = (() => { /** * Try find svelte config. First in current working dir otherwise try to - * founding it by climbing up the directory tree. + * find it by climbing up the directory tree. * - * @returns {string} - * Returns an array containing all available config files. + * @returns {Promise} + * Returns the absolute path of the config file. */ -function findSvelteConfig() { +async function findSvelteConfig() { const lookupDir = process.cwd(); - let configFiles = getConfigFiles(lookupDir); + let configFiles = await getConfigFiles(lookupDir); if (configFiles.length === 0) { - configFiles = getConfigFilesUp(); + configFiles = await getConfigFilesUp(); } if (configFiles.length === 0) { - throw new Error('no svelte config found'); - } else if (configFiles.length > 1) { + return undefined; + } + if (configFiles.length > 1) { logger.warn( - `found more than one svelte config file, using ${configFiles[0]}. ` + - 'you should only have one!', + 'Multiple svelte configuration files were found, which is unexpected. The first one will be used.', configFiles ); } @@ -133,10 +140,10 @@ function findSvelteConfig() { * Returning the first found. Should solves most of monorepos with * only one config at workspace root. * - * @returns {string[]} + * @returns {Promise} * Returns an array containing all available config files. */ -function getConfigFilesUp() { +async function getConfigFilesUp() { const importPath = fileURLToPath(import.meta.url); const pathChunks = path.dirname(importPath).split(path.sep); @@ -144,7 +151,8 @@ function getConfigFilesUp() { pathChunks.pop(); const parentDir = pathChunks.join(path.posix.sep); - const configFiles = getConfigFiles(parentDir); + // eslint-disable-next-line no-await-in-loop + const configFiles = await getConfigFiles(parentDir); if (configFiles.length !== 0) { return configFiles; @@ -159,13 +167,22 @@ function getConfigFilesUp() { * @param {string} lookupDir * Directory in which to look for svelte files. * - * @returns {string[]} + * @returns {Promise} * Returns an array containing all available config files. */ -function getConfigFiles(lookupDir) { - return knownConfigFiles - .map((candidate) => path.resolve(lookupDir, candidate)) - .filter((file) => fs.existsSync(file)); +async function getConfigFiles(lookupDir) { + /** @type {[string, boolean][]} */ + const fileChecks = await Promise.all( + knownConfigFiles.map(async (candidate) => { + const filePath = path.resolve(lookupDir, candidate); + return [filePath, await pathExists(filePath)]; + }) + ); + + return fileChecks.reduce((files, [file, exists]) => { + if (exists) files.push(file); + return files; + }, []); } const knownConfigFiles = ['js', 'cjs', 'mjs'].map((ext) => `svelte.config.${ext}`); diff --git a/src/preset/indexer.js b/src/preset/indexer.js index 1a23dbe2..fd3390f4 100644 --- a/src/preset/indexer.js +++ b/src/preset/indexer.js @@ -5,7 +5,7 @@ import { loadSvelteConfig } from '../config-loader'; export async function svelteIndexer(fileName, { makeTitle }) { let code = (await fs.readFile(fileName, 'utf-8')).toString(); - const svelteOptions = loadSvelteConfig(); + const svelteOptions = await loadSvelteConfig(); if (svelteOptions && svelteOptions.preprocess) { code = (await svelte.preprocess(code, svelteOptions.preprocess, { filename: fileName })).code; From c073b818c5dba01f27362f140391f85fb8e174b4 Mon Sep 17 00:00:00 2001 From: Yoann Date: Tue, 21 Mar 2023 10:51:59 +0100 Subject: [PATCH 09/10] chore: apply typo fix Co-authored-by: Jeppe Reinhold --- src/config-loader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config-loader.js b/src/config-loader.js index cca23ac6..3fffa7d9 100644 --- a/src/config-loader.js +++ b/src/config-loader.js @@ -17,7 +17,7 @@ export async function loadSvelteConfig() { const configFile = await findSvelteConfig(); // no need to throw error since we handle projects without config files - if (configFile !== undefined) { + if (configFile === undefined) { return undefined; } From f2eaa1399ebbee93df875a5c40c5f9dc3b4e9b1d Mon Sep 17 00:00:00 2001 From: ysaskia Date: Tue, 21 Mar 2023 10:57:00 +0100 Subject: [PATCH 10/10] chore: add `fs-extra` as project dependency --- package.json | 1 + yarn.lock | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/package.json b/package.json index 4d0b110d..5f800d9a 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ }, "dependencies": { "@babel/runtime": "^7.17.5", + "fs-extra": "^11.1.1", "magic-string": "^0.26.6", "ts-dedent": "^2.0.0" }, diff --git a/yarn.lock b/yarn.lock index 081cd589..c56bb7c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7784,6 +7784,15 @@ fs-extra@^10.1.0: jsonfile "^6.0.1" universalify "^2.0.0" +fs-extra@^11.1.1: + version "11.1.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d" + integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-extra@^9.0.1: version "9.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d"