Skip to content

Commit

Permalink
feat: use module runner to import the config
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va committed Nov 11, 2024
1 parent 67e3e95 commit fa4e6dd
Show file tree
Hide file tree
Showing 2 changed files with 27 additions and 213 deletions.
234 changes: 23 additions & 211 deletions packages/vite/src/node/config.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import fs from 'node:fs'
import fsp from 'node:fs/promises'
import path from 'node:path'
import { pathToFileURL } from 'node:url'
import { promisify } from 'node:util'
import { performance } from 'node:perf_hooks'
import { createRequire } from 'node:module'
import colors from 'picocolors'
import type { Alias, AliasOptions } from 'dep-types/alias'
import { build } from 'esbuild'
import type { RollupOptions } from 'rollup'
import picomatch from 'picomatch'
import type { AnymatchFn } from '../types/anymatch'
Expand Down Expand Up @@ -52,11 +47,8 @@ import {
asyncFlatten,
createDebugger,
createFilter,
isBuiltin,
isExternalUrl,
isFilePathESM,
isInNodeModules,
isNodeBuiltin,
isObject,
isParentDirectory,
mergeAlias,
Expand All @@ -76,7 +68,6 @@ import type {
InternalResolveOptions,
ResolveOptions,
} from './plugins/resolve'
import { tryNodeResolve } from './plugins/resolve'
import type { LogLevel, Logger } from './logger'
import { createLogger } from './logger'
import type { DepOptimizationOptions } from './optimizer'
Expand All @@ -88,9 +79,9 @@ import type { ResolvedSSROptions, SSROptions } from './ssr'
import { resolveSSROptions } from './ssr'
import { PartialEnvironment } from './baseEnvironment'
import { createIdResolver } from './idResolver'
import { createServerModuleRunner } from './ssr/runtime/serverModuleRunner'

const debug = createDebugger('vite:config', { depth: 10 })
const promisifiedRealpath = promisify(fs.realpath)

export interface ConfigEnv {
/**
Expand Down Expand Up @@ -1502,17 +1493,27 @@ export async function loadConfigFromFile(
return null
}

const isESM =
typeof process.versions.deno === 'string' || isFilePathESM(resolvedPath)

try {
const bundled = await bundleConfigFile(resolvedPath, isESM)
const userConfig = await loadConfigFromBundledFile(
resolvedPath,
bundled.code,
isESM,
// console.time('config')
const environment = new DevEnvironment(
'config',
await resolveConfig({ configFile: false }, 'serve'),
{
options: {
consumer: 'server',
dev: {
moduleRunnerTransform: true,
},
},
hot: false,
},
)
debug?.(`bundled config file loaded in ${getTime()}`)
await environment.init()
const runner = createServerModuleRunner(environment)
const { default: userConfig } = (await runner.import(resolvedPath)) as {
default: UserConfigExport
}
debug?.(`config file loaded in ${getTime()}`)

const config = await (typeof userConfig === 'function'
? userConfig(configEnv)
Expand All @@ -1523,7 +1524,7 @@ export async function loadConfigFromFile(
return {
path: normalizePath(resolvedPath),
config,
dependencies: bundled.dependencies,
dependencies: [],
}
} catch (e) {
createLogger(logLevel, { customLogger }).error(
Expand All @@ -1533,197 +1534,8 @@ export async function loadConfigFromFile(
},
)
throw e
}
}

async function bundleConfigFile(
fileName: string,
isESM: boolean,
): Promise<{ code: string; dependencies: string[] }> {
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: [fileName],
write: false,
target: [`node${process.versions.node}`],
platform: 'node',
bundle: true,
format: isESM ? 'esm' : 'cjs',
mainFields: ['main'],
sourcemap: 'inline',
metafile: true,
define: {
__dirname: dirnameVarName,
__filename: filenameVarName,
'import.meta.url': importMetaUrlVarName,
'import.meta.dirname': dirnameVarName,
'import.meta.filename': filenameVarName,
},
plugins: [
{
name: 'externalize-deps',
setup(build) {
const packageCache = new Map()
const resolveByViteResolver = (
id: string,
importer: string,
isRequire: boolean,
) => {
return tryNodeResolve(id, importer, {
root: path.dirname(fileName),
isBuild: true,
isProduction: true,
preferRelative: false,
tryIndex: true,
mainFields: [],
conditions: [],
externalConditions: [],
external: [],
noExternal: [],
overrideConditions: ['node'],
dedupe: [],
extensions: DEFAULT_EXTENSIONS,
preserveSymlinks: false,
packageCache,
isRequire,
webCompatible: false,
})?.id
}

// externalize bare imports
build.onResolve(
{ filter: /^[^.].*/ },
async ({ path: id, importer, kind }) => {
if (
kind === 'entry-point' ||
path.isAbsolute(id) ||
isNodeBuiltin(id)
) {
return
}

// With the `isNodeBuiltin` check above, this check captures if the builtin is a
// non-node built-in, which esbuild doesn't know how to handle. In that case, we
// externalize it so the non-node runtime handles it instead.
if (isBuiltin(id)) {
return { external: true }
}

const isImport = isESM || kind === 'dynamic-import'
let idFsPath: string | undefined
try {
idFsPath = resolveByViteResolver(id, importer, !isImport)
} catch (e) {
if (!isImport) {
let canResolveWithImport = false
try {
canResolveWithImport = !!resolveByViteResolver(
id,
importer,
false,
)
} catch {}
if (canResolveWithImport) {
throw new Error(
`Failed to resolve ${JSON.stringify(
id,
)}. This package is ESM only but it was tried to load by \`require\`. See https://vite.dev/guide/troubleshooting.html#this-package-is-esm-only for more details.`,
)
}
}
throw e
}
if (idFsPath && isImport) {
idFsPath = pathToFileURL(idFsPath).href
}
return {
path: idFsPath,
external: true,
}
},
)
},
},
{
name: 'inject-file-scope-variables',
setup(build) {
build.onLoad({ filter: /\.[cm]?[jt]s$/ }, async (args) => {
const contents = await fsp.readFile(args.path, 'utf-8')
const injectValues =
`const ${dirnameVarName} = ${JSON.stringify(
path.dirname(args.path),
)};` +
`const ${filenameVarName} = ${JSON.stringify(args.path)};` +
`const ${importMetaUrlVarName} = ${JSON.stringify(
pathToFileURL(args.path).href,
)};`

return {
loader: args.path.endsWith('ts') ? 'ts' : 'js',
contents: injectValues + contents,
}
})
},
},
],
})
const { text } = result.outputFiles[0]
return {
code: text,
dependencies: result.metafile ? Object.keys(result.metafile.inputs) : [],
}
}

interface NodeModuleWithCompile extends NodeModule {
_compile(code: string, filename: string): any
}

const _require = createRequire(import.meta.url)
async function loadConfigFromBundledFile(
fileName: string,
bundledCode: string,
isESM: boolean,
): Promise<UserConfigExport> {
// for esm, before we can register loaders without requiring users to run node
// with --experimental-loader themselves, we have to do a hack here:
// write it to disk, load it with native Node ESM, then delete the file.
if (isESM) {
const fileBase = `${fileName}.timestamp-${Date.now()}-${Math.random()
.toString(16)
.slice(2)}`
const fileNameTmp = `${fileBase}.mjs`
const fileUrl = `${pathToFileURL(fileBase)}.mjs`
await fsp.writeFile(fileNameTmp, bundledCode)
try {
return (await import(fileUrl)).default
} finally {
fs.unlink(fileNameTmp, () => {}) // Ignore errors
}
}
// for cjs, we can register a custom loader via `_require.extensions`
else {
const extension = path.extname(fileName)
// We don't use fsp.realpath() here because it has the same behaviour as
// fs.realpath.native. On some Windows systems, it returns uppercase volume
// letters (e.g. "C:\") while the Node.js loader uses lowercase volume letters.
// See https://github.com/vitejs/vite/issues/12923
const realFileName = await promisifiedRealpath(fileName)
const loaderExt = extension in _require.extensions ? extension : '.js'
const defaultLoader = _require.extensions[loaderExt]!
_require.extensions[loaderExt] = (module: NodeModule, filename: string) => {
if (filename === realFileName) {
;(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)
_require.extensions[loaderExt] = defaultLoader
return raw.__esModule ? raw.default : raw
} finally {
// console.timeEnd('config')
}
}

Expand Down
6 changes: 4 additions & 2 deletions packages/vite/src/node/ssr/runtime/serverModuleRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,10 @@ export function createServerModuleRunner(
...options,
root: environment.config.root,
transport: {
fetchModule: (id, importer, options) =>
environment.fetchModule(id, importer, options),
fetchModule: async (id, importer, options) => {
const result = await environment.fetchModule(id, importer, options)
return result
},
},
hmr,
sourcemapInterceptor: resolveSourceMapOptions(options),
Expand Down

0 comments on commit fa4e6dd

Please sign in to comment.