diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.ts similarity index 95% rename from docs/.vitepress/config.js rename to docs/.vitepress/config.ts index b1db23935bd7..2df48f2a4f5d 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.ts @@ -1,4 +1,6 @@ -module.exports = { +import { UserConfig } from 'vitepress' + +const config: UserConfig = { lang: 'en-US', title: 'VitePress', description: 'Vite & Vue powered static site generator.', @@ -43,6 +45,8 @@ module.exports = { } } +export default config + function getGuideSidebar() { return [ { diff --git a/src/node/config.ts b/src/node/config.ts index a7b052b579b1..1d72635348c8 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -12,6 +12,7 @@ import { } from './shared' import { resolveAliases, APP_PATH, DEFAULT_THEME_PATH } from './alias' import { MarkdownOptions } from './markdown/markdown' +import { build } from 'esbuild' export { resolveSiteDataByRoute } from './shared' @@ -70,7 +71,7 @@ const resolve = (root: string, file: string) => export async function resolveConfig( root: string = process.cwd() ): Promise { - const userConfig = await resolveUserConfig(root) + const { configPath, userConfig } = await resolveUserConfig(root) if (userConfig.vueOptions) { console.warn( @@ -104,7 +105,7 @@ export async function resolveConfig( cwd: srcDir, ignore: ['**/node_modules', ...(userConfig.srcExclude || [])] }), - configPath: resolve(root, 'config.js'), + configPath: configPath, outDir: resolve(root, 'dist'), tempDir: path.resolve(APP_PATH, 'temp'), markdown: userConfig.markdown, @@ -116,29 +117,98 @@ export async function resolveConfig( return config } -export async function resolveUserConfig(root: string): Promise { - // load user config - const configPath = resolve(root, 'config.js') - const hasUserConfig = await fs.pathExists(configPath) - // always delete cache first before loading config - delete require.cache[configPath] - const userConfig: UserConfig | (() => UserConfig) = hasUserConfig - ? require(configPath) - : {} - if (hasUserConfig) { - debug(`loaded config at ${chalk.yellow(configPath)}`) +/** + * load user config + * @param root + * @param configFile + * @returns + */ +export async function resolveUserConfig( + root: string, + configFile?: string +): Promise<{ + configPath: string + userConfig: UserConfig +}> { + const start = Date.now() + + let configPath: string | undefined + let isTS = false + + if (configFile) { + configPath = resolve(root, configFile) + isTS = configFile.endsWith('.ts') } else { + const jsConfigPath = resolve(root, 'config.js') + if (fs.existsSync(jsConfigPath)) { + configPath = jsConfigPath + } + + if (!configPath) { + const tsConfigPath = resolve(root, 'config.ts') + if (fs.existsSync(tsConfigPath)) { + configPath = tsConfigPath + isTS = true + } + } + } + + if (!configPath) { debug(`no config file found.`) + return { + configPath: '', + userConfig: {} + } } - return typeof userConfig === 'function' ? userConfig() : userConfig + try { + let userConfig: UserConfig | (() => UserConfig) | undefined + + if (!userConfig && !isTS) { + try { + // always delete cache first before loading config + delete require.cache[configPath] + userConfig = configPath ? require(configPath) : {} + debug(`loaded config at ${chalk.yellow(configPath)}`) + } catch (e) { + const ignored = new RegExp( + [ + `Cannot use import statement`, + `Must use import to load ES Module`, + `Unexpected token`, + `Unexpected identifier` + ].join('|') + ) + if (!ignored.test(e.message)) { + throw e + } + } + } + + if (!userConfig) { + const bundled = await bundleConfigFile(configPath) + userConfig = await loadConfigFromBundledFile(configPath, bundled.code) + debug(`bundled config file loaded in ${Date.now() - start}ms`) + } + + const config = typeof userConfig === 'function' ? userConfig() : userConfig + return { + configPath, + userConfig: config + } + } catch (e) { + console.error(chalk.red(`failed to load config from ${configPath}`), { + error: e + }) + throw e + } } export async function resolveSiteData( root: string, userConfig?: UserConfig ): Promise { - userConfig = userConfig || (await resolveUserConfig(root)) + userConfig = userConfig || (await (await resolveUserConfig(root)).userConfig) return { lang: userConfig.lang || 'en-US', title: userConfig.title || 'VitePress', @@ -151,3 +221,86 @@ export async function resolveSiteData( customData: userConfig.customData || {} } } + +interface NodeModuleWithCompile extends NodeModule { + _compile(code: string, filename: string): any +} + +async function loadConfigFromBundledFile( + fileName: string, + bundledCode: string +): Promise { + const extension = path.extname(fileName) + const defaultLoader = require.extensions[extension]! + require.extensions[extension] = (module: NodeModule, filename: string) => { + if (filename === fileName) { + ;(module as NodeModuleWithCompile)._compile(bundledCode, filename) + } else { + defaultLoader(module, filename) + } + } + // clear cache in case of server restart + delete require.cache[require.resolve(fileName)] + const raw = require(fileName) + const config = raw.__esModule ? raw.default : raw + require.extensions[extension] = defaultLoader + return config +} + +async function bundleConfigFile( + fileName: string, + mjs = false +): Promise<{ code: string; dependencies: string[] }> { + const result = await build({ + absWorkingDir: process.cwd(), + entryPoints: [fileName], + outfile: 'out.js', + write: false, + platform: 'node', + bundle: true, + format: mjs ? 'esm' : 'cjs', + sourcemap: 'inline', + metafile: true, + plugins: [ + { + name: 'externalize-deps', + setup(build) { + build.onResolve({ filter: /.*/ }, (args) => { + const id = args.path + if (id[0] !== '.' && !path.isAbsolute(id)) { + return { + external: true + } + } + }) + } + }, + { + name: 'replace-import-meta', + setup(build) { + build.onLoad({ filter: /\.[jt]s$/ }, async (args) => { + const contents = await fs.promises.readFile(args.path, 'utf8') + return { + loader: args.path.endsWith('.ts') ? 'ts' : 'js', + contents: contents + .replace( + /\bimport\.meta\.url\b/g, + JSON.stringify(`file://${args.path}`) + ) + .replace( + /\b__dirname\b/g, + JSON.stringify(path.dirname(args.path)) + ) + .replace(/\b__filename\b/g, JSON.stringify(args.path)) + } + }) + } + } + ] + }) + const { text } = result.outputFiles[0] + return { + code: text, + dependencies: result.metafile ? Object.keys(result.metafile.inputs) : [] + } +}