From b7e0e42098dd2d42285a9d3c4f39c48a580367e7 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 23 Jan 2025 13:26:53 +0100 Subject: [PATCH] feat: use module runner to import the config (#18637) Co-authored-by: bluwy Co-authored-by: Hiroshi Ogawa --- docs/config/index.md | 4 + docs/guide/cli.md | 138 +++++++++--------- packages/vite/index.cjs | 1 + .../vite/src/module-runner/esmEvaluator.ts | 2 +- .../__tests__/fixtures/runner-import/basic.ts | 7 + .../__tests__/fixtures/runner-import/cjs.js | 1 + .../runner-import/dynamic-import-dep.ts | 1 + .../fixtures/runner-import/dynamic-import.ts | 1 + .../fixtures/runner-import/plugin.ts | 7 + .../vite.config.outside-pkg-import.mts | 5 + .../fixtures/runner-import/vite.config.ts | 7 + packages/vite/src/node/__tests__/package.json | 1 + .../node/__tests__/packages/child/index.js | 1 + .../__tests__/packages/child/package.json | 5 + .../node/__tests__/packages/parent/index.ts | 6 + .../__tests__/packages/parent/package.json | 8 + .../src/node/__tests__/runnerImport.spec.ts | 73 +++++++++ packages/vite/src/node/cli.ts | 10 ++ packages/vite/src/node/config.ts | 77 +++++++--- packages/vite/src/node/index.ts | 1 + packages/vite/src/node/ssr/runnerImport.ts | 76 ++++++++++ pnpm-lock.yaml | 11 ++ vitest.config.ts | 4 + 23 files changed, 357 insertions(+), 90 deletions(-) create mode 100644 packages/vite/src/node/__tests__/fixtures/runner-import/basic.ts create mode 100644 packages/vite/src/node/__tests__/fixtures/runner-import/cjs.js create mode 100644 packages/vite/src/node/__tests__/fixtures/runner-import/dynamic-import-dep.ts create mode 100644 packages/vite/src/node/__tests__/fixtures/runner-import/dynamic-import.ts create mode 100644 packages/vite/src/node/__tests__/fixtures/runner-import/plugin.ts create mode 100644 packages/vite/src/node/__tests__/fixtures/runner-import/vite.config.outside-pkg-import.mts create mode 100644 packages/vite/src/node/__tests__/fixtures/runner-import/vite.config.ts create mode 100644 packages/vite/src/node/__tests__/packages/child/index.js create mode 100644 packages/vite/src/node/__tests__/packages/child/package.json create mode 100644 packages/vite/src/node/__tests__/packages/parent/index.ts create mode 100644 packages/vite/src/node/__tests__/packages/parent/package.json create mode 100644 packages/vite/src/node/__tests__/runnerImport.spec.ts create mode 100644 packages/vite/src/node/ssr/runnerImport.ts diff --git a/docs/config/index.md b/docs/config/index.md index 0583f69bd52d13..3dac90dc386d06 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -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: diff --git a/docs/guide/cli.md b/docs/guide/cli.md index eb3254be98b80a..faaeefa18cb20f 100644 --- a/docs/guide/cli.md +++ b/docs/guide/cli.md @@ -14,24 +14,25 @@ vite [root] #### Options -| Options | | -| ------------------------ | ------------------------------------------------------------------------------------------------------------------ | -| `--host [host]` | Specify hostname (`string`) | -| `--port ` | Specify port (`number`) | -| `--open [path]` | Open browser on startup (`boolean \| string`) | -| `--cors` | Enable CORS (`boolean`) | -| `--strictPort` | Exit if specified port is already in use (`boolean`) | -| `--force` | Force the optimizer to ignore the cache and re-bundle (`boolean`) | -| `-c, --config ` | Use specified config file (`string`) | -| `--base ` | Public base path (default: `/`) (`string`) | -| `-l, --logLevel ` | info \| warn \| error \| silent (`string`) | -| `--clearScreen` | Allow/disable clear screen when logging (`boolean`) | -| `--profile` | Start built-in Node.js inspector (check [Performance bottlenecks](/guide/troubleshooting#performance-bottlenecks)) | -| `-d, --debug [feat]` | Show debug logs (`string \| boolean`) | -| `-f, --filter ` | Filter debug logs (`string`) | -| `-m, --mode ` | Set env mode (`string`) | -| `-h, --help` | Display available CLI options | -| `-v, --version` | Display version number | +| Options | | +| ------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `--host [host]` | Specify hostname (`string`) | +| `--port ` | Specify port (`number`) | +| `--open [path]` | Open browser on startup (`boolean \| string`) | +| `--cors` | Enable CORS (`boolean`) | +| `--strictPort` | Exit if specified port is already in use (`boolean`) | +| `--force` | Force the optimizer to ignore the cache and re-bundle (`boolean`) | +| `-c, --config ` | Use specified config file (`string`) | +| `--base ` | Public base path (default: `/`) (`string`) | +| `-l, --logLevel ` | info \| warn \| error \| silent (`string`) | +| `--clearScreen` | Allow/disable clear screen when logging (`boolean`) | +| `--configLoader ` | Use `bundle` to bundle the config with esbuild or `runner` (experimental) to process it on the fly (default: `bundle`) | +| `--profile` | Start built-in Node.js inspector (check [Performance bottlenecks](/guide/troubleshooting#performance-bottlenecks)) | +| `-d, --debug [feat]` | Show debug logs (`string \| boolean`) | +| `-f, --filter ` | Filter debug logs (`string`) | +| `-m, --mode ` | Set env mode (`string`) | +| `-h, --help` | Display available CLI options | +| `-v, --version` | Display version number | ## Build @@ -47,29 +48,30 @@ vite build [root] #### Options -| Options | | -| ------------------------------ | ------------------------------------------------------------------------------------------------------------------- | -| `--target ` | Transpile target (default: `"modules"`) (`string`) | -| `--outDir ` | Output directory (default: `dist`) (`string`) | -| `--assetsDir ` | Directory under outDir to place assets in (default: `"assets"`) (`string`) | -| `--assetsInlineLimit ` | Static asset base64 inline threshold in bytes (default: `4096`) (`number`) | -| `--ssr [entry]` | Build specified entry for server-side rendering (`string`) | -| `--sourcemap [output]` | Output source maps for build (default: `false`) (`boolean \| "inline" \| "hidden"`) | -| `--minify [minifier]` | Enable/disable minification, or specify minifier to use (default: `"esbuild"`) (`boolean \| "terser" \| "esbuild"`) | -| `--manifest [name]` | Emit build manifest json (`boolean \| string`) | -| `--ssrManifest [name]` | Emit ssr manifest json (`boolean \| string`) | -| `--emptyOutDir` | Force empty outDir when it's outside of root (`boolean`) | -| `-w, --watch` | Rebuilds when modules have changed on disk (`boolean`) | -| `-c, --config ` | Use specified config file (`string`) | -| `--base ` | Public base path (default: `/`) (`string`) | -| `-l, --logLevel ` | Info \| warn \| error \| silent (`string`) | -| `--clearScreen` | Allow/disable clear screen when logging (`boolean`) | -| `--profile` | Start built-in Node.js inspector (check [Performance bottlenecks](/guide/troubleshooting#performance-bottlenecks)) | -| `-d, --debug [feat]` | Show debug logs (`string \| boolean`) | -| `-f, --filter ` | Filter debug logs (`string`) | -| `-m, --mode ` | Set env mode (`string`) | -| `-h, --help` | Display available CLI options | -| `--app` | Build all environments, same as `builder: {}` (`boolean`, experimental) | +| Options | | +| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | +| `--target ` | Transpile target (default: `"modules"`) (`string`) | +| `--outDir ` | Output directory (default: `dist`) (`string`) | +| `--assetsDir ` | Directory under outDir to place assets in (default: `"assets"`) (`string`) | +| `--assetsInlineLimit ` | Static asset base64 inline threshold in bytes (default: `4096`) (`number`) | +| `--ssr [entry]` | Build specified entry for server-side rendering (`string`) | +| `--sourcemap [output]` | Output source maps for build (default: `false`) (`boolean \| "inline" \| "hidden"`) | +| `--minify [minifier]` | Enable/disable minification, or specify minifier to use (default: `"esbuild"`) (`boolean \| "terser" \| "esbuild"`) | +| `--manifest [name]` | Emit build manifest json (`boolean \| string`) | +| `--ssrManifest [name]` | Emit ssr manifest json (`boolean \| string`) | +| `--emptyOutDir` | Force empty outDir when it's outside of root (`boolean`) | +| `-w, --watch` | Rebuilds when modules have changed on disk (`boolean`) | +| `-c, --config ` | Use specified config file (`string`) | +| `--base ` | Public base path (default: `/`) (`string`) | +| `-l, --logLevel ` | Info \| warn \| error \| silent (`string`) | +| `--clearScreen` | Allow/disable clear screen when logging (`boolean`) | +| `--configLoader ` | Use `bundle` to bundle the config with esbuild or `runner` (experimental) to process it on the fly (default: `bundle`) | +| `--profile` | Start built-in Node.js inspector (check [Performance bottlenecks](/guide/troubleshooting#performance-bottlenecks)) | +| `-d, --debug [feat]` | Show debug logs (`string \| boolean`) | +| `-f, --filter ` | Filter debug logs (`string`) | +| `-m, --mode ` | Set env mode (`string`) | +| `-h, --help` | Display available CLI options | +| `--app` | Build all environments, same as `builder: {}` (`boolean`, experimental) | ## Others @@ -85,17 +87,18 @@ vite optimize [root] #### Options -| Options | | -| ------------------------ | ----------------------------------------------------------------- | -| `--force` | Force the optimizer to ignore the cache and re-bundle (`boolean`) | -| `-c, --config ` | Use specified config file (`string`) | -| `--base ` | Public base path (default: `/`) (`string`) | -| `-l, --logLevel ` | Info \| warn \| error \| silent (`string`) | -| `--clearScreen` | Allow/disable clear screen when logging (`boolean`) | -| `-d, --debug [feat]` | Show debug logs (`string \| boolean`) | -| `-f, --filter ` | Filter debug logs (`string`) | -| `-m, --mode ` | Set env mode (`string`) | -| `-h, --help` | Display available CLI options | +| Options | | +| ------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `--force` | Force the optimizer to ignore the cache and re-bundle (`boolean`) | +| `-c, --config ` | Use specified config file (`string`) | +| `--base ` | Public base path (default: `/`) (`string`) | +| `-l, --logLevel ` | Info \| warn \| error \| silent (`string`) | +| `--clearScreen` | Allow/disable clear screen when logging (`boolean`) | +| `--configLoader ` | Use `bundle` to bundle the config with esbuild or `runner` (experimental) to process it on the fly (default: `bundle`) | +| `-d, --debug [feat]` | Show debug logs (`string \| boolean`) | +| `-f, --filter ` | Filter debug logs (`string`) | +| `-m, --mode ` | Set env mode (`string`) | +| `-h, --help` | Display available CLI options | ### `vite preview` @@ -109,18 +112,19 @@ vite preview [root] #### Options -| Options | | -| ------------------------ | ---------------------------------------------------- | -| `--host [host]` | Specify hostname (`string`) | -| `--port ` | Specify port (`number`) | -| `--strictPort` | Exit if specified port is already in use (`boolean`) | -| `--open [path]` | Open browser on startup (`boolean \| string`) | -| `--outDir ` | Output directory (default: `dist`)(`string`) | -| `-c, --config ` | Use specified config file (`string`) | -| `--base ` | Public base path (default: `/`) (`string`) | -| `-l, --logLevel ` | Info \| warn \| error \| silent (`string`) | -| `--clearScreen` | Allow/disable clear screen when logging (`boolean`) | -| `-d, --debug [feat]` | Show debug logs (`string \| boolean`) | -| `-f, --filter ` | Filter debug logs (`string`) | -| `-m, --mode ` | Set env mode (`string`) | -| `-h, --help` | Display available CLI options | +| Options | | +| ------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `--host [host]` | Specify hostname (`string`) | +| `--port ` | Specify port (`number`) | +| `--strictPort` | Exit if specified port is already in use (`boolean`) | +| `--open [path]` | Open browser on startup (`boolean \| string`) | +| `--outDir ` | Output directory (default: `dist`)(`string`) | +| `-c, --config ` | Use specified config file (`string`) | +| `--base ` | Public base path (default: `/`) (`string`) | +| `-l, --logLevel ` | Info \| warn \| error \| silent (`string`) | +| `--clearScreen` | Allow/disable clear screen when logging (`boolean`) | +| `--configLoader ` | Use `bundle` to bundle the config with esbuild or `runner` (experimental) to process it on the fly (default: `bundle`) | +| `-d, --debug [feat]` | Show debug logs (`string \| boolean`) | +| `-f, --filter ` | Filter debug logs (`string`) | +| `-m, --mode ` | Set env mode (`string`) | +| `-h, --help` | Display available CLI options | diff --git a/packages/vite/index.cjs b/packages/vite/index.cjs index 70515aa90c7a8d..c46eb3b7333e7d 100644 --- a/packages/vite/index.cjs +++ b/packages/vite/index.cjs @@ -21,6 +21,7 @@ const asyncFunctions = [ 'loadConfigFromFile', 'preprocessCSS', 'createBuilder', + 'runnerImport', ] asyncFunctions.forEach((name) => { module.exports[name] = (...args) => diff --git a/packages/vite/src/module-runner/esmEvaluator.ts b/packages/vite/src/module-runner/esmEvaluator.ts index f7f8c8ab52de80..003d6b2d242b88 100644 --- a/packages/vite/src/module-runner/esmEvaluator.ts +++ b/packages/vite/src/module-runner/esmEvaluator.ts @@ -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, diff --git a/packages/vite/src/node/__tests__/fixtures/runner-import/basic.ts b/packages/vite/src/node/__tests__/fixtures/runner-import/basic.ts new file mode 100644 index 00000000000000..2fee8764679469 --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/runner-import/basic.ts @@ -0,0 +1,7 @@ +interface Test { + field: true +} + +export const test: Test = { + field: true, +} diff --git a/packages/vite/src/node/__tests__/fixtures/runner-import/cjs.js b/packages/vite/src/node/__tests__/fixtures/runner-import/cjs.js new file mode 100644 index 00000000000000..4ba52ba2c8df67 --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/runner-import/cjs.js @@ -0,0 +1 @@ +module.exports = {} diff --git a/packages/vite/src/node/__tests__/fixtures/runner-import/dynamic-import-dep.ts b/packages/vite/src/node/__tests__/fixtures/runner-import/dynamic-import-dep.ts new file mode 100644 index 00000000000000..60c71f346d9a3e --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/runner-import/dynamic-import-dep.ts @@ -0,0 +1 @@ +export default 'ok' diff --git a/packages/vite/src/node/__tests__/fixtures/runner-import/dynamic-import.ts b/packages/vite/src/node/__tests__/fixtures/runner-import/dynamic-import.ts new file mode 100644 index 00000000000000..b406cfff6e5ac2 --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/runner-import/dynamic-import.ts @@ -0,0 +1 @@ +export default () => import('./dynamic-import-dep') diff --git a/packages/vite/src/node/__tests__/fixtures/runner-import/plugin.ts b/packages/vite/src/node/__tests__/fixtures/runner-import/plugin.ts new file mode 100644 index 00000000000000..173c7d928c4f17 --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/runner-import/plugin.ts @@ -0,0 +1,7 @@ +import { Plugin } from 'vite' + +export default function testPlugin(): Plugin { + return { + name: 'test', + } +} diff --git a/packages/vite/src/node/__tests__/fixtures/runner-import/vite.config.outside-pkg-import.mts b/packages/vite/src/node/__tests__/fixtures/runner-import/vite.config.outside-pkg-import.mts new file mode 100644 index 00000000000000..86fea8ffe713f7 --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/runner-import/vite.config.outside-pkg-import.mts @@ -0,0 +1,5 @@ +import parent from '@vitejs/parent' + +export default { + __injected: parent.child, +} diff --git a/packages/vite/src/node/__tests__/fixtures/runner-import/vite.config.ts b/packages/vite/src/node/__tests__/fixtures/runner-import/vite.config.ts new file mode 100644 index 00000000000000..2cbd562498124b --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/runner-import/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import plugin from './plugin' + +export default defineConfig({ + root: './test', + plugins: [plugin()], +}) diff --git a/packages/vite/src/node/__tests__/package.json b/packages/vite/src/node/__tests__/package.json index 90a5d7c132c1ee..4f6f029f385542 100644 --- a/packages/vite/src/node/__tests__/package.json +++ b/packages/vite/src/node/__tests__/package.json @@ -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" } diff --git a/packages/vite/src/node/__tests__/packages/child/index.js b/packages/vite/src/node/__tests__/packages/child/index.js new file mode 100644 index 00000000000000..186b120756be19 --- /dev/null +++ b/packages/vite/src/node/__tests__/packages/child/index.js @@ -0,0 +1 @@ +export default true diff --git a/packages/vite/src/node/__tests__/packages/child/package.json b/packages/vite/src/node/__tests__/packages/child/package.json new file mode 100644 index 00000000000000..77e2aa64615b63 --- /dev/null +++ b/packages/vite/src/node/__tests__/packages/child/package.json @@ -0,0 +1,5 @@ +{ + "name": "@vitejs/child", + "type": "module", + "main": "./index.js" +} diff --git a/packages/vite/src/node/__tests__/packages/parent/index.ts b/packages/vite/src/node/__tests__/packages/parent/index.ts new file mode 100644 index 00000000000000..747305283cadb2 --- /dev/null +++ b/packages/vite/src/node/__tests__/packages/parent/index.ts @@ -0,0 +1,6 @@ +// @ts-expect-error not typed +import child from '@vitejs/child' + +export default { + child, +} diff --git a/packages/vite/src/node/__tests__/packages/parent/package.json b/packages/vite/src/node/__tests__/packages/parent/package.json new file mode 100644 index 00000000000000..d966448a0560a8 --- /dev/null +++ b/packages/vite/src/node/__tests__/packages/parent/package.json @@ -0,0 +1,8 @@ +{ + "name": "@vitejs/parent", + "type": "module", + "main": "./index.ts", + "dependencies": { + "@vitejs/child": "link:../child" + } +} diff --git a/packages/vite/src/node/__tests__/runnerImport.spec.ts b/packages/vite/src/node/__tests__/runnerImport.spec.ts new file mode 100644 index 00000000000000..d6084b84bbbf30 --- /dev/null +++ b/packages/vite/src/node/__tests__/runnerImport.spec.ts @@ -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( + 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(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"`) + }) +}) diff --git a/packages/vite/src/node/cli.ts b/packages/vite/src/node/cli.ts index b520b9b5144b46..bb86e1c6e8a34a 100644 --- a/packages/vite/src/node/cli.ts +++ b/packages/vite/src/node/cli.ts @@ -23,6 +23,7 @@ interface GlobalCLIOptions { l?: LogLevel logLevel?: LogLevel clearScreen?: boolean + configLoader?: 'bundle' | 'runner' d?: boolean | string debug?: boolean | string f?: string @@ -87,6 +88,7 @@ function cleanGlobalCLIOptions( delete ret.l delete ret.logLevel delete ret.clearScreen + delete ret.configLoader delete ret.d delete ret.debug delete ret.f @@ -151,6 +153,10 @@ cli }) .option('-l, --logLevel ', `[string] info | warn | error | silent`) .option('--clearScreen', `[boolean] allow/disable clear screen when logging`) + .option( + '--configLoader ', + `[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 ', `[string] filter debug logs`) .option('-m, --mode ', `[string] set env mode`) @@ -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 }, @@ -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, @@ -340,6 +348,7 @@ cli root, base: options.base, configFile: options.config, + configLoader: options.configLoader, logLevel: options.logLevel, mode: options.mode, }, @@ -382,6 +391,7 @@ cli root, base: options.base, configFile: options.config, + configLoader: options.configLoader, logLevel: options.logLevel, mode: options.mode, build: { diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 9c2e36cd9d9ea3..cc697229940a3d 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -1,6 +1,6 @@ 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' @@ -8,9 +8,9 @@ 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 { @@ -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' @@ -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 }) @@ -543,6 +544,8 @@ export interface ResolvedWorkerOptions { export interface InlineConfig extends UserConfig { configFile?: string | false + /** @experimental */ + configLoader?: 'bundle' | 'runner' envFile?: false } @@ -1024,6 +1027,7 @@ export async function resolveConfig( config.root, config.logLevel, config.customLogger, + config.configLoader, ) if (loadResult) { config = mergeConfig(loadResult.config, config) @@ -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` @@ -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( @@ -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, diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index ae52db468474f1..f27b510da63358 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -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' diff --git a/packages/vite/src/node/ssr/runnerImport.ts b/packages/vite/src/node/ssr/runnerImport.ts new file mode 100644 index 00000000000000..d24f9e58a55368 --- /dev/null +++ b/packages/vite/src/node/ssr/runnerImport.ts @@ -0,0 +1,76 @@ +import type { InlineConfig } from '../config' +import { resolveConfig } from '../config' +import { createRunnableDevEnvironment } from '../server/environments/runnableEnvironment' +import { mergeConfig } from '../utils' + +interface RunnerImportResult { + module: T + dependencies: string[] +} + +/** + * Import any file using the default Vite environment. + * @experimental + */ +export async function runnerImport( + moduleId: string, + inlineConfig?: InlineConfig, +): Promise> { + const isModuleSyncConditionEnabled = (await import('#module-sync-enabled')) + .default + const config = await resolveConfig( + mergeConfig(inlineConfig || {}, { + configFile: false, + envFile: false, + cacheDir: process.cwd(), + environments: { + inline: { + consumer: 'server', + dev: { + moduleRunnerTransform: true, + }, + resolve: { + external: true, + mainFields: [], + conditions: [ + 'node', + ...(isModuleSyncConditionEnabled ? ['module-sync'] : []), + ], + }, + }, + }, + } satisfies InlineConfig), + 'serve', + ) + const environment = createRunnableDevEnvironment('inline', config, { + runnerOptions: { + hmr: { + logger: false, + }, + }, + hot: false, + }) + await environment.init() + try { + const module = await environment.runner.import(moduleId) + const modules = [ + ...environment.runner.evaluatedModules.urlToIdModuleMap.values(), + ] + const dependencies = modules + .filter((m) => { + // ignore all externalized modules + if (!m.meta || 'externalize' in m.meta) { + return false + } + // ignore the current module + return m.exports !== module + }) + .map((m) => m.file) + return { + module, + dependencies, + } + } finally { + await environment.close() + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 11c7de107d6fd0..e96ceba3003193 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -420,6 +420,9 @@ importers: '@vitejs/cjs-ssr-dep': specifier: link:./fixtures/cjs-ssr-dep version: link:fixtures/cjs-ssr-dep + '@vitejs/parent': + specifier: link:./packages/parent + version: link:packages/parent '@vitejs/test-dep-conditions': specifier: file:./fixtures/test-dep-conditions version: file:packages/vite/src/node/__tests__/fixtures/test-dep-conditions @@ -430,12 +433,20 @@ importers: packages/vite/src/node/__tests__/fixtures/test-dep-conditions: {} + packages/vite/src/node/__tests__/packages/child: {} + packages/vite/src/node/__tests__/packages/module: {} packages/vite/src/node/__tests__/packages/name: {} packages/vite/src/node/__tests__/packages/noname: {} + packages/vite/src/node/__tests__/packages/parent: + dependencies: + '@vitejs/child': + specifier: link:../child + version: link:../child + packages/vite/src/node/server/__tests__/fixtures/lerna/nested: {} packages/vite/src/node/server/__tests__/fixtures/none/nested: {} diff --git a/vitest.config.ts b/vitest.config.ts index 7ee61c4585006d..919e0a30d650bd 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -13,6 +13,10 @@ export default defineConfig({ './playground/**/*.*', './playground-temp/**/*.*', ], + deps: { + // we specify 'packages' so Vitest doesn't inline the files + moduleDirectories: ['node_modules', 'packages'], + }, testTimeout: 20000, isolate: false, },