Skip to content

Commit

Permalink
feat: use module runner to import the config (#18637)
Browse files Browse the repository at this point in the history
Co-authored-by: bluwy <[email protected]>
Co-authored-by: Hiroshi Ogawa <[email protected]>
  • Loading branch information
3 people authored Jan 23, 2025
1 parent 93d5443 commit b7e0e42
Show file tree
Hide file tree
Showing 23 changed files with 357 additions and 90 deletions.
4 changes: 4 additions & 0 deletions docs/config/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ You can also explicitly specify a config file to use with the `--config` CLI opt
vite --config my-config.js
```

::: tip BUNDLING THE CONFIG
By default, Vite uses `esbuild` to bundle the config into a temporary file. This can cause issues when importing TypeScript files in a monorepo. If you encounter any issues with this approach, you can specify `--configLoader=runner` to use the module runner instead - it will not create a temporary config and will transform any files on the fly. Note that module runner doesn't support CJS in config files, but external CJS packages should work as usual.
:::

## Config Intellisense

Since Vite ships with TypeScript typings, you can leverage your IDE's intellisense with jsdoc type hints:
Expand Down
138 changes: 71 additions & 67 deletions docs/guide/cli.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/vite/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const asyncFunctions = [
'loadConfigFromFile',
'preprocessCSS',
'createBuilder',
'runnerImport',
]
asyncFunctions.forEach((name) => {
module.exports[name] = (...args) =>
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/module-runner/esmEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import type { ModuleEvaluator, ModuleRunnerContext } from './types'

export class ESModulesEvaluator implements ModuleEvaluator {
startOffset = getAsyncFunctionDeclarationPaddingLineCount()
public readonly startOffset = getAsyncFunctionDeclarationPaddingLineCount()

async runInlinedModule(
context: ModuleRunnerContext,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
interface Test {
field: true
}

export const test: Test = {
field: true,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = {}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'ok'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default () => import('./dynamic-import-dep')
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Plugin } from 'vite'

export default function testPlugin(): Plugin {
return {
name: 'test',
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import parent from '@vitejs/parent'

export default {
__injected: parent.child,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import plugin from './plugin'

export default defineConfig({
root: './test',
plugins: [plugin()],
})
1 change: 1 addition & 0 deletions packages/vite/src/node/__tests__/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"private": true,
"version": "0.0.0",
"dependencies": {
"@vitejs/parent": "link:./packages/parent",
"@vitejs/cjs-ssr-dep": "link:./fixtures/cjs-ssr-dep",
"@vitejs/test-dep-conditions": "file:./fixtures/test-dep-conditions"
}
Expand Down
1 change: 1 addition & 0 deletions packages/vite/src/node/__tests__/packages/child/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default true
5 changes: 5 additions & 0 deletions packages/vite/src/node/__tests__/packages/child/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "@vitejs/child",
"type": "module",
"main": "./index.js"
}
6 changes: 6 additions & 0 deletions packages/vite/src/node/__tests__/packages/parent/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// @ts-expect-error not typed
import child from '@vitejs/child'

export default {
child,
}
8 changes: 8 additions & 0 deletions packages/vite/src/node/__tests__/packages/parent/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@vitejs/parent",
"type": "module",
"main": "./index.ts",
"dependencies": {
"@vitejs/child": "link:../child"
}
}
73 changes: 73 additions & 0 deletions packages/vite/src/node/__tests__/runnerImport.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { resolve } from 'node:path'
import { describe, expect, test } from 'vitest'
import { loadConfigFromFile } from 'vite'
import { runnerImport } from '../ssr/runnerImport'
import { slash } from '../../shared/utils'

describe('importing files using inlined environment', () => {
const fixture = (name: string) =>
resolve(import.meta.dirname, './fixtures/runner-import', name)

test('importing a basic file works', async () => {
const { module } = await runnerImport<
typeof import('./fixtures/runner-import/basic')
>(fixture('basic'))
expect(module.test).toEqual({
field: true,
})
})

test("cannot import cjs, 'runnerImport' doesn't support CJS syntax at all", async () => {
await expect(() =>
runnerImport<typeof import('./fixtures/runner-import/basic')>(
fixture('cjs.js'),
),
).rejects.toThrow('module is not defined')
})

test('can import vite config', async () => {
const { module, dependencies } = await runnerImport<
typeof import('./fixtures/runner-import/vite.config')
>(fixture('vite.config'))
expect(module.default).toEqual({
root: './test',
plugins: [
{
name: 'test',
},
],
})
expect(dependencies).toEqual([slash(fixture('plugin.ts'))])
})

test('can import vite config that imports a TS external module', async () => {
const { module, dependencies } = await runnerImport<
typeof import('./fixtures/runner-import/vite.config.outside-pkg-import.mjs')
>(fixture('vite.config.outside-pkg-import.mts'))

expect(module.default.__injected).toBe(true)
expect(dependencies).toEqual([
slash(resolve(import.meta.dirname, './packages/parent/index.ts')),
])

// confirm that it fails with a bundle approach
await expect(async () => {
const root = resolve(import.meta.dirname, './fixtures/runner-import')
await loadConfigFromFile(
{ mode: 'production', command: 'serve' },
resolve(root, './vite.config.outside-pkg-import.mts'),
root,
'silent',
)
}).rejects.toThrow('Unknown file extension ".ts"')
})

test('dynamic import', async () => {
const { module } = await runnerImport<any>(fixture('dynamic-import.ts'))
await expect(() => module.default()).rejects.toMatchInlineSnapshot(
`[Error: Vite module runner has been closed.]`,
)
// const dep = await module.default();
// expect(dep.default).toMatchInlineSnapshot(`"ok"`)
})
})
10 changes: 10 additions & 0 deletions packages/vite/src/node/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ interface GlobalCLIOptions {
l?: LogLevel
logLevel?: LogLevel
clearScreen?: boolean
configLoader?: 'bundle' | 'runner'
d?: boolean | string
debug?: boolean | string
f?: string
Expand Down Expand Up @@ -87,6 +88,7 @@ function cleanGlobalCLIOptions<Options extends GlobalCLIOptions>(
delete ret.l
delete ret.logLevel
delete ret.clearScreen
delete ret.configLoader
delete ret.d
delete ret.debug
delete ret.f
Expand Down Expand Up @@ -151,6 +153,10 @@ cli
})
.option('-l, --logLevel <level>', `[string] info | warn | error | silent`)
.option('--clearScreen', `[boolean] allow/disable clear screen when logging`)
.option(
'--configLoader <loader>',
`[string] use 'bundle' to bundle the config with esbuild or 'runner' (experimental) to process it on the fly (default: bundle)`,
)
.option('-d, --debug [feat]', `[string | boolean] show debug logs`)
.option('-f, --filter <filter>', `[string] filter debug logs`)
.option('-m, --mode <mode>', `[string] set env mode`)
Expand Down Expand Up @@ -180,6 +186,7 @@ cli
base: options.base,
mode: options.mode,
configFile: options.config,
configLoader: options.configLoader,
logLevel: options.logLevel,
clearScreen: options.clearScreen,
optimizeDeps: { force: options.force },
Expand Down Expand Up @@ -304,6 +311,7 @@ cli
base: options.base,
mode: options.mode,
configFile: options.config,
configLoader: options.configLoader,
logLevel: options.logLevel,
clearScreen: options.clearScreen,
build: buildOptions,
Expand Down Expand Up @@ -340,6 +348,7 @@ cli
root,
base: options.base,
configFile: options.config,
configLoader: options.configLoader,
logLevel: options.logLevel,
mode: options.mode,
},
Expand Down Expand Up @@ -382,6 +391,7 @@ cli
root,
base: options.base,
configFile: options.config,
configLoader: options.configLoader,
logLevel: options.logLevel,
mode: options.mode,
build: {
Expand Down
77 changes: 55 additions & 22 deletions packages/vite/src/node/config.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import fs from 'node:fs'
import fsp from 'node:fs/promises'
import path from 'node:path'
import fsp from 'node:fs/promises'
import { pathToFileURL } from 'node:url'
import { promisify } from 'node:util'
import { performance } from 'node:perf_hooks'
import { createRequire } from 'node:module'
import crypto from 'node:crypto'
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 { build } from 'esbuild'
import type { AnymatchFn } from '../types/anymatch'
import { withTrailingSlash } from '../shared/utils'
import {
Expand Down Expand Up @@ -83,12 +83,12 @@ import {
resolvePlugins,
} from './plugins'
import type { ESBuildOptions } from './plugins/esbuild'
import type {
EnvironmentResolveOptions,
InternalResolveOptions,
ResolveOptions,
import {
type EnvironmentResolveOptions,
type InternalResolveOptions,
type ResolveOptions,
tryNodeResolve,
} 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 @@ -100,6 +100,7 @@ import type { ResolvedSSROptions, SSROptions } from './ssr'
import { resolveSSROptions, ssrConfigDefaults } from './ssr'
import { PartialEnvironment } from './baseEnvironment'
import { createIdResolver } from './idResolver'
import { runnerImport } from './ssr/runnerImport'
import { getAdditionalAllowedHosts } from './server/middlewares/hostCheck'

const debug = createDebugger('vite:config', { depth: 10 })
Expand Down Expand Up @@ -543,6 +544,8 @@ export interface ResolvedWorkerOptions {

export interface InlineConfig extends UserConfig {
configFile?: string | false
/** @experimental */
configLoader?: 'bundle' | 'runner'
envFile?: false
}

Expand Down Expand Up @@ -1024,6 +1027,7 @@ export async function resolveConfig(
config.root,
config.logLevel,
config.customLogger,
config.configLoader,
)
if (loadResult) {
config = mergeConfig(loadResult.config, config)
Expand Down Expand Up @@ -1680,11 +1684,18 @@ export async function loadConfigFromFile(
configRoot: string = process.cwd(),
logLevel?: LogLevel,
customLogger?: Logger,
configLoader: 'bundle' | 'runner' = 'bundle',
): Promise<{
path: string
config: UserConfig
dependencies: string[]
} | null> {
if (configLoader !== 'bundle' && configLoader !== 'runner') {
throw new Error(
`Unsupported configLoader: ${configLoader}. Accepted values are 'bundle' and 'runner'.`,
)
}

const start = performance.now()
const getTime = () => `${(performance.now() - start).toFixed(2)}ms`

Expand All @@ -1710,28 +1721,23 @@ 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,
)
debug?.(`bundled config file loaded in ${getTime()}`)

const config = await (typeof userConfig === 'function'
? userConfig(configEnv)
: userConfig)
const resolver =
configLoader === 'bundle' ? bundleAndLoadConfigFile : importConfigFile
const { configExport, dependencies } = await resolver(resolvedPath)
debug?.(`config file loaded in ${getTime()}`)

const config = await (typeof configExport === 'function'
? configExport(configEnv)
: configExport)
if (!isObject(config)) {
throw new Error(`config must export or return an object.`)
}

return {
path: normalizePath(resolvedPath),
config,
dependencies: bundled.dependencies,
dependencies,
}
} catch (e) {
createLogger(logLevel, { customLogger }).error(
Expand All @@ -1744,6 +1750,33 @@ export async function loadConfigFromFile(
}
}

async function importConfigFile(resolvedPath: string) {
const { module, dependencies } = await runnerImport<{
default: UserConfigExport
}>(resolvedPath)
return {
configExport: module.default,
dependencies,
}
}

async function bundleAndLoadConfigFile(resolvedPath: string) {
const isESM =
typeof process.versions.deno === 'string' || isFilePathESM(resolvedPath)

const bundled = await bundleConfigFile(resolvedPath, isESM)
const userConfig = await loadConfigFromBundledFile(
resolvedPath,
bundled.code,
isESM,
)

return {
configExport: userConfig,
dependencies: bundled.dependencies,
}
}

async function bundleConfigFile(
fileName: string,
isESM: boolean,
Expand Down
1 change: 1 addition & 0 deletions packages/vite/src/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export {
DevEnvironment,
type DevEnvironmentContext,
} from './server/environment'
export { runnerImport } from './ssr/runnerImport'
export { BuildEnvironment } from './build'

export { fetchModule, type FetchModuleOptions } from './ssr/fetchModule'
Expand Down
Loading

0 comments on commit b7e0e42

Please sign in to comment.