diff --git a/package.json b/package.json index 4d0b110..5f800d9 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/src/config-loader.js b/src/config-loader.js new file mode 100644 index 0000000..3fffa7d --- /dev/null +++ b/src/config-loader.js @@ -0,0 +1,188 @@ +// 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 { pathExists } from "fs-extra"; +import { pathToFileURL, fileURLToPath } from 'url'; + +/** + * Try find svelte config and then load it. + * + * @returns {import('@sveltejs/kit').Config | undefined} + * Returns the svelte configuration object. + */ +export async function loadSvelteConfig() { + 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 + 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 + * find it by climbing up the directory tree. + * + * @returns {Promise} + * Returns the absolute path of the config file. + */ +async function findSvelteConfig() { + const lookupDir = process.cwd(); + let configFiles = await getConfigFiles(lookupDir); + + if (configFiles.length === 0) { + configFiles = await getConfigFilesUp(); + } + if (configFiles.length === 0) { + return undefined; + } + if (configFiles.length > 1) { + logger.warn( + 'Multiple svelte configuration files were found, which is unexpected. The first one will be used.', + 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 {Promise} + * 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); + // eslint-disable-next-line no-await-in-loop + const configFiles = await 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 {Promise} + * Returns an array containing all available config files. + */ +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 08a956e..fd3390f 100644 --- a/src/preset/indexer.js +++ b/src/preset/indexer.js @@ -1,8 +1,15 @@ -import { extractStories } from '../parser/extract-stories'; 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 svelteOptions = await loadSvelteConfig(); + + if (svelteOptions && svelteOptions.preprocess) { + code = (await svelte.preprocess(code, svelteOptions.preprocess, { filename: fileName })).code; + } const defs = extractStories(code); diff --git a/yarn.lock b/yarn.lock index 081cd58..c56bb7c 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"