Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"cac": "^7.0.0",
"chokidar": "^5.0.0",
"envinfo": "^7.21.0",
"esbuild": "^0.28.0"
"rolldown": "catalog:vite"
},
"devDependencies": {
"@types/envinfo": "^7.8.4"
Expand Down
151 changes: 108 additions & 43 deletions packages/cli/src/config/loadUserConfig.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
import process from 'node:process'
import { pathToFileURL } from 'node:url'

import { fs, hash, importFileDefault, path } from '@vuepress/utils'
import { build } from 'esbuild'
import { rolldown } from 'rolldown'
import type { OutputChunk } from 'rolldown'

import type { UserConfig } from '../types/index.js'

function collectAllModules(
chunks: Record<string, OutputChunk | undefined>,
fileName: string,
set: Set<string>,
): void {
const chunk = chunks[fileName]
if (!chunk) return
for (const modId of chunk.moduleIds) {
if (!set.has(modId)) {
set.add(modId)
for (const importFileName of chunk.imports) {
collectAllModules(chunks, importFileName, set)
}
}
}
}

/**
* Load user config file
*/
Expand All @@ -21,82 +38,130 @@ export const loadUserConfig = async (
userConfigDependencies: [],
}
}
// forked and modified from https://github.com/vitejs/vite/blob/889bfc0ada6d6cd356bb7a92efdce96298f82fef/packages/vite/src/node/config.ts#L1531
// TODO: we can migrate to something like `bundler-require`, but its `__dirname` support is not as good as vite

// forked and modified from https://github.com/vitejs/vite/blob/main/packages/vite/src/node/config.ts
const dirnameVarName = '__vite_injected_original_dirname'
const filenameVarName = '__vite_injected_original_filename'
const importMetaUrlVarName = '__vite_injected_original_import_meta_url'
const result = await build({
absWorkingDir: process.cwd(),
entryPoints: [userConfigPath],
write: false,
target: [`node${process.versions.node}`],
const importMetaResolveVarName =
'__vite_injected_original_import_meta_resolve'
const importMetaResolveRegex = /import\.meta\s*\.\s*resolve/

const bundle = await rolldown({
input: userConfigPath,
platform: 'node',
bundle: true,
format: 'esm',
mainFields: ['main'],
sourcemap: 'inline',
metafile: true,
define: {
'__dirname': dirnameVarName,
'__filename': filenameVarName,
'import.meta.url': importMetaUrlVarName,
'import.meta.dirname': dirnameVarName,
'import.meta.filename': filenameVarName,
resolve: {
mainFields: ['main'],
},
transform: {
define: {
'__dirname': dirnameVarName,
'__filename': filenameVarName,
'import.meta.url': importMetaUrlVarName,
'import.meta.dirname': dirnameVarName,
'import.meta.filename': filenameVarName,
'import.meta.resolve': importMetaResolveVarName,
'import.meta.main': 'false',
},
},
treeshake: false,
tsconfig: false,
plugins: [
{
name: 'externalize-deps',
setup(pluginBuild) {
pluginBuild.onResolve({ filter: /.*/ }, ({ path: id }) => {
resolveId: {
filter: { id: /^[^.#].*/ },
handler(id, importer) {
// externalize bare imports
if (!id.startsWith('.') && !path.isAbsolute(id)) {
return {
external: true,
}
if (!importer || path.isAbsolute(id)) {
return
}
return null
})

return { id, external: true }
},
},
},
{
name: 'inject-file-scope-variables',
setup(pluginBuild) {
pluginBuild.onLoad({ filter: /\.[cm]?[jt]s$/ }, async (args) => {
const contents = await fs.readFile(args.path, 'utf-8')
const injectValues =
`const ${dirnameVarName} = ${JSON.stringify(
path.dirname(args.path),
)};` +
`const ${filenameVarName} = ${JSON.stringify(args.path)};` +
transform: {
filter: { id: /\.[cm]?[jt]s$/ },
handler(code, id) {
let injectValues =
`const ${dirnameVarName} = ${JSON.stringify(path.dirname(id))};` +
`const ${filenameVarName} = ${JSON.stringify(id)};` +
`const ${importMetaUrlVarName} = ${JSON.stringify(
pathToFileURL(args.path).href,
pathToFileURL(id).href,
)};`

if (importMetaResolveRegex.test(code)) {
injectValues += `const ${importMetaResolveVarName} = (specifier, importer = ${importMetaUrlVarName}) => import.meta.resolve(specifier, importer);`
}

let injectedContents: string
if (code.startsWith('#!')) {
// hashbang
let firstLineEndIndex = code.indexOf('\n')
if (firstLineEndIndex < 0) firstLineEndIndex = code.length
injectedContents =
code.slice(0, firstLineEndIndex + 1) +
injectValues +
code.slice(firstLineEndIndex + 1)
} else {
injectedContents = injectValues + code
}

return {
loader: args.path.endsWith('ts') ? 'ts' : 'js',
contents: injectValues + contents,
code: injectedContents,
map: null,
}
})
},
},
},
],
})

const result = await bundle.generate({
format: 'esm',
sourcemap: 'inline',
sourcemapPathTransform(relative) {
return path.resolve(path.dirname(userConfigPath), relative)
},
codeSplitting: false,
})
await bundle.close()

const entryChunk = result.output.find(
(chunk): chunk is OutputChunk => chunk.type === 'chunk' && chunk.isEntry,
)!

const bundleChunks = Object.fromEntries(
result.output.flatMap((c) => (c.type === 'chunk' ? [[c.fileName, c]] : [])),
)

const userConfigDependencies: string[] = []
const seen = new Set<string>()
collectAllModules(bundleChunks, entryChunk.fileName, seen)
for (const modId of seen) {
if (!modId.startsWith('\0')) {
userConfigDependencies.push(modId)
}
}

// add hash to temp file name to avoid naming conflict, and avoid import cache when reloading in dev mode.
// notice that currently we could not delete import cache when using esm like cjs `require.cache`, so it
// could be kind of "memory leak" after modifying and reloading config file too many times.
const { text } = result.outputFiles[0]
const { code: text } = entryChunk
const tempFilePath = `${userConfigPath}.${hash(text)}.mjs`
let userConfig: UserConfig
try {
await fs.writeFile(tempFilePath, text)
userConfig = await importFileDefault(tempFilePath)
} finally {
await fs.rm(tempFilePath)
fs.unlink(tempFilePath, () => {}) // Ignore errors
}

return {
userConfig,
userConfigDependencies: Object.keys(result.metafile.inputs),
userConfigDependencies,
}
}
Loading
Loading