diff --git a/docs/config/build-options.md b/docs/config/build-options.md index 4d4214e6a6b73b..1c7d9c526e615c 100644 --- a/docs/config/build-options.md +++ b/docs/config/build-options.md @@ -192,10 +192,18 @@ When set to `true`, the build will also generate an SSR manifest for determining Produce SSR-oriented build. The value can be a string to directly specify the SSR entry, or `true`, which requires specifying the SSR entry via `rollupOptions.input`. +## build.emitAssets + +- **Type:** `boolean` +- **Default:** `false` + +During non-client builds, static assets aren't emitted as it is assumed they would be emitted as part of the client build. This option allows frameworks to force emitting them in other environments build. It is responsibility of the framework to merge the assets with a post build step. + ## build.ssrEmitAssets - **Type:** `boolean` - **Default:** `false` +- **Deprecated:** use `build.emitAssets` During the SSR build, static assets aren't emitted as it is assumed they would be emitted as part of the client build. This option allows frameworks to force emitting them in both the client and SSR build. It is responsibility of the framework to merge the assets with a post build step. diff --git a/docs/guide/api-javascript.md b/docs/guide/api-javascript.md index eda204c2cf951d..22502a03d9f931 100644 --- a/docs/guide/api-javascript.md +++ b/docs/guide/api-javascript.md @@ -135,6 +135,7 @@ interface ViteDevServer { /** * Programmatically resolve, load and transform a URL and get the result * without going through the http request pipeline. + * @deprecated use environment.transformRequest */ transformRequest( url: string, diff --git a/docs/guide/api-vite-runtime.md b/docs/guide/api-vite-runtime.md index 9aa579d268ddcf..55559a61b37491 100644 --- a/docs/guide/api-vite-runtime.md +++ b/docs/guide/api-vite-runtime.md @@ -26,13 +26,7 @@ export class ViteRuntime { /** * URL to execute. Accepts file path, server path, or id relative to the root. */ - public async executeUrl(url: string): Promise - /** - * Entry point URL to execute. Accepts file path, server path or id relative to the root. - * In the case of a full reload triggered by HMR, this is the module that will be reloaded. - * If this method is called multiple times, all entry points will be reloaded one at a time. - */ - public async executeEntrypoint(url: string): Promise + public async import(url: string): Promise /** * Clear all caches including HMR listeners. */ @@ -57,7 +51,7 @@ The `ViteRuntime` class requires `root` and `fetchModule` options when initiated Runner in `ViteRuntime` is responsible for executing the code. Vite exports `ESModulesRunner` out of the box, it uses `new AsyncFunction` to run the code. You can provide your own implementation if your JavaScript runtime doesn't support unsafe evaluation. -The two main methods that runtime exposes are `executeUrl` and `executeEntrypoint`. The only difference between them is that all modules executed by `executeEntrypoint` will be reexecuted if HMR triggers `full-reload` event. Be aware that Vite Runtime doesn't update `exports` object when this happens (it overrides it), you would need to run `executeUrl` or get the module from `moduleCache` again if you rely on having the latest `exports` object. +Module runner exposes `import` method. When Vite server triggers `full-reload` HMR event, all affected modules will be re-executed. Be aware that Module Runner doesn't update `exports` object when this happens (it overrides it), you would need to run `import` or get the module from `moduleCache` again if you rely on having the latest `exports` object. **Example Usage:** @@ -74,7 +68,7 @@ const runtime = new ViteRuntime( new ESModulesRunner(), ) -await runtime.executeEntrypoint('/src/entry-point.js') +await runtime.import('/src/entry-point.js') ``` ## `ViteRuntimeOptions` @@ -209,7 +203,7 @@ const __dirname = fileURLToPath(new URL('.', import.meta.url)) await server.listen() const runtime = await createViteRuntime(server) - await runtime.executeEntrypoint('/src/entry-point.js') + await runtime.import('/src/entry-point.js') })() ``` diff --git a/packages/vite/CHANGELOG.md b/packages/vite/CHANGELOG.md index b6e258362ff36d..08e7293e470bf2 100644 --- a/packages/vite/CHANGELOG.md +++ b/packages/vite/CHANGELOG.md @@ -1,3 +1,114 @@ +## 6.0.0-alpha.2 (2024-04-09) + +* chore: update ([46c8910](https://github.com/vitejs/vite/commit/46c8910)) +* feat: environment aware define ([9f9a716](https://github.com/vitejs/vite/commit/9f9a716)) +* feat: rework more ssr.target webworker branches ([1f644d0](https://github.com/vitejs/vite/commit/1f644d0)) + + + +## 6.0.0-alpha.1 (2024-04-08) + +* fix: `fsp.rm` removing files does not take effect (#16032) ([b05c405](https://github.com/vitejs/vite/commit/b05c405)), closes [#16032](https://github.com/vitejs/vite/issues/16032) +* fix: csp nonce injection when no closing tag (#16281) (#16282) ([67a74f8](https://github.com/vitejs/vite/commit/67a74f8)), closes [#16281](https://github.com/vitejs/vite/issues/16281) [#16282](https://github.com/vitejs/vite/issues/16282) +* fix: do not access document in `/@vite/client` when not defined (#16318) ([6c5536b](https://github.com/vitejs/vite/commit/6c5536b)), closes [#16318](https://github.com/vitejs/vite/issues/16318) +* fix: fix sourcemap when using object as `define` value (#15805) ([9699ba3](https://github.com/vitejs/vite/commit/9699ba3)), closes [#15805](https://github.com/vitejs/vite/issues/15805) +* fix: package types ([bdf13bb](https://github.com/vitejs/vite/commit/bdf13bb)) +* fix(deps): update all non-major dependencies (#16376) ([58a2938](https://github.com/vitejs/vite/commit/58a2938)), closes [#16376](https://github.com/vitejs/vite/issues/16376) +* fix(environment): use `environments.client.build.outDir` for preview (#16301) ([8621c3f](https://github.com/vitejs/vite/commit/8621c3f)), closes [#16301](https://github.com/vitejs/vite/issues/16301) +* feat: async createEnvironment ([d15a157](https://github.com/vitejs/vite/commit/d15a157)) +* feat: dedupe/preserveSymlinks ([3ba9214](https://github.com/vitejs/vite/commit/3ba9214)) +* refactor: environment.dev.recoverable ([ea1c7eb](https://github.com/vitejs/vite/commit/ea1c7eb)) +* refactor: isFileServingAllowed load fallback for SSR ([d91714b](https://github.com/vitejs/vite/commit/d91714b)) +* refactor: lib options ([70731ce](https://github.com/vitejs/vite/commit/70731ce)) +* chore: merge ([bcac048](https://github.com/vitejs/vite/commit/bcac048)) +* chore: merge ([833dabf](https://github.com/vitejs/vite/commit/833dabf)) +* chore: remove ssr.target use ([0ea8be9](https://github.com/vitejs/vite/commit/0ea8be9)) +* chore: remove ssrConfig ([27371dc](https://github.com/vitejs/vite/commit/27371dc)) +* chore: update region comment (#16380) ([77562c3](https://github.com/vitejs/vite/commit/77562c3)), closes [#16380](https://github.com/vitejs/vite/issues/16380) +* chore(deps): update all non-major dependencies (#16325) ([c7efec4](https://github.com/vitejs/vite/commit/c7efec4)), closes [#16325](https://github.com/vitejs/vite/issues/16325) +* perf: reduce size of injected __vite__mapDeps code (#16184) ([a9bf430](https://github.com/vitejs/vite/commit/a9bf430)), closes [#16184](https://github.com/vitejs/vite/issues/16184) +* perf: reduce size of injected __vite__mapDeps code (#16184) ([c0ec6be](https://github.com/vitejs/vite/commit/c0ec6be)), closes [#16184](https://github.com/vitejs/vite/issues/16184) +* perf(css): only replace empty chunk if imported (#16349) ([f61d8b1](https://github.com/vitejs/vite/commit/f61d8b1)), closes [#16349](https://github.com/vitejs/vite/issues/16349) + + + +## 6.0.0-alpha.0 (2024-04-05) + +* feat: abstract moduleGraph into ModuleExecutionEnvironment ([5f5e0ec](https://github.com/vitejs/vite/commit/5f5e0ec)) +* feat: add `hot` property to environments ([e966ba0](https://github.com/vitejs/vite/commit/e966ba0)) +* feat: build.ssrEmitAssets -> build.emitAssets ([ef8c9b9](https://github.com/vitejs/vite/commit/ef8c9b9)) +* feat: builder config, runBuildTasks option ([f4789a3](https://github.com/vitejs/vite/commit/f4789a3)) +* feat: configureDevEnvironments + configureBuildEnvironments ([88fea3b](https://github.com/vitejs/vite/commit/88fea3b)) +* feat: environment aware createIdResolver ([f1dcd2c](https://github.com/vitejs/vite/commit/f1dcd2c)) +* feat: environment aware createResolver and resolvePlugin ([dd6332e](https://github.com/vitejs/vite/commit/dd6332e)) +* feat: environment aware depsOptimizer ([a7e52aa](https://github.com/vitejs/vite/commit/a7e52aa)) +* feat: environment id resolver for css plugin ([0bec1b9](https://github.com/vitejs/vite/commit/0bec1b9)) +* feat: environment in hooks, context vs param (#16261) ([fbe6361](https://github.com/vitejs/vite/commit/fbe6361)), closes [#16261](https://github.com/vitejs/vite/issues/16261) +* feat: environment.transformRequest ([fcebb7d](https://github.com/vitejs/vite/commit/fcebb7d)) +* feat: inject environment in build hooks ([cef1091](https://github.com/vitejs/vite/commit/cef1091)) +* feat: separate module graphs per environment ([83068fe](https://github.com/vitejs/vite/commit/83068fe)) +* feat: server.runHmrTasks ([7f94c03](https://github.com/vitejs/vite/commit/7f94c03)) +* feat: ssr.external/noExternal -> resolve.external/noExternal ([2a0b524](https://github.com/vitejs/vite/commit/2a0b524)) +* feat: ssr.target -> environment.webCompatible ([1a7d290](https://github.com/vitejs/vite/commit/1a7d290)) +* feat: support transport options to communicate between the environment and the runner (#16209) ([dbcc375](https://github.com/vitejs/vite/commit/dbcc375)), closes [#16209](https://github.com/vitejs/vite/issues/16209) +* feat: vite runtime renamed to module runner (#16137) ([60f7f2b](https://github.com/vitejs/vite/commit/60f7f2b)), closes [#16137](https://github.com/vitejs/vite/issues/16137) +* feat(hmr): call `hotUpdate` hook with file create/delete (#16249) ([3d37ac1](https://github.com/vitejs/vite/commit/3d37ac1)), closes [#16249](https://github.com/vitejs/vite/issues/16249) +* refactor: allow custom connections in node module runner ([9005841](https://github.com/vitejs/vite/commit/9005841)) +* refactor: base environment.config + environment.options ([c7e4da2](https://github.com/vitejs/vite/commit/c7e4da2)) +* refactor: buildEnvironments + hmrEnvironments ([c1fc111](https://github.com/vitejs/vite/commit/c1fc111)) +* refactor: clientEnvironment instead of browserEnvironment (#16194) ([ccf3de4](https://github.com/vitejs/vite/commit/ccf3de4)), closes [#16194](https://github.com/vitejs/vite/issues/16194) +* refactor: configEnvironment hook + enviroment config resolving ([fee54ea](https://github.com/vitejs/vite/commit/fee54ea)) +* refactor: environment id,type -> name + fixes ([29f1b7b](https://github.com/vitejs/vite/commit/29f1b7b)) +* refactor: environments array to plain object ([a7a06fe](https://github.com/vitejs/vite/commit/a7a06fe)) +* refactor: environments as array instead of map (#16193) ([f1d660c](https://github.com/vitejs/vite/commit/f1d660c)), closes [#16193](https://github.com/vitejs/vite/issues/16193) +* refactor: hooks get an environment object instead of a string ([5e60d8a](https://github.com/vitejs/vite/commit/5e60d8a)) +* refactor: hooks to config for creating environments ([3e6216c](https://github.com/vitejs/vite/commit/3e6216c)) +* refactor: isolate back compat module graph in its own module ([8000e8e](https://github.com/vitejs/vite/commit/8000e8e)) +* refactor: ModuleExecutionEnvironment -> DevEnvironment ([6e71b24](https://github.com/vitejs/vite/commit/6e71b24)) +* refactor: move safeModulesPath set to server ([95ae29b](https://github.com/vitejs/vite/commit/95ae29b)) +* refactor: move transport to properties ([9cfa916](https://github.com/vitejs/vite/commit/9cfa916)) +* refactor: node -> ssr for default environment ([e03bac8](https://github.com/vitejs/vite/commit/e03bac8)) +* refactor: options and environment are required when calling container.hook ([e30b858](https://github.com/vitejs/vite/commit/e30b858)) +* refactor: pass down name to the environment factory ([52edfc9](https://github.com/vitejs/vite/commit/52edfc9)) +* refactor: remove default nodeModuleRunner because it's not used anywhere ([f29e95a](https://github.com/vitejs/vite/commit/f29e95a)) +* refactor: remove environment name from the hmr context ([a183a0f](https://github.com/vitejs/vite/commit/a183a0f)) +* refactor: rename "hmrEnvironments" to "hotUpdateEnvironments" ([a0b7edb](https://github.com/vitejs/vite/commit/a0b7edb)) +* refactor: rename createSsrEnvironment to createNodeEnvironment ([c9abcfc](https://github.com/vitejs/vite/commit/c9abcfc)) +* refactor: rename ssrInvalidates to invalidates ([72fe84e](https://github.com/vitejs/vite/commit/72fe84e)) +* refactor: rework resolveId in ModuleExecutionEnvironment constructor ([03d3889](https://github.com/vitejs/vite/commit/03d3889)) +* refactor: ssrConfig.optimizeDeps.include/exclude ([5bd8e95](https://github.com/vitejs/vite/commit/5bd8e95)) +* refactor: use ssr environment module graph in ssrFixStacktrace ([5477972](https://github.com/vitejs/vite/commit/5477972)) +* fix: add auto complete to server.environments ([a160a1b](https://github.com/vitejs/vite/commit/a160a1b)) +* fix: call updateModules for each environmnet ([281cf97](https://github.com/vitejs/vite/commit/281cf97)) +* fix: fine-grained hmr ([31e1d3a](https://github.com/vitejs/vite/commit/31e1d3a)) +* fix: HotContext only gets ModuleExecutionEnvironment ([30be775](https://github.com/vitejs/vite/commit/30be775)) +* fix: injectEnvironmentInContext ([a1d385c](https://github.com/vitejs/vite/commit/a1d385c)) +* fix: injectEnvironmentToHooks ([681ccd4](https://github.com/vitejs/vite/commit/681ccd4)) +* fix: missing externalConditions back compat ([beb40ef](https://github.com/vitejs/vite/commit/beb40ef)) +* fix: optimizeDeps backward compatibility layer ([3806fe6](https://github.com/vitejs/vite/commit/3806fe6)) +* fix: partial backward compat for config.ssr ([85ada0d](https://github.com/vitejs/vite/commit/85ada0d)) +* fix: resolve.externalConditions ([fb9365c](https://github.com/vitejs/vite/commit/fb9365c)) +* fix: use "register" event for remote environment transport ([c4f4dfb](https://github.com/vitejs/vite/commit/c4f4dfb)) +* fix(css): unknown file error happened with lightningcss (#16306) ([01af308](https://github.com/vitejs/vite/commit/01af308)), closes [#16306](https://github.com/vitejs/vite/issues/16306) +* fix(hmr): multiple updates happened when invalidate is called while multiple tabs open (#16307) ([21cc10b](https://github.com/vitejs/vite/commit/21cc10b)), closes [#16307](https://github.com/vitejs/vite/issues/16307) +* fix(scanner): duplicate modules for same id if glob is used in html-like types (#16305) ([eca68fa](https://github.com/vitejs/vite/commit/eca68fa)), closes [#16305](https://github.com/vitejs/vite/issues/16305) +* test: add test for worker transport ([a5ef42e](https://github.com/vitejs/vite/commit/a5ef42e)) +* test: fix after merge ([d9ed857](https://github.com/vitejs/vite/commit/d9ed857)) +* test(environment): add environment playground (#16299) ([a5c7e4f](https://github.com/vitejs/vite/commit/a5c7e4f)), closes [#16299](https://github.com/vitejs/vite/issues/16299) +* chore: fix lint ([b4e46fe](https://github.com/vitejs/vite/commit/b4e46fe)) +* chore: fix lint ([6040ab3](https://github.com/vitejs/vite/commit/6040ab3)) +* chore: lint ([8785f4f](https://github.com/vitejs/vite/commit/8785f4f)) +* chore: lint ([92eccf9](https://github.com/vitejs/vite/commit/92eccf9)) +* chore: lint ([f927702](https://github.com/vitejs/vite/commit/f927702)) +* chore: rename module and error back to ssrModule and ssrError ([d8ff12a](https://github.com/vitejs/vite/commit/d8ff12a)) +* chore: rename server environment to node environment ([4808b27](https://github.com/vitejs/vite/commit/4808b27)) +* chore: run prettier on environment file ([1fe63b1](https://github.com/vitejs/vite/commit/1fe63b1)) +* chore: update ([9a600fe](https://github.com/vitejs/vite/commit/9a600fe)) +* chore: update environment.server.config ([2ddf28e](https://github.com/vitejs/vite/commit/2ddf28e)) +* wip: environment config overrides ([81abf6e](https://github.com/vitejs/vite/commit/81abf6e)) + + + ## 5.2.9 (2024-04-15) * fix: `fsp.rm` removing files does not take effect (#16032) ([b05c405](https://github.com/vitejs/vite/commit/b05c405)), closes [#16032](https://github.com/vitejs/vite/issues/16032) @@ -9,6 +120,18 @@ +## 5.2.8 (2024-04-03) + +* release: v5.2.8 ([8b8d402](https://github.com/vitejs/vite/commit/8b8d402)) +* fix: csp nonce injection when no closing tag (#16281) (#16282) ([3c85c6b](https://github.com/vitejs/vite/commit/3c85c6b)), closes [#16281](https://github.com/vitejs/vite/issues/16281) [#16282](https://github.com/vitejs/vite/issues/16282) +* fix: do not access document in `/@vite/client` when not defined (#16318) ([646319c](https://github.com/vitejs/vite/commit/646319c)), closes [#16318](https://github.com/vitejs/vite/issues/16318) +* fix: fix sourcemap when using object as `define` value (#15805) ([445c4f2](https://github.com/vitejs/vite/commit/445c4f2)), closes [#15805](https://github.com/vitejs/vite/issues/15805) +* chore(deps): update all non-major dependencies (#16325) ([a78e265](https://github.com/vitejs/vite/commit/a78e265)), closes [#16325](https://github.com/vitejs/vite/issues/16325) +* refactor: use types from sass instead of @types/sass (#16340) ([4581e83](https://github.com/vitejs/vite/commit/4581e83)), closes [#16340](https://github.com/vitejs/vite/issues/16340) + + + + ## 5.2.8 (2024-04-03) * fix: csp nonce injection when no closing tag (#16281) (#16282) ([3c85c6b](https://github.com/vitejs/vite/commit/3c85c6b)), closes [#16281](https://github.com/vitejs/vite/issues/16281) [#16282](https://github.com/vitejs/vite/issues/16282) diff --git a/packages/vite/package.json b/packages/vite/package.json index 74423dc3a5ca0d..3a0648671f0460 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -1,6 +1,6 @@ { "name": "vite", - "version": "5.2.9", + "version": "6.0.0-alpha.2", "type": "module", "license": "MIT", "author": "Evan You", @@ -32,9 +32,9 @@ "./client": { "types": "./client.d.ts" }, - "./runtime": { - "types": "./dist/node/runtime.d.ts", - "import": "./dist/node/runtime.js" + "./module-runner": { + "types": "./dist/node/module-runner.d.ts", + "import": "./dist/node/module-runner.js" }, "./dist/client/*": "./dist/client/*", "./types/*": { @@ -44,8 +44,8 @@ }, "typesVersions": { "*": { - "runtime": [ - "dist/node/runtime.d.ts" + "module-runner": [ + "dist/node/module-runner.d.ts" ] } }, diff --git a/packages/vite/rollup.config.ts b/packages/vite/rollup.config.ts index 2d14eace0eea9c..a83fcc5e62f525 100644 --- a/packages/vite/rollup.config.ts +++ b/packages/vite/rollup.config.ts @@ -177,11 +177,11 @@ function createNodeConfig(isProduction: boolean) { }) } -function createRuntimeConfig(isProduction: boolean) { +function createModuleRunnerConfig(isProduction: boolean) { return defineConfig({ ...sharedNodeOptions, input: { - runtime: path.resolve(__dirname, 'src/runtime/index.ts'), + 'module-runner': path.resolve(__dirname, 'src/module-runner/index.ts'), }, output: { ...sharedNodeOptions.output, @@ -202,7 +202,7 @@ function createRuntimeConfig(isProduction: boolean) { isProduction ? false : './dist/node', ), esbuildMinifyPlugin({ minify: false, minifySyntax: true }), - bundleSizeLimit(45), + bundleSizeLimit(47), ], }) } @@ -240,7 +240,7 @@ export default (commandLineArgs: any): RollupOptions[] => { envConfig, clientConfig, createNodeConfig(isProduction), - createRuntimeConfig(isProduction), + createModuleRunnerConfig(isProduction), createCjsConfig(isProduction), ]) } @@ -332,10 +332,10 @@ const __require = require; name: 'cjs-chunk-patch', renderChunk(code, chunk) { if (!chunk.fileName.includes('chunks/dep-')) return - // don't patch runtime utils chunk because it should stay lightweight and we know it doesn't use require + // don't patch runner utils chunk because it should stay lightweight and we know it doesn't use require if ( chunk.name === 'utils' && - chunk.moduleIds.some((id) => id.endsWith('/ssr/runtime/utils.ts')) + chunk.moduleIds.some((id) => id.endsWith('/ssr/module-runner/utils.ts')) ) return const match = code.match(/^(?:import[\s\S]*?;\s*)+/) diff --git a/packages/vite/rollup.dts.config.ts b/packages/vite/rollup.dts.config.ts index 41b1da1458a31e..1848c2c2b5ba04 100644 --- a/packages/vite/rollup.dts.config.ts +++ b/packages/vite/rollup.dts.config.ts @@ -25,7 +25,7 @@ const external = [ export default defineConfig({ input: { index: './temp/node/index.d.ts', - runtime: './temp/runtime/index.d.ts', + 'module-runner': './temp/module-runner/index.d.ts', }, output: { dir: './dist/node', @@ -48,6 +48,8 @@ const identifierWithTrailingDollarRE = /\b(\w+)\$\d+\b/g const identifierReplacements: Record> = { rollup: { Plugin$1: 'rollup.Plugin', + PluginContext$1: 'rollup.PluginContext', + TransformPluginContext$1: 'rollup.TransformPluginContext', TransformResult$2: 'rollup.TransformResult', }, esbuild: { @@ -91,10 +93,10 @@ function patchTypes(): Plugin { }, renderChunk(code, chunk) { if ( - chunk.fileName.startsWith('runtime') || + chunk.fileName.startsWith('module-runner') || chunk.fileName.startsWith('types.d-') ) { - validateRuntimeChunk.call(this, chunk) + validateRunnerChunk.call(this, chunk) } else { validateChunkImports.call(this, chunk) code = replaceConfusingTypeNames.call(this, code, chunk) @@ -107,9 +109,9 @@ function patchTypes(): Plugin { } /** - * Runtime chunk should only import local dependencies to stay lightweight + * Runner chunk should only import local dependencies to stay lightweight */ -function validateRuntimeChunk(this: PluginContext, chunk: RenderedChunk) { +function validateRunnerChunk(this: PluginContext, chunk: RenderedChunk) { for (const id of chunk.imports) { if ( !id.startsWith('./') && diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index 8fcb145fd0242c..6beae7ff9ad9ed 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -136,7 +136,10 @@ const debounceReload = (time: number) => { const pageReload = debounceReload(50) const hmrClient = new HMRClient( - console, + { + error: (err) => console.error('[vite]', err), + debug: (...msg) => console.debug('[vite]', ...msg), + }, { isReady: () => socket && socket.readyState === 1, send: (message) => socket.send(message), @@ -172,7 +175,7 @@ const hmrClient = new HMRClient( async function handleMessage(payload: HMRPayload) { switch (payload.type) { case 'connected': - console.debug(`[vite] connected.`) + console.debug(`connected.`) hmrClient.messenger.flush() // proxy(nginx, docker) hmr ws maybe caused timeout, // so send ping package let ws keep alive. diff --git a/packages/vite/src/runtime/constants.ts b/packages/vite/src/module-runner/constants.ts similarity index 100% rename from packages/vite/src/runtime/constants.ts rename to packages/vite/src/module-runner/constants.ts diff --git a/packages/vite/src/runtime/esmRunner.ts b/packages/vite/src/module-runner/esmEvaluator.ts similarity index 82% rename from packages/vite/src/runtime/esmRunner.ts rename to packages/vite/src/module-runner/esmEvaluator.ts index 5d4c481c39e85a..3e84f98f7646bd 100644 --- a/packages/vite/src/runtime/esmRunner.ts +++ b/packages/vite/src/module-runner/esmEvaluator.ts @@ -6,11 +6,11 @@ import { ssrImportMetaKey, ssrModuleExportsKey, } from './constants' -import type { ViteModuleRunner, ViteRuntimeModuleContext } from './types' +import type { ModuleEvaluator, ModuleRunnerContext } from './types' -export class ESModulesRunner implements ViteModuleRunner { - async runViteModule( - context: ViteRuntimeModuleContext, +export class ESModulesEvaluator implements ModuleEvaluator { + async runInlinedModule( + context: ModuleRunnerContext, code: string, ): Promise { // use AsyncFunction instead of vm module to support broader array of environments out of the box diff --git a/packages/vite/src/runtime/hmrHandler.ts b/packages/vite/src/module-runner/hmrHandler.ts similarity index 55% rename from packages/vite/src/runtime/hmrHandler.ts rename to packages/vite/src/module-runner/hmrHandler.ts index b0b9fdd5fd6f32..d46b8b9c5581d2 100644 --- a/packages/vite/src/runtime/hmrHandler.ts +++ b/packages/vite/src/module-runner/hmrHandler.ts @@ -1,24 +1,24 @@ import type { HMRPayload } from 'types/hmrPayload' -import { unwrapId } from '../shared/utils' -import type { ViteRuntime } from './runtime' +import { slash, unwrapId } from '../shared/utils' +import type { ModuleRunner } from './runner' // updates to HMR should go one after another. It is possible to trigger another update during the invalidation for example. export function createHMRHandler( - runtime: ViteRuntime, + runner: ModuleRunner, ): (payload: HMRPayload) => Promise { const queue = new Queue() - return (payload) => queue.enqueue(() => handleHMRPayload(runtime, payload)) + return (payload) => queue.enqueue(() => handleHMRPayload(runner, payload)) } export async function handleHMRPayload( - runtime: ViteRuntime, + runner: ModuleRunner, payload: HMRPayload, ): Promise { - const hmrClient = runtime.hmrClient - if (!hmrClient || runtime.isDestroyed()) return + const hmrClient = runner.hmrClient + if (!hmrClient || runner.isDestroyed()) return switch (payload.type) { case 'connected': - hmrClient.logger.debug(`[vite] connected.`) + hmrClient.logger.debug(`connected.`) hmrClient.messenger.flush() break case 'update': @@ -26,15 +26,13 @@ export async function handleHMRPayload( await Promise.all( payload.updates.map(async (update): Promise => { if (update.type === 'js-update') { - // runtime always caches modules by their full path without /@id/ prefix + // runner always caches modules by their full path without /@id/ prefix update.acceptedPath = unwrapId(update.acceptedPath) update.path = unwrapId(update.path) return hmrClient.queueUpdate(update) } - hmrClient.logger.error( - '[vite] css hmr is not supported in runtime mode.', - ) + hmrClient.logger.error('css hmr is not supported in runner mode.') }), ) await hmrClient.notifyListeners('vite:afterUpdate', payload) @@ -46,22 +44,20 @@ export async function handleHMRPayload( case 'full-reload': { const { triggeredBy } = payload const clearEntrypoints = triggeredBy - ? [...runtime.entrypoints].filter((entrypoint) => - runtime.moduleCache.isImported({ - importedId: triggeredBy, - importedBy: entrypoint, - }), + ? getModulesEntrypoints( + runner, + getModulesByFile(runner, slash(triggeredBy)), ) - : [...runtime.entrypoints] + : findAllEntrypoints(runner) - if (!clearEntrypoints.length) break + if (!clearEntrypoints.size) break - hmrClient.logger.debug(`[vite] program reload`) + hmrClient.logger.debug(`program reload`) await hmrClient.notifyListeners('vite:beforeFullReload', payload) - runtime.moduleCache.clear() + runner.moduleCache.clear() for (const id of clearEntrypoints) { - await runtime.executeUrl(id) + await runner.import(id) } break } @@ -73,7 +69,7 @@ export async function handleHMRPayload( await hmrClient.notifyListeners('vite:error', payload) const err = payload.err hmrClient.logger.error( - `[vite] Internal Server Error\n${err.message}\n${err.stack}`, + `Internal Server Error\n${err.message}\n${err.stack}`, ) break } @@ -123,3 +119,46 @@ class Queue { return true } } + +function getModulesByFile(runner: ModuleRunner, file: string) { + const modules: string[] = [] + for (const [id, mod] of runner.moduleCache.entries()) { + if (mod.meta && 'file' in mod.meta && mod.meta.file === file) { + modules.push(id) + } + } + return modules +} + +function getModulesEntrypoints( + runner: ModuleRunner, + modules: string[], + visited = new Set(), + entrypoints = new Set(), +) { + for (const moduleId of modules) { + if (visited.has(moduleId)) continue + visited.add(moduleId) + const module = runner.moduleCache.getByModuleId(moduleId) + if (module.importers && !module.importers.size) { + entrypoints.add(moduleId) + continue + } + for (const importer of module.importers || []) { + getModulesEntrypoints(runner, [importer], visited, entrypoints) + } + } + return entrypoints +} + +function findAllEntrypoints( + runner: ModuleRunner, + entrypoints = new Set(), +): Set { + for (const [id, mod] of runner.moduleCache.entries()) { + if (mod.importers && !mod.importers.size) { + entrypoints.add(id) + } + } + return entrypoints +} diff --git a/packages/vite/src/runtime/hmrLogger.ts b/packages/vite/src/module-runner/hmrLogger.ts similarity index 51% rename from packages/vite/src/runtime/hmrLogger.ts rename to packages/vite/src/module-runner/hmrLogger.ts index 57325298949e09..931a69d125d45b 100644 --- a/packages/vite/src/runtime/hmrLogger.ts +++ b/packages/vite/src/module-runner/hmrLogger.ts @@ -6,3 +6,8 @@ export const silentConsole: HMRLogger = { debug: noop, error: noop, } + +export const hmrLogger: HMRLogger = { + debug: (...msg) => console.log('[vite]', ...msg), + error: (error) => console.log('[vite]', error), +} diff --git a/packages/vite/src/runtime/index.ts b/packages/vite/src/module-runner/index.ts similarity index 52% rename from packages/vite/src/runtime/index.ts rename to packages/vite/src/module-runner/index.ts index ded7222e45d690..efcd72c340a623 100644 --- a/packages/vite/src/runtime/index.ts +++ b/packages/vite/src/module-runner/index.ts @@ -1,21 +1,24 @@ -// this file should re-export only things that don't rely on Node.js or other runtime features +// this file should re-export only things that don't rely on Node.js or other runner features export { ModuleCacheMap } from './moduleCache' -export { ViteRuntime } from './runtime' -export { ESModulesRunner } from './esmRunner' +export { ModuleRunner } from './runner' +export { ESModulesEvaluator } from './esmEvaluator' +export { RemoteRunnerTransport } from './runnerTransport' +export type { RunnerTransport } from './runnerTransport' export type { HMRLogger, HMRConnection } from '../shared/hmr' export type { - ViteModuleRunner, - ViteRuntimeModuleContext, + ModuleEvaluator, + ModuleRunnerContext, ModuleCache, FetchResult, FetchFunction, ResolvedResult, SSRImportMetadata, - HMRRuntimeConnection, - ViteRuntimeImportMeta, - ViteRuntimeOptions, + ModuleRunnerHMRConnection, + ModuleRunnerImportMeta, + ModuleRunnerOptions, + ModuleRunnerHmr, } from './types' export { ssrDynamicImportKey, diff --git a/packages/vite/src/runtime/moduleCache.ts b/packages/vite/src/module-runner/moduleCache.ts similarity index 83% rename from packages/vite/src/runtime/moduleCache.ts rename to packages/vite/src/module-runner/moduleCache.ts index 3681e3db1ed78d..ed94cc7bcbcbe2 100644 --- a/packages/vite/src/runtime/moduleCache.ts +++ b/packages/vite/src/module-runner/moduleCache.ts @@ -4,7 +4,7 @@ import { decodeBase64 } from './utils' import { DecodedMap } from './sourcemap/decoder' import type { ModuleCache } from './types' -const VITE_RUNTIME_SOURCEMAPPING_REGEXP = new RegExp( +const MODULE_RUNNER_SOURCEMAPPING_REGEXP = new RegExp( `//# ${SOURCEMAPPING_URL}=data:application/json;base64,(.+)`, ) @@ -46,6 +46,7 @@ export class ModuleCacheMap extends Map { Object.assign(mod, { imports: new Set(), importers: new Set(), + timestamp: 0, }) } return mod @@ -63,8 +64,12 @@ export class ModuleCacheMap extends Map { return this.deleteByModuleId(this.normalize(fsPath)) } - invalidate(id: string): void { + invalidateUrl(id: string): void { const module = this.get(id) + this.invalidateModule(module) + } + + invalidateModule(module: ModuleCache): void { module.evaluated = false module.meta = undefined module.map = undefined @@ -77,43 +82,6 @@ export class ModuleCacheMap extends Map { module.imports?.clear() } - isImported( - { - importedId, - importedBy, - }: { - importedId: string - importedBy: string - }, - seen = new Set(), - ): boolean { - importedId = this.normalize(importedId) - importedBy = this.normalize(importedBy) - - if (importedBy === importedId) return true - - if (seen.has(importedId)) return false - seen.add(importedId) - - const fileModule = this.getByModuleId(importedId) - const importers = fileModule?.importers - - if (!importers) return false - - if (importers.has(importedBy)) return true - - for (const importer of importers) { - if ( - this.isImported({ - importedBy: importedBy, - importedId: importer, - }) - ) - return true - } - return false - } - /** * Invalidate modules that dependent on the given modules, up to the main entry */ @@ -127,7 +95,7 @@ export class ModuleCacheMap extends Map { invalidated.add(id) const mod = super.get(id) if (mod?.importers) this.invalidateDepTree(mod.importers, invalidated) - super.delete(id) + this.invalidateUrl(id) } return invalidated } @@ -157,7 +125,7 @@ export class ModuleCacheMap extends Map { if (mod.map) return mod.map if (!mod.meta || !('code' in mod.meta)) return null const mapString = mod.meta.code.match( - VITE_RUNTIME_SOURCEMAPPING_REGEXP, + MODULE_RUNNER_SOURCEMAPPING_REGEXP, )?.[1] if (!mapString) return null const baseFile = mod.meta.file || moduleId.split('?')[0] diff --git a/packages/vite/src/runtime/runtime.ts b/packages/vite/src/module-runner/runner.ts similarity index 65% rename from packages/vite/src/runtime/runtime.ts rename to packages/vite/src/module-runner/runner.ts index 5feff38352617f..4dbec334d06341 100644 --- a/packages/vite/src/runtime/runtime.ts +++ b/packages/vite/src/module-runner/runner.ts @@ -14,16 +14,16 @@ import { } from '../shared/ssrTransform' import { ModuleCacheMap } from './moduleCache' import type { - FetchResult, ModuleCache, + ModuleEvaluator, + ModuleRunnerContext, + ModuleRunnerImportMeta, + ModuleRunnerOptions, ResolvedResult, SSRImportMetadata, - ViteModuleRunner, - ViteRuntimeImportMeta, - ViteRuntimeModuleContext, - ViteRuntimeOptions, } from './types' import { + parseUrl, posixDirname, posixPathToFileHref, posixResolve, @@ -36,92 +36,82 @@ import { ssrImportMetaKey, ssrModuleExportsKey, } from './constants' -import { silentConsole } from './hmrLogger' +import { hmrLogger, silentConsole } from './hmrLogger' import { createHMRHandler } from './hmrHandler' import { enableSourceMapSupport } from './sourcemap/index' +import type { RunnerTransport } from './runnerTransport' -interface ViteRuntimeDebugger { +interface ModuleRunnerDebugger { (formatter: unknown, ...args: unknown[]): void } -export class ViteRuntime { +export class ModuleRunner { /** * Holds the cache of modules * Keys of the map are ids */ public moduleCache: ModuleCacheMap public hmrClient?: HMRClient - public entrypoints = new Set() - private idToUrlMap = new Map() - private fileToIdMap = new Map() - private envProxy = new Proxy({} as any, { + private readonly urlToIdMap = new Map() + private readonly fileToIdMap = new Map() + private readonly envProxy = new Proxy({} as any, { get(_, p) { throw new Error( - `[vite-runtime] Dynamic access of "import.meta.env" is not supported. Please, use "import.meta.env.${String(p)}" instead.`, + `[module runner] Dynamic access of "import.meta.env" is not supported. Please, use "import.meta.env.${String(p)}" instead.`, ) }, }) + private readonly transport: RunnerTransport + private readonly resetSourceMapSupport?: () => void - private _destroyed = false - private _resetSourceMapSupport?: () => void + private destroyed = false constructor( - public options: ViteRuntimeOptions, - public runner: ViteModuleRunner, - private debug?: ViteRuntimeDebugger, + public options: ModuleRunnerOptions, + public evaluator: ModuleEvaluator, + private debug?: ModuleRunnerDebugger, ) { this.moduleCache = options.moduleCache ?? new ModuleCacheMap(options.root) + this.transport = options.transport if (typeof options.hmr === 'object') { this.hmrClient = new HMRClient( options.hmr.logger === false ? silentConsole - : options.hmr.logger || console, + : options.hmr.logger || hmrLogger, options.hmr.connection, - ({ acceptedPath, ssrInvalidates }) => { - this.moduleCache.invalidate(acceptedPath) - if (ssrInvalidates) { - this.invalidateFiles(ssrInvalidates) - } - return this.executeUrl(acceptedPath) + ({ acceptedPath, explicitImportRequired, timestamp }) => { + const [acceptedPathWithoutQuery, query] = acceptedPath.split(`?`) + const url = + acceptedPathWithoutQuery + + `?${explicitImportRequired ? 'import&' : ''}t=${timestamp}${ + query ? `&${query}` : '' + }` + return this.import(url) }, ) options.hmr.connection.onUpdate(createHMRHandler(this)) } if (options.sourcemapInterceptor !== false) { - this._resetSourceMapSupport = enableSourceMapSupport(this) + this.resetSourceMapSupport = enableSourceMapSupport(this) } } /** * URL to execute. Accepts file path, server path or id relative to the root. */ - public async executeUrl(url: string): Promise { + public async import(url: string): Promise { url = this.normalizeEntryUrl(url) const fetchedModule = await this.cachedModule(url) return await this.cachedRequest(url, fetchedModule) } - /** - * Entrypoint URL to execute. Accepts file path, server path or id relative to the root. - * In the case of a full reload triggered by HMR, this is the module that will be reloaded. - * If this method is called multiple times, all entrypoints will be reloaded one at a time. - */ - public async executeEntrypoint(url: string): Promise { - url = this.normalizeEntryUrl(url) - const fetchedModule = await this.cachedModule(url) - return await this.cachedRequest(url, fetchedModule, [], { - entrypoint: true, - }) - } - /** * Clear all caches including HMR listeners. */ public clearCache(): void { this.moduleCache.clear() - this.idToUrlMap.clear() - this.entrypoints.clear() + this.urlToIdMap.clear() this.hmrClient?.clear() } @@ -130,26 +120,17 @@ export class ViteRuntime { * This method doesn't stop the HMR connection. */ public async destroy(): Promise { - this._resetSourceMapSupport?.() + this.resetSourceMapSupport?.() this.clearCache() this.hmrClient = undefined - this._destroyed = true + this.destroyed = true } /** * Returns `true` if the runtime has been destroyed by calling `destroy()` method. */ public isDestroyed(): boolean { - return this._destroyed - } - - private invalidateFiles(files: string[]) { - files.forEach((file) => { - const ids = this.fileToIdMap.get(file) - if (ids) { - ids.forEach((id) => this.moduleCache.invalidate(id)) - } - }) + return this.destroyed } // we don't use moduleCache.normalize because this URL doesn't have to follow the same rules @@ -198,17 +179,12 @@ export class ViteRuntime { private async cachedRequest( id: string, - fetchedModule: ResolvedResult, + mod: ModuleCache, callstack: string[] = [], metadata?: SSRImportMetadata, ): Promise { - const moduleId = fetchedModule.id - - if (metadata?.entrypoint) { - this.entrypoints.add(moduleId) - } - - const mod = this.moduleCache.getByModuleId(moduleId) + const meta = mod.meta! + const moduleId = meta.id const { imports, importers } = mod as Required @@ -221,8 +197,7 @@ export class ViteRuntime { callstack.includes(moduleId) || Array.from(imports.values()).some((i) => importers.has(i)) ) { - if (mod.exports) - return this.processImport(mod.exports, fetchedModule, metadata) + if (mod.exports) return this.processImport(mod.exports, meta, metadata) } let debugTimer: any @@ -235,7 +210,7 @@ export class ViteRuntime { .join('\n')}` this.debug!( - `[vite-runtime] module ${moduleId} takes over 2s to load.\n${getStack()}`, + `[module runner] module ${moduleId} takes over 2s to load.\n${getStack()}`, ) }, 2000) } @@ -243,12 +218,12 @@ export class ViteRuntime { try { // cached module if (mod.promise) - return this.processImport(await mod.promise, fetchedModule, metadata) + return this.processImport(await mod.promise, meta, metadata) - const promise = this.directRequest(id, fetchedModule, callstack) + const promise = this.directRequest(id, mod, callstack) mod.promise = promise mod.evaluated = false - return this.processImport(await promise, fetchedModule, metadata) + return this.processImport(await promise, meta, metadata) } finally { mod.evaluated = true if (debugTimer) clearTimeout(debugTimer) @@ -256,35 +231,46 @@ export class ViteRuntime { } private async cachedModule( - id: string, + url: string, importer?: string, - ): Promise { - if (this._destroyed) { - throw new Error(`[vite] Vite runtime has been destroyed.`) + ): Promise { + if (this.destroyed) { + throw new Error(`Vite module runner has been destroyed.`) } - const normalized = this.idToUrlMap.get(id) + const normalized = this.urlToIdMap.get(url) if (normalized) { const mod = this.moduleCache.getByModuleId(normalized) if (mod.meta) { - return mod.meta as ResolvedResult + return mod } } - this.debug?.('[vite-runtime] fetching', id) + + this.debug?.('[module runner] fetching', url) // fast return for established externalized patterns - const fetchedModule = id.startsWith('data:') - ? ({ externalize: id, type: 'builtin' } satisfies FetchResult) - : await this.options.fetchModule(id, importer) + const fetchedModule = ( + url.startsWith('data:') + ? { externalize: url, type: 'builtin' } + : await this.transport.fetchModule(url, importer) + ) as ResolvedResult + // base moduleId on "file" and not on id // if `import(variable)` is called it's possible that it doesn't have an extension for example - // if we used id for that, it's possible to have a duplicated module - const idQuery = id.split('?')[1] - const query = idQuery ? `?${idQuery}` : '' + // if we used id for that, then a module will be duplicated + const { query, timestamp } = parseUrl(url) const file = 'file' in fetchedModule ? fetchedModule.file : undefined - const fullFile = file ? `${file}${query}` : id - const moduleId = this.moduleCache.normalize(fullFile) + const fileId = file ? `${file}${query}` : url + const moduleId = this.moduleCache.normalize(fileId) const mod = this.moduleCache.getByModuleId(moduleId) - ;(fetchedModule as ResolvedResult).id = moduleId + + // if URL has a ?t= query, it might've been invalidated due to HMR + // checking if we should also invalidate the module + if (mod.timestamp != null && timestamp > 0 && mod.timestamp < timestamp) { + this.moduleCache.invalidateModule(mod) + } + + fetchedModule.id = moduleId mod.meta = fetchedModule + mod.timestamp = timestamp if (file) { const fileModules = this.fileToIdMap.get(file) || [] @@ -292,27 +278,28 @@ export class ViteRuntime { this.fileToIdMap.set(file, fileModules) } - this.idToUrlMap.set(id, moduleId) - this.idToUrlMap.set(unwrapId(id), moduleId) - return fetchedModule as ResolvedResult + this.urlToIdMap.set(url, moduleId) + this.urlToIdMap.set(unwrapId(url), moduleId) + return mod } // override is allowed, consider this a public API protected async directRequest( id: string, - fetchResult: ResolvedResult, + mod: ModuleCache, _callstack: string[], ): Promise { + const fetchResult = mod.meta! const moduleId = fetchResult.id const callstack = [..._callstack, moduleId] - const mod = this.moduleCache.getByModuleId(moduleId) - const request = async (dep: string, metadata?: SSRImportMetadata) => { - const fetchedModule = await this.cachedModule(dep, moduleId) - const depMod = this.moduleCache.getByModuleId(fetchedModule.id) + const importer = ('file' in fetchResult && fetchResult.file) || moduleId + const fetchedModule = await this.cachedModule(dep, importer) + const resolvedId = fetchedModule.meta!.id + const depMod = this.moduleCache.getByModuleId(resolvedId) depMod.importers!.add(moduleId) - mod.imports!.add(fetchedModule.id) + mod.imports!.add(resolvedId) return this.cachedRequest(dep, fetchedModule, callstack, metadata) } @@ -328,8 +315,8 @@ export class ViteRuntime { if ('externalize' in fetchResult) { const { externalize } = fetchResult - this.debug?.('[vite-runtime] externalizing', externalize) - const exports = await this.runner.runExternalModule(externalize) + this.debug?.('[module runner] externalizing', externalize) + const exports = await this.evaluator.runExternalModule(externalize) mod.exports = exports return exports } @@ -339,7 +326,7 @@ export class ViteRuntime { if (code == null) { const importer = callstack[callstack.length - 2] throw new Error( - `[vite-runtime] Failed to load "${id}"${ + `[module runner] Failed to load "${id}"${ importer ? ` imported from ${importer}` : '' }`, ) @@ -350,19 +337,19 @@ export class ViteRuntime { const href = posixPathToFileHref(modulePath) const filename = modulePath const dirname = posixDirname(modulePath) - const meta: ViteRuntimeImportMeta = { + const meta: ModuleRunnerImportMeta = { filename: isWindows ? toWindowsPath(filename) : filename, dirname: isWindows ? toWindowsPath(dirname) : dirname, url: href, env: this.envProxy, resolve(id, parent) { throw new Error( - '[vite-runtime] "import.meta.resolve" is not supported.', + '[module runner] "import.meta.resolve" is not supported.', ) }, // should be replaced during transformation glob() { - throw new Error('[vite-runtime] "import.meta.glob" is not supported.') + throw new Error('[module runner] "import.meta.glob" is not supported.') }, } const exports = Object.create(null) @@ -380,9 +367,9 @@ export class ViteRuntime { enumerable: true, get: () => { if (!this.hmrClient) { - throw new Error(`[vite-runtime] HMR client was destroyed.`) + throw new Error(`[module runner] HMR client was destroyed.`) } - this.debug?.('[vite-runtime] creating hmr context for', moduleId) + this.debug?.('[module runner] creating hmr context for', moduleId) hotContext ||= new HMRContext(this.hmrClient, moduleId) return hotContext }, @@ -392,7 +379,7 @@ export class ViteRuntime { }) } - const context: ViteRuntimeModuleContext = { + const context: ModuleRunnerContext = { [ssrImportKey]: request, [ssrDynamicImportKey]: dynamicRequest, [ssrModuleExportsKey]: exports, @@ -400,9 +387,9 @@ export class ViteRuntime { [ssrImportMetaKey]: meta, } - this.debug?.('[vite-runtime] executing', href) + this.debug?.('[module runner] executing', href) - await this.runner.runViteModule(context, code, id) + await this.evaluator.runInlinedModule(context, code, id) return exports } diff --git a/packages/vite/src/module-runner/runnerTransport.ts b/packages/vite/src/module-runner/runnerTransport.ts new file mode 100644 index 00000000000000..f946d956342c25 --- /dev/null +++ b/packages/vite/src/module-runner/runnerTransport.ts @@ -0,0 +1,83 @@ +import type { FetchFunction, FetchResult } from './types' + +export interface RunnerTransport { + fetchModule: FetchFunction +} + +export class RemoteRunnerTransport implements RunnerTransport { + private rpcPromises = new Map< + string, + { + resolve: (data: any) => void + reject: (data: any) => void + timeoutId?: NodeJS.Timeout + } + >() + + constructor( + private readonly options: { + send: (data: any) => void + onMessage: (handler: (data: any) => void) => void + timeout?: number + }, + ) { + this.options.onMessage(async (data) => { + if (typeof data !== 'object' || !data || !data.__v) return + + const promise = this.rpcPromises.get(data.i) + if (!promise) return + + if (promise.timeoutId) clearTimeout(promise.timeoutId) + + this.rpcPromises.delete(data.i) + + if (data.e) { + promise.reject(data.e) + } else { + promise.resolve(data.r) + } + }) + } + + private resolve(method: string, ...args: any[]) { + const promiseId = nanoid() + this.options.send({ + __v: true, + m: method, + a: args, + i: promiseId, + }) + + return new Promise((resolve, reject) => { + const timeout = this.options.timeout ?? 60000 + let timeoutId + if (timeout > 0) { + timeoutId = setTimeout(() => { + this.rpcPromises.delete(promiseId) + reject( + new Error( + `${method}(${args.map((arg) => JSON.stringify(arg)).join(', ')}) timed out after ${timeout}ms`, + ), + ) + }, timeout) + timeoutId?.unref?.() + } + this.rpcPromises.set(promiseId, { resolve, reject, timeoutId }) + }) + } + + fetchModule(id: string, importer?: string): Promise { + return this.resolve('fetchModule', id, importer) + } +} + +// port from nanoid +// https://github.com/ai/nanoid +const urlAlphabet = + 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict' +function nanoid(size = 21) { + let id = '' + let i = size + while (i--) id += urlAlphabet[(Math.random() * 64) | 0] + return id +} diff --git a/packages/vite/src/runtime/sourcemap/decoder.ts b/packages/vite/src/module-runner/sourcemap/decoder.ts similarity index 100% rename from packages/vite/src/runtime/sourcemap/decoder.ts rename to packages/vite/src/module-runner/sourcemap/decoder.ts diff --git a/packages/vite/src/runtime/sourcemap/index.ts b/packages/vite/src/module-runner/sourcemap/index.ts similarity index 76% rename from packages/vite/src/runtime/sourcemap/index.ts rename to packages/vite/src/module-runner/sourcemap/index.ts index 648c5e52717fc2..0efc5ca2db97b5 100644 --- a/packages/vite/src/runtime/sourcemap/index.ts +++ b/packages/vite/src/module-runner/sourcemap/index.ts @@ -1,8 +1,8 @@ -import type { ViteRuntime } from '../runtime' +import type { ModuleRunner } from '../runner' import { interceptStackTrace } from './interceptor' -export function enableSourceMapSupport(runtime: ViteRuntime): () => void { - if (runtime.options.sourcemapInterceptor === 'node') { +export function enableSourceMapSupport(runner: ModuleRunner): () => void { + if (runner.options.sourcemapInterceptor === 'node') { if (typeof process === 'undefined') { throw new TypeError( `Cannot use "sourcemapInterceptor: 'node'" because global "process" variable is not available.`, @@ -20,9 +20,9 @@ export function enableSourceMapSupport(runtime: ViteRuntime): () => void { /* eslint-enable n/no-unsupported-features/node-builtins */ } return interceptStackTrace( - runtime, - typeof runtime.options.sourcemapInterceptor === 'object' - ? runtime.options.sourcemapInterceptor + runner, + typeof runner.options.sourcemapInterceptor === 'object' + ? runner.options.sourcemapInterceptor : undefined, ) } diff --git a/packages/vite/src/runtime/sourcemap/interceptor.ts b/packages/vite/src/module-runner/sourcemap/interceptor.ts similarity index 97% rename from packages/vite/src/runtime/sourcemap/interceptor.ts rename to packages/vite/src/module-runner/sourcemap/interceptor.ts index 58d324e79b943c..424337839dc610 100644 --- a/packages/vite/src/runtime/sourcemap/interceptor.ts +++ b/packages/vite/src/module-runner/sourcemap/interceptor.ts @@ -1,5 +1,5 @@ import type { OriginalMapping } from '@jridgewell/trace-mapping' -import type { ViteRuntime } from '../runtime' +import type { ModuleRunner } from '../runner' import { posixDirname, posixResolve } from '../utils' import type { ModuleCacheMap } from '../moduleCache' import { slash } from '../../shared/utils' @@ -45,8 +45,8 @@ const retrieveSourceMapFromHandlers = createExecHandlers( let overridden = false const originalPrepare = Error.prepareStackTrace -function resetInterceptor(runtime: ViteRuntime, options: InterceptorOptions) { - moduleGraphs.delete(runtime.moduleCache) +function resetInterceptor(runner: ModuleRunner, options: InterceptorOptions) { + moduleGraphs.delete(runner.moduleCache) if (options.retrieveFile) retrieveFileHandlers.delete(options.retrieveFile) if (options.retrieveSourceMap) retrieveSourceMapHandlers.delete(options.retrieveSourceMap) @@ -57,18 +57,18 @@ function resetInterceptor(runtime: ViteRuntime, options: InterceptorOptions) { } export function interceptStackTrace( - runtime: ViteRuntime, + runner: ModuleRunner, options: InterceptorOptions = {}, ): () => void { if (!overridden) { Error.prepareStackTrace = prepareStackTrace overridden = true } - moduleGraphs.add(runtime.moduleCache) + moduleGraphs.add(runner.moduleCache) if (options.retrieveFile) retrieveFileHandlers.add(options.retrieveFile) if (options.retrieveSourceMap) retrieveSourceMapHandlers.add(options.retrieveSourceMap) - return () => resetInterceptor(runtime, options) + return () => resetInterceptor(runner, options) } interface CallSite extends NodeJS.CallSite { @@ -101,7 +101,7 @@ function supportRelativeURL(file: string, url: string) { return protocol + posixResolve(startPath, url) } -function getRuntimeSourceMap(position: OriginalMapping): CachedMapEntry | null { +function getRunnerSourceMap(position: OriginalMapping): CachedMapEntry | null { for (const moduleCache of moduleGraphs) { const sourceMap = moduleCache.getSourceMap(position.source!) if (sourceMap) { @@ -172,7 +172,7 @@ function retrieveSourceMap(source: string) { function mapSourcePosition(position: OriginalMapping) { if (!position.source) return position - let sourceMap = getRuntimeSourceMap(position) + let sourceMap = getRunnerSourceMap(position) if (!sourceMap) sourceMap = sourceMapCache[position.source] if (!sourceMap) { // Call the (overrideable) retrieveSourceMap function to get the source map. diff --git a/packages/vite/src/runtime/tsconfig.json b/packages/vite/src/module-runner/tsconfig.json similarity index 100% rename from packages/vite/src/runtime/tsconfig.json rename to packages/vite/src/module-runner/tsconfig.json diff --git a/packages/vite/src/runtime/types.ts b/packages/vite/src/module-runner/types.ts similarity index 74% rename from packages/vite/src/runtime/types.ts rename to packages/vite/src/module-runner/types.ts index 730ed59630e26d..165a593fa31654 100644 --- a/packages/vite/src/runtime/types.ts +++ b/packages/vite/src/module-runner/types.ts @@ -15,13 +15,11 @@ import type { } from './constants' import type { DecodedMap } from './sourcemap/decoder' import type { InterceptorOptions } from './sourcemap/interceptor' +import type { RunnerTransport } from './runnerTransport' -export type { DefineImportMetadata } -export interface SSRImportMetadata extends SSRImportBaseMetadata { - entrypoint?: boolean -} +export type { DefineImportMetadata, SSRImportBaseMetadata as SSRImportMetadata } -export interface HMRRuntimeConnection extends HMRConnection { +export interface ModuleRunnerHMRConnection extends HMRConnection { /** * Configure how HMR is handled when this connection triggers an update. * This method expects that connection will start listening for HMR updates and call this callback when it's received. @@ -29,14 +27,14 @@ export interface HMRRuntimeConnection extends HMRConnection { onUpdate(callback: (payload: HMRPayload) => void): void } -export interface ViteRuntimeImportMeta extends ImportMeta { +export interface ModuleRunnerImportMeta extends ImportMeta { url: string env: ImportMetaEnv hot?: ViteHotContext [key: string]: any } -export interface ViteRuntimeModuleContext { +export interface ModuleRunnerContext { [ssrModuleExportsKey]: Record [ssrImportKey]: (id: string, metadata?: DefineImportMetadata) => Promise [ssrDynamicImportKey]: ( @@ -44,18 +42,18 @@ export interface ViteRuntimeModuleContext { options?: ImportCallOptions, ) => Promise [ssrExportAllKey]: (obj: any) => void - [ssrImportMetaKey]: ViteRuntimeImportMeta + [ssrImportMetaKey]: ModuleRunnerImportMeta } -export interface ViteModuleRunner { +export interface ModuleEvaluator { /** * Run code that was transformed by Vite. * @param context Function context * @param code Transformed code * @param id ID that was used to fetch the module */ - runViteModule( - context: ViteRuntimeModuleContext, + runInlinedModule( + context: ModuleRunnerContext, code: string, id: string, ): Promise @@ -71,7 +69,8 @@ export interface ModuleCache { exports?: any evaluated?: boolean map?: DecodedMap - meta?: FetchResult + meta?: ResolvedResult + timestamp?: number /** * Module ids that imports this module */ @@ -85,7 +84,7 @@ export interface ExternalFetchResult { /** * The path to the externalized module starting with file://, * by default this will be imported via a dynamic "import" - * instead of being transformed by vite and loaded with vite runtime + * instead of being transformed by vite and loaded with vite runner */ externalize: string /** @@ -97,7 +96,7 @@ export interface ExternalFetchResult { export interface ViteFetchResult { /** - * Code that will be evaluated by vite runtime + * Code that will be evaluated by vite runner * by default this will be wrapped in an async function */ code: string @@ -120,21 +119,26 @@ export type FetchFunction = ( importer?: string, ) => Promise -export interface ViteRuntimeOptions { +export interface ModuleRunnerHmr { /** - * Root of the project + * Configure how HMR communicates between the client and the server. */ - root: string + connection: ModuleRunnerHMRConnection + /** + * Configure HMR logger. + */ + logger?: false | HMRLogger +} + +export interface ModuleRunnerOptions { /** - * A method to get the information about the module. - * For SSR, Vite exposes `server.ssrFetchModule` function that you can use here. - * For other runtime use cases, Vite also exposes `fetchModule` from its main entry point. + * Root of the project */ - fetchModule: FetchFunction + root: string /** - * Custom environment variables available on `import.meta.env`. This doesn't modify the actual `process.env`. + * A set of methods to communicate with the server. */ - environmentVariables?: Record + transport: RunnerTransport /** * Configure how source maps are resolved. Prefers `node` if `process.setSourceMapsEnabled` is available. * Otherwise it will use `prepareStackTrace` by default which overrides `Error.prepareStackTrace` method. @@ -148,20 +152,9 @@ export interface ViteRuntimeOptions { /** * Disable HMR or configure HMR options. */ - hmr?: - | false - | { - /** - * Configure how HMR communicates between the client and the server. - */ - connection: HMRRuntimeConnection - /** - * Configure HMR logger. - */ - logger?: false | HMRLogger - } + hmr?: false | ModuleRunnerHmr /** - * Custom module cache. If not provided, creates a separate module cache for each ViteRuntime instance. + * Custom module cache. If not provided, creates a separate module cache for each ModuleRunner instance. */ moduleCache?: ModuleCacheMap } diff --git a/packages/vite/src/runtime/utils.ts b/packages/vite/src/module-runner/utils.ts similarity index 75% rename from packages/vite/src/runtime/utils.ts rename to packages/vite/src/module-runner/utils.ts index 12e06a3ebb1882..60070fc22531a2 100644 --- a/packages/vite/src/runtime/utils.ts +++ b/packages/vite/src/module-runner/utils.ts @@ -16,6 +16,33 @@ const carriageReturnRegEx = /\r/g const tabRegEx = /\t/g const questionRegex = /\?/g const hashRegex = /#/g +const timestampRegex = /[?&]t=(\d{13})(&?)/ + +interface ParsedPath { + query: string + timestamp: number +} + +export function parseUrl(url: string): ParsedPath { + const idQuery = url.split('?')[1] + let timestamp = 0 + // for performance, we avoid using URL constructor and parsing twice + // it's not really needed, but it's a micro-optimization that we can do for free + const query = idQuery + ? ('?' + idQuery).replace( + timestampRegex, + (substring, tsString, nextItem) => { + timestamp = Number(tsString) + // remove the "?t=" query since it's only used for invalidation + return substring[0] === '?' && nextItem === '&' ? '?' : '' + }, + ) + : '' + return { + query, + timestamp, + } +} function encodePathChars(filepath: string) { if (filepath.indexOf('%') !== -1) diff --git a/packages/vite/src/node/__tests__/dev.spec.ts b/packages/vite/src/node/__tests__/dev.spec.ts index 1ade6c0adde9ea..346bebd2aac42e 100644 --- a/packages/vite/src/node/__tests__/dev.spec.ts +++ b/packages/vite/src/node/__tests__/dev.spec.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from 'vitest' import { resolveConfig } from '..' -describe('resolveBuildOptions in dev', () => { +describe('resolveBuildEnvironmentOptions in dev', () => { test('build.rollupOptions should not have input in lib', async () => { const config = await resolveConfig( { diff --git a/packages/vite/src/node/ssr/__tests__/ssrExternal.spec.ts b/packages/vite/src/node/__tests__/external.spec.ts similarity index 53% rename from packages/vite/src/node/ssr/__tests__/ssrExternal.spec.ts rename to packages/vite/src/node/__tests__/external.spec.ts index 68e753af703ce2..59d7ef6d159a4a 100644 --- a/packages/vite/src/node/ssr/__tests__/ssrExternal.spec.ts +++ b/packages/vite/src/node/__tests__/external.spec.ts @@ -1,29 +1,30 @@ import { fileURLToPath } from 'node:url' import { describe, expect, test } from 'vitest' -import type { SSROptions } from '..' -import { resolveConfig } from '../../config' -import { createIsConfiguredAsSsrExternal } from '../ssrExternal' +import { resolveConfig } from '../config' +import { createIsConfiguredAsExternal } from '../external' +import { Environment } from '../environment' -describe('createIsConfiguredAsSsrExternal', () => { +describe('createIsConfiguredAsExternal', () => { test('default', async () => { const isExternal = await createIsExternal() expect(isExternal('@vitejs/cjs-ssr-dep')).toBe(false) }) test('force external', async () => { - const isExternal = await createIsExternal({ external: true }) + const isExternal = await createIsExternal(true) expect(isExternal('@vitejs/cjs-ssr-dep')).toBe(true) }) }) -async function createIsExternal(ssrConfig?: SSROptions) { +async function createIsExternal(external?: true) { const resolvedConfig = await resolveConfig( { configFile: false, root: fileURLToPath(new URL('./', import.meta.url)), - ssr: ssrConfig, + resolve: { external }, }, 'serve', ) - return createIsConfiguredAsSsrExternal(resolvedConfig) + const environment = new Environment('ssr', resolvedConfig) + return createIsConfiguredAsExternal(environment) } diff --git a/packages/vite/src/node/ssr/__tests__/fixtures/cjs-ssr-dep/index.js b/packages/vite/src/node/__tests__/fixtures/cjs-ssr-dep/index.js similarity index 100% rename from packages/vite/src/node/ssr/__tests__/fixtures/cjs-ssr-dep/index.js rename to packages/vite/src/node/__tests__/fixtures/cjs-ssr-dep/index.js diff --git a/packages/vite/src/node/ssr/__tests__/fixtures/cjs-ssr-dep/package.json b/packages/vite/src/node/__tests__/fixtures/cjs-ssr-dep/package.json similarity index 100% rename from packages/vite/src/node/ssr/__tests__/fixtures/cjs-ssr-dep/package.json rename to packages/vite/src/node/__tests__/fixtures/cjs-ssr-dep/package.json diff --git a/packages/vite/src/node/ssr/__tests__/package.json b/packages/vite/src/node/__tests__/package.json similarity index 100% rename from packages/vite/src/node/ssr/__tests__/package.json rename to packages/vite/src/node/__tests__/package.json diff --git a/packages/vite/src/node/__tests__/plugins/css.spec.ts b/packages/vite/src/node/__tests__/plugins/css.spec.ts index cfd7dc6e6d4e47..8a6c6bd5d42737 100644 --- a/packages/vite/src/node/__tests__/plugins/css.spec.ts +++ b/packages/vite/src/node/__tests__/plugins/css.spec.ts @@ -2,6 +2,8 @@ import fs from 'node:fs' import path from 'node:path' import { describe, expect, test, vi } from 'vitest' import { resolveConfig } from '../../config' +import { Environment } from '../../environment' +import type { PluginEnvironment } from '../../plugin' import type { InlineConfig } from '../../config' import { convertTargets, @@ -213,6 +215,8 @@ async function createCssPluginTransform( inlineConfig: InlineConfig = {}, ) { const config = await resolveConfig(inlineConfig, 'serve') + const environment = new Environment('client', config) as PluginEnvironment + const { transform, buildStart } = cssPlugin(config) // @ts-expect-error buildStart is function @@ -233,6 +237,7 @@ async function createCssPluginTransform( addWatchFile() { return }, + environment, }, code, id, diff --git a/packages/vite/src/node/__tests__/plugins/define.spec.ts b/packages/vite/src/node/__tests__/plugins/define.spec.ts index 8876a9d7ecc3e2..3748746af23430 100644 --- a/packages/vite/src/node/__tests__/plugins/define.spec.ts +++ b/packages/vite/src/node/__tests__/plugins/define.spec.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from 'vitest' import { definePlugin } from '../../plugins/define' import { resolveConfig } from '../../config' +import { Environment } from '../../environment' async function createDefinePluginTransform( define: Record = {}, @@ -12,9 +13,15 @@ async function createDefinePluginTransform( build ? 'build' : 'serve', ) const instance = definePlugin(config) + const environment = new Environment(ssr ? 'ssr' : 'client', config) + return async (code: string) => { // @ts-expect-error transform should exist - const result = await instance.transform.call({}, code, 'foo.ts', { ssr }) + const result = await instance.transform.call( + { environment }, + code, + 'foo.ts', + ) return result?.code || result } } diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 4bc57ce58f76aa..4e4f2944bd60a5 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -14,6 +14,7 @@ import type { RollupLog, RollupOptions, RollupOutput, + PluginContext as RollupPluginContext, RollupWatcher, WatcherOptions, } from 'rollup' @@ -27,8 +28,14 @@ import { ESBUILD_MODULES_TARGET, VERSION, } from './constants' -import type { InlineConfig, ResolvedConfig } from './config' -import { resolveConfig } from './config' +import type { + EnvironmentOptions, + InlineConfig, + ResolvedConfig, + ResolvedEnvironmentOptions, +} from './config' +import type { PluginOption } from './plugin' +import { getDefaultResolvedEnvironmentOptions, resolveConfig } from './config' import { buildReporterPlugin } from './plugins/reporter' import { buildEsbuildPlugin } from './plugins/esbuild' import { type TerserOptions, terserPlugin } from './plugins/terser' @@ -48,7 +55,7 @@ import type { Logger } from './logger' import { dataURIPlugin } from './plugins/dataUri' import { buildImportAnalysisPlugin } from './plugins/importAnalysisBuild' import { ssrManifestPlugin } from './ssr/ssrManifestPlugin' -import { loadFallbackPlugin } from './plugins/loadFallback' +import { buildLoadFallbackPlugin } from './plugins/loadFallback' import { findNearestPackageData } from './packages' import type { PackageCache } from './packages' import { @@ -60,8 +67,9 @@ import { completeSystemWrapPlugin } from './plugins/completeSystemWrap' import { mergeConfig } from './publicUtils' import { webWorkerPostPlugin } from './plugins/worker' import { getHookHandler } from './plugins' +import { Environment } from './environment' -export interface BuildOptions { +export interface BuildEnvironmentOptions { /** * Compatibility transform target. The transform is performed with esbuild * and the lowest supported target is es2015/es6. Note this only handles @@ -206,13 +214,6 @@ export interface BuildOptions { * @default false */ manifest?: boolean | string - /** - * Build in library mode. The value should be the global name of the lib in - * UMD mode. This will produce esm + cjs + umd bundle formats with default - * configurations that are suitable for distributing libraries. - * @default false - */ - lib?: LibraryOptions | false /** * Produce SSR oriented build. Note this requires specifying SSR entry via * `rollupOptions.input`. @@ -228,8 +229,16 @@ export interface BuildOptions { /** * Emit assets during SSR. * @default false + * @deprecated use emitAssets */ ssrEmitAssets?: boolean + /** + * Emit assets during build. Frameworks can set environments.ssr.build.emitAssets + * By default, it is true for the client and false for other environments. + * TODO: Should this be true for all environments by default? Or should this be + * controlled by the builder so so we can avoid emitting duplicated assets. + */ + emitAssets?: boolean /** * Set to false to disable reporting compressed chunk sizes. * Can slightly improve build speed. @@ -247,6 +256,23 @@ export interface BuildOptions { * @default null */ watch?: WatcherOptions | null + /** + * create the Build Environment instance + */ + createEnvironment?: ( + name: string, + config: ResolvedConfig, + ) => Promise | BuildEnvironment +} + +export interface BuildOptions extends BuildEnvironmentOptions { + /** + * Build in library mode. The value should be the global name of the lib in + * UMD mode. This will produce esm + cjs + umd bundle formats with default + * configurations that are suitable for distributing libraries. + * @default false + */ + lib?: LibraryOptions | false } export interface LibraryOptions { @@ -301,78 +327,99 @@ export type ResolveModulePreloadDependenciesFn = ( }, ) => string[] +export interface ResolvedBuildEnvironmentOptions + extends Required> { + modulePreload: false | ResolvedModulePreloadOptions +} + export interface ResolvedBuildOptions extends Required> { modulePreload: false | ResolvedModulePreloadOptions } export function resolveBuildOptions( - raw: BuildOptions | undefined, + raw: BuildOptions, logger: Logger, root: string, ): ResolvedBuildOptions { + const libMode = raw.lib ?? false + const buildOptions = resolveBuildEnvironmentOptions( + raw, + logger, + root, + undefined, + libMode, + ) + return { ...buildOptions, lib: libMode } +} + +export function resolveBuildEnvironmentOptions( + raw: BuildEnvironmentOptions, + logger: Logger, + root: string, + environmentName: string | undefined, + libMode: false | LibraryOptions = false, +): ResolvedBuildEnvironmentOptions { const deprecatedPolyfillModulePreload = raw?.polyfillModulePreload - if (raw) { - const { polyfillModulePreload, ...rest } = raw - raw = rest - if (deprecatedPolyfillModulePreload !== undefined) { - logger.warn( - 'polyfillModulePreload is deprecated. Use modulePreload.polyfill instead.', - ) - } - if ( - deprecatedPolyfillModulePreload === false && - raw.modulePreload === undefined - ) { - raw.modulePreload = { polyfill: false } - } + const { polyfillModulePreload, ...rest } = raw + raw = rest + if (deprecatedPolyfillModulePreload !== undefined) { + logger.warn( + 'polyfillModulePreload is deprecated. Use modulePreload.polyfill instead.', + ) + } + if ( + deprecatedPolyfillModulePreload === false && + raw.modulePreload === undefined + ) { + raw.modulePreload = { polyfill: false } } - const modulePreload = raw?.modulePreload + const modulePreload = raw.modulePreload const defaultModulePreload = { polyfill: true, } - const defaultBuildOptions: BuildOptions = { + const defaultBuildEnvironmentOptions: BuildEnvironmentOptions = { outDir: 'dist', assetsDir: 'assets', assetsInlineLimit: DEFAULT_ASSETS_INLINE_LIMIT, - cssCodeSplit: !raw?.lib, + cssCodeSplit: !libMode, sourcemap: false, rollupOptions: {}, - minify: raw?.ssr ? false : 'esbuild', + minify: raw.ssr ? false : 'esbuild', terserOptions: {}, write: true, emptyOutDir: null, copyPublicDir: true, manifest: false, - lib: false, ssr: false, ssrManifest: false, ssrEmitAssets: false, + emitAssets: environmentName === 'client', reportCompressedSize: true, chunkSizeWarningLimit: 500, watch: null, } - const userBuildOptions = raw - ? mergeConfig(defaultBuildOptions, raw) - : defaultBuildOptions + const userBuildEnvironmentOptions = raw + ? mergeConfig(defaultBuildEnvironmentOptions, raw) + : defaultBuildEnvironmentOptions // @ts-expect-error Fallback options instead of merging - const resolved: ResolvedBuildOptions = { + const resolved: ResolvedBuildEnvironmentOptions = { target: 'modules', cssTarget: false, - ...userBuildOptions, + ...userBuildEnvironmentOptions, commonjsOptions: { include: [/node_modules/], extensions: ['.js', '.cjs'], - ...userBuildOptions.commonjsOptions, + ...userBuildEnvironmentOptions.commonjsOptions, }, dynamicImportVarsOptions: { warnOnError: true, exclude: [/node_modules/], - ...userBuildOptions.dynamicImportVarsOptions, + ...userBuildEnvironmentOptions.dynamicImportVarsOptions, }, // Resolve to false | object modulePreload: @@ -455,7 +502,7 @@ export async function resolveBuildPlugins(config: ResolvedConfig): Promise<{ buildReporterPlugin(config), ] : []), - loadFallbackPlugin(), + buildLoadFallbackPlugin(), ], } } @@ -467,15 +514,41 @@ export async function resolveBuildPlugins(config: ResolvedConfig): Promise<{ export async function build( inlineConfig: InlineConfig = {}, ): Promise { - const config = await resolveConfig( - inlineConfig, - 'build', - 'production', - 'production', + const builder = await createViteBuilder( + {}, + { ...inlineConfig, plugins: () => inlineConfig.plugins ?? [] }, ) + + if (builder.config.build.lib) { + // TODO: temporal workaround. Should we support `libraries: Record` + // to build multiple libraries and be able to target different environments (for example for a Svelte components + // library generating both client and SSR builds)? + return buildEnvironment( + builder.config, + builder.environments.client, + builder.config.build.lib, + ) + } else { + const ssr = !!builder.config.build.ssr + const environment = builder.environments[ssr ? 'ssr' : 'client'] + return builder.build(environment) + } +} + +function resolveConfigToBuild(inlineConfig: InlineConfig = {}) { + return resolveConfig(inlineConfig, 'build', 'production', 'production') +} + +/** + * Build an App environment, or a App library (if librayOptions is provided) + **/ +export async function buildEnvironment( + config: ResolvedConfig, + environment: BuildEnvironment, + libOptions: LibraryOptions | false = false, +): Promise { const options = config.build - const ssr = !!options.ssr - const libOptions = options.lib + const ssr = environment.name !== 'client' config.logger.info( colors.cyan( @@ -524,9 +597,11 @@ export async function build( const outDir = resolve(options.outDir) - // inject ssr arg to plugin load/transform hooks + // inject environment and ssr arg to plugin load/transform hooks const plugins = ( - ssr ? config.plugins.map((p) => injectSsrFlagToHooks(p)) : config.plugins + environment || ssr + ? config.plugins.map((p) => injectEnvironmentToHooks(p, environment)) + : config.plugins ) as Plugin[] const rollupOptions: RollupOptions = { @@ -591,12 +666,9 @@ export async function build( ) } - const ssrNodeBuild = ssr && config.ssr.target === 'node' - const ssrWorkerBuild = ssr && config.ssr.target === 'webworker' - const format = output.format || 'es' const jsExt = - ssrNodeBuild || libOptions + !environment.options.webCompatible || libOptions ? resolveOutputJsExtension( format, findNearestPackageData(config.root, config.packageCache)?.data @@ -637,7 +709,11 @@ export async function build( inlineDynamicImports: output.format === 'umd' || output.format === 'iife' || - (ssrWorkerBuild && + // TODO: We need an abstraction for non-client environments? + // We should remove the explicit 'client' hcek here. + // Or maybe `inlineDynamicImports` should be an environment option? + (environment.name !== 'client' && + environment.options.webCompatible && (typeof input === 'string' || Object.keys(input).length === 1)), ...output, } @@ -992,22 +1068,33 @@ function isExternal(id: string, test: string | RegExp) { } } -function injectSsrFlagToHooks(plugin: Plugin): Plugin { +export function injectEnvironmentToHooks( + plugin: Plugin, + environment?: BuildEnvironment, +): Plugin { const { resolveId, load, transform } = plugin return { ...plugin, - resolveId: wrapSsrResolveId(resolveId), - load: wrapSsrLoad(load), - transform: wrapSsrTransform(transform), + resolveId: wrapEnvironmentResolveId(resolveId, environment), + load: wrapEnvironmentLoad(load, environment), + transform: wrapEnvironmentTransform(transform, environment), } } -function wrapSsrResolveId(hook?: Plugin['resolveId']): Plugin['resolveId'] { +function wrapEnvironmentResolveId( + hook?: Plugin['resolveId'], + environment?: BuildEnvironment, +): Plugin['resolveId'] { if (!hook) return const fn = getHookHandler(hook) const handler: Plugin['resolveId'] = function (id, importer, options) { - return fn.call(this, id, importer, injectSsrFlag(options)) + return fn.call( + injectEnvironmentInContext(this, environment), + id, + importer, + injectSsrFlag(options, environment), + ) } if ('handler' in hook) { @@ -1020,13 +1107,20 @@ function wrapSsrResolveId(hook?: Plugin['resolveId']): Plugin['resolveId'] { } } -function wrapSsrLoad(hook?: Plugin['load']): Plugin['load'] { +function wrapEnvironmentLoad( + hook?: Plugin['load'], + environment?: BuildEnvironment, +): Plugin['load'] { if (!hook) return const fn = getHookHandler(hook) const handler: Plugin['load'] = function (id, ...args) { - // @ts-expect-error: Receiving options param to be future-proof if Rollup adds it - return fn.call(this, id, injectSsrFlag(args[0])) + return fn.call( + injectEnvironmentInContext(this, environment), + id, + // @ts-expect-error: Receiving options param to be future-proof if Rollup adds it + injectSsrFlag(args[0], environment), + ) } if ('handler' in hook) { @@ -1039,13 +1133,21 @@ function wrapSsrLoad(hook?: Plugin['load']): Plugin['load'] { } } -function wrapSsrTransform(hook?: Plugin['transform']): Plugin['transform'] { +function wrapEnvironmentTransform( + hook?: Plugin['transform'], + environment?: BuildEnvironment, +): Plugin['transform'] { if (!hook) return const fn = getHookHandler(hook) const handler: Plugin['transform'] = function (code, importer, ...args) { - // @ts-expect-error: Receiving options param to be future-proof if Rollup adds it - return fn.call(this, code, importer, injectSsrFlag(args[0])) + return fn.call( + injectEnvironmentInContext(this, environment), + code, + importer, + // @ts-expect-error: Receiving options param to be future-proof if Rollup adds it + injectSsrFlag(args[0], environment), + ) } if ('handler' in hook) { @@ -1058,10 +1160,28 @@ function wrapSsrTransform(hook?: Plugin['transform']): Plugin['transform'] { } } +function injectEnvironmentInContext( + context: RollupPluginContext, + environment?: BuildEnvironment, +) { + return new Proxy(context, { + get(target, prop, receiver) { + if (prop === 'environment') { + return environment + } + return Reflect.get(target, prop, receiver) + }, + }) +} + function injectSsrFlag>( options?: T, -): T & { ssr: boolean } { - return { ...(options ?? {}), ssr: true } as T & { ssr: boolean } + environment?: BuildEnvironment, +): T & { ssr?: boolean } { + const ssr = environment ? environment.name !== 'client' : true + return { ...(options ?? {}), ssr } as T & { + ssr?: boolean + } } /* @@ -1247,3 +1367,137 @@ function areSeparateFolders(a: string, b: string) { !nb.startsWith(withTrailingSlash(na)) ) } + +export class BuildEnvironment extends Environment { + mode = 'build' as const + + constructor( + name: string, + config: ResolvedConfig, + setup?: { + options?: EnvironmentOptions + }, + ) { + // TODO: move this to the base Environment class? + let options = + config.environments[name] ?? getDefaultResolvedEnvironmentOptions(config) + if (setup?.options) { + options = mergeConfig( + options, + setup?.options, + ) as ResolvedEnvironmentOptions + } + super(name, config, options) + } +} + +export interface ViteBuilder { + environments: Record + config: ResolvedConfig + buildEnvironments(): Promise + build( + environment: BuildEnvironment, + ): Promise +} + +export interface BuilderOptions { + buildEnvironments?: ( + builder: ViteBuilder, + build: (environment: BuildEnvironment) => Promise, + ) => Promise +} + +async function defaultBuildEnvironments( + builder: ViteBuilder, + build: (environment: BuildEnvironment) => Promise, +): Promise { + for (const environment of Object.values(builder.environments)) { + await build(environment) + } +} + +export function resolveBuilderOptions( + options: BuilderOptions = {}, +): ResolvedBuilderOptions { + return { + buildEnvironments: options.buildEnvironments ?? defaultBuildEnvironments, + } +} + +export type ResolvedBuilderOptions = Required + +export interface BuilderInlineConfig extends Omit { + plugins?: () => PluginOption[] +} + +export async function createViteBuilder( + builderOptions: BuilderOptions = {}, + defaultBuilderInlineConfig: BuilderInlineConfig = {}, +): Promise { + // Plugins passed to the Builder inline config needs to be created + // from a factory to ensure each build has their own instances + const resolveConfig = ( + environmentOptions?: EnvironmentOptions, + ): Promise => { + const { plugins } = defaultBuilderInlineConfig + let defaultInlineConfig = plugins + ? { + ...defaultBuilderInlineConfig, + plugins: plugins(), + } + : (defaultBuilderInlineConfig as InlineConfig) + + if (environmentOptions) { + defaultInlineConfig = mergeConfig(defaultInlineConfig, environmentOptions) + } + + // We resolve the whole config including plugins here but later on we + // need to refactor resolveConfig to only resolve the environments config + return resolveConfigToBuild(defaultInlineConfig) + } + + const defaultConfig = await resolveConfig() + + const environments: Record = {} + + const builder: ViteBuilder = { + environments, + config: defaultConfig, + async buildEnvironments() { + if (defaultConfig.build.watch) { + throw new Error( + 'Watch mode is not yet supported in viteBuilder.buildEnvironments()', + ) + } + return defaultConfig.builder.buildEnvironments( + builder, + async (environment) => { + await this.build(environment) + }, + ) + }, + async build(environment: BuildEnvironment) { + return buildEnvironment(environment.config, environment) + }, + } + + for (const name of Object.keys(defaultConfig.environments)) { + const environmentOptions = defaultConfig.environments[name] + const createEnvironment = + environmentOptions.build?.createEnvironment ?? + ((name: string, config: ResolvedConfig) => + new BuildEnvironment(name, config)) + + // We need to resolve the config again so we can properly merge options + // and get a new set of plugins for each build environment. The ecosystem + // expects plugins to be run for the same environment once they are created + // and to process a single bundle at a time (contrary to dev mode where + // plugins are built to handle multiple environments concurrently). + const environmentConfig = await resolveConfig(environmentOptions) + + const environment = await createEnvironment(name, environmentConfig) + environments[name] = environment + } + + return builder +} diff --git a/packages/vite/src/node/cli.ts b/packages/vite/src/node/cli.ts index f0fa2092110175..32a735a47c58e6 100644 --- a/packages/vite/src/node/cli.ts +++ b/packages/vite/src/node/cli.ts @@ -4,12 +4,11 @@ import { performance } from 'node:perf_hooks' import { cac } from 'cac' import colors from 'picocolors' import { VERSION } from './constants' -import type { BuildOptions } from './build' +import type { BuildEnvironmentOptions } from './build' import type { ServerOptions } from './server' import type { CLIShortcut } from './shortcuts' import type { LogLevel } from './logger' import { createLogger } from './logger' -import { resolveConfig } from './config' const cli = cac('vite') @@ -31,6 +30,11 @@ interface GlobalCLIOptions { force?: boolean } +interface BuilderCLIOptions { + environment?: string + all?: boolean +} + let profileSession = global.__vite_profile_session let profileCount = 0 @@ -70,7 +74,7 @@ const filterDuplicateOptions = (options: T) => { /** * removing global flags before passing as command specific sub-configs */ -function cleanOptions( +function cleanGlobalCLIOptions( options: Options, ): Omit { const ret = { ...options } @@ -102,6 +106,19 @@ function cleanOptions( return ret } +/** + * removing builder flags before passing as command specific sub-configs + */ +function cleanBuilderCLIOptions( + options: Options, +): Omit { + const ret = { ...options } + delete ret.environment + delete ret.all + + return ret +} + /** * host may be a number (like 0), should convert to string */ @@ -161,7 +178,7 @@ cli logLevel: options.logLevel, clearScreen: options.clearScreen, optimizeDeps: { force: options.force }, - server: cleanOptions(options), + server: cleanGlobalCLIOptions(options), }) if (!server.httpServer) { @@ -263,13 +280,21 @@ cli `[boolean] force empty outDir when it's outside of root`, ) .option('-w, --watch', `[boolean] rebuilds when modules have changed on disk`) - .action(async (root: string, options: BuildOptions & GlobalCLIOptions) => { - filterDuplicateOptions(options) - const { build } = await import('./build') - const buildOptions: BuildOptions = cleanOptions(options) + .option('--environment [name]', `[string] build a single environment`) + .option('--all', `[boolean] build all environments`) + .action( + async ( + root: string, + options: BuildEnvironmentOptions & BuilderCLIOptions & GlobalCLIOptions, + ) => { + filterDuplicateOptions(options) + const { build, createViteBuilder } = await import('./build') - try { - await build({ + const buildOptions: BuildEnvironmentOptions = cleanGlobalCLIOptions( + cleanBuilderCLIOptions(options), + ) + + const config = { root, base: options.base, mode: options.mode, @@ -277,17 +302,37 @@ cli logLevel: options.logLevel, clearScreen: options.clearScreen, build: buildOptions, - }) - } catch (e) { - createLogger(options.logLevel).error( - colors.red(`error during build:\n${e.stack}`), - { error: e }, - ) - process.exit(1) - } finally { - stopProfiler((message) => createLogger(options.logLevel).info(message)) - } - }) + } + + try { + if (options.all || options.environment) { + const builder = await createViteBuilder({}, config) + if (options.environment) { + const environment = builder.environments[options.environment] + if (!environment) { + throw new Error( + `The environment ${options.environment} isn't configured.`, + ) + } + await builder.build(environment) + } else { + // --all: build all environments + await builder.buildEnvironments() + } + } else { + await build(config) + } + } catch (e) { + createLogger(options.logLevel).error( + colors.red(`error during build:\n${e.stack}`), + { error: e }, + ) + process.exit(1) + } finally { + stopProfiler((message) => createLogger(options.logLevel).info(message)) + } + }, + ) // optimize cli @@ -298,6 +343,8 @@ cli ) .action( async (root: string, options: { force?: boolean } & GlobalCLIOptions) => { + /* TODO: do we need this command? + filterDuplicateOptions(options) const { optimizeDeps } = await import('./optimizer') try { @@ -311,7 +358,8 @@ cli }, 'serve', ) - await optimizeDeps(config, options.force, true) + const environment = new Environment('client', config) + await optimizeDeps(environment, options.force, true) } catch (e) { createLogger(options.logLevel).error( colors.red(`error when optimizing deps:\n${e.stack}`), @@ -319,6 +367,7 @@ cli ) process.exit(1) } + */ }, ) diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 2b0f6d28360987..af2292c58014d5 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -9,7 +9,7 @@ import colors from 'picocolors' import type { Alias, AliasOptions } from 'dep-types/alias' import aliasPlugin from '@rollup/plugin-alias' import { build } from 'esbuild' -import type { RollupOptions } from 'rollup' +import type { PartialResolvedId, RollupOptions } from 'rollup' import { withTrailingSlash } from '../shared/utils' import { CLIENT_ENTRY, @@ -20,15 +20,31 @@ import { ENV_ENTRY, FS_PREFIX, } from './constants' -import type { HookHandler, Plugin, PluginWithRequiredHook } from './plugin' import type { + HookHandler, + Plugin, + PluginEnvironment, + PluginOption, + PluginWithRequiredHook, +} from './plugin' +import type { + BuildEnvironmentOptions, BuildOptions, + BuilderOptions, RenderBuiltAssetUrl, + ResolvedBuildEnvironmentOptions, ResolvedBuildOptions, + ResolvedBuilderOptions, +} from './build' +import { + resolveBuildEnvironmentOptions, + resolveBuildOptions, + resolveBuilderOptions, } from './build' -import { resolveBuildOptions } from './build' import type { ResolvedServerOptions, ServerOptions } from './server' import { resolveServerOptions } from './server' +import { Environment } from './environment' +import type { DevEnvironment } from './server/environment' import type { PreviewOptions, ResolvedPreviewOptions } from './preview' import { resolvePreviewOptions } from './preview' import { @@ -43,6 +59,7 @@ import { isBuiltin, isExternalUrl, isFilePathESM, + isInNodeModules, isNodeBuiltin, isObject, isParentDirectory, @@ -65,8 +82,8 @@ import type { LogLevel, Logger } from './logger' import { createLogger } from './logger' import type { DepOptimizationConfig, DepOptimizationOptions } from './optimizer' import type { JsonOptions } from './plugins/json' -import type { PluginContainer } from './server/pluginContainer' -import { createPluginContainer } from './server/pluginContainer' +import type { BoundedPluginContainer } from './server/pluginContainer' +import { createBoundedPluginContainer } from './server/pluginContainer' import type { PackageCache } from './packages' import { findNearestPackageData } from './packages' import { loadEnv, resolveEnvPrefix } from './env' @@ -120,15 +137,142 @@ export function defineConfig(config: UserConfigExport): UserConfigExport { return config } -export type PluginOption = - | Plugin - | false - | null - | undefined - | PluginOption[] - | Promise +export interface DevEnvironmentOptions { + /** + * Files tßo be pre-transformed. Supports glob patterns. + */ + warmup?: string[] + /** + * Pre-transform known direct imports + * defaults to true for the client environment, false for the rest + */ + preTransformRequests?: boolean + /** + * Enables sourcemaps during dev + * @default { js: true } + * @experimental + */ + sourcemap?: boolean | { js?: boolean; css?: boolean } + /** + * Whether or not to ignore-list source files in the dev server sourcemap, used to populate + * the [`x_google_ignoreList` source map extension](https://developer.chrome.com/blog/devtools-better-angular-debugging/#the-x_google_ignorelist-source-map-extension). + * + * By default, it excludes all paths containing `node_modules`. You can pass `false` to + * disable this behavior, or, for full control, a function that takes the source path and + * sourcemap path and returns whether to ignore the source path. + */ + sourcemapIgnoreList?: + | false + | ((sourcePath: string, sourcemapPath: string) => boolean) + + /** + * Optimize deps config + */ + optimizeDeps?: DepOptimizationConfig + + /** + * create the Dev Environment instance + */ + createEnvironment?: ( + name: string, + config: ResolvedConfig, + ) => Promise | DevEnvironment + + /** + * For environments that support a full-reload, like the client, we can short-circuit when + * restarting the server throwing early to stop processing current files. We avoided this for + * SSR requests. Maybe this is no longer needed. + * @experimental + */ + recoverable?: boolean + + /** + * For environments associated with a module runner. + * By default it is true for the client environment and false for non-client environments. + * This option can also be used instead of the removed config.experimental.skipSsrTransform. + */ + moduleRunnerTransform?: boolean + + /** + * Defaults to true for the client environment and false for others, following node permissive + * security model. + * TODO: We need to move at least server.fs.strict to dev options because we want to restrict + * fs access from the client, but keep it open for SSR running on node. For now, we moved + * the check to use environment.nodeCompatible + * Should we only have a boolean toggle per environment and a keep allow/deny configuration + * in server.fs, or move the whole configuration to the environment? + */ + // fs: { strict?: boolean, allow, deny } +} + +export type ResolvedDevEnvironmentOptions = Required< + Omit +> & { + // TODO: Should we set the default at config time? For now, it is defined on server init + createEnvironment: + | (( + name: string, + config: ResolvedConfig, + ) => Promise | DevEnvironment) + | undefined +} + +type EnvironmentResolveOptions = ResolveOptions & { + alias?: AliasOptions +} + +export interface SharedEnvironmentOptions { + /** + * Configure resolver + */ + resolve?: EnvironmentResolveOptions + /** + * Runtime Compatibility + * Temporal options, we should remove these in favor of fine-grained control + */ + nodeCompatible?: boolean + webCompatible?: boolean // was ssr.target === 'webworker' + /** + * Should Vite inject timestamp if module is invalidated + * Disabling this will break built-in HMR support + * @experimental + * @default true + */ + injectInvalidationTimestamp?: boolean +} + +export interface EnvironmentOptions extends SharedEnvironmentOptions { + /** + * Dev specific options + */ + dev?: DevEnvironmentOptions + /** + * Build specific options + */ + build?: BuildEnvironmentOptions +} + +export type ResolvedEnvironmentResolveOptions = + Required + +export type ResolvedEnvironmentOptions = { + resolve: ResolvedEnvironmentResolveOptions + nodeCompatible: boolean + webCompatible: boolean + injectInvalidationTimestamp: boolean + dev: ResolvedDevEnvironmentOptions + build: ResolvedBuildEnvironmentOptions +} + +export type DefaultEnvironmentOptions = Omit< + EnvironmentOptions, + 'build' | 'nodeCompatible' | 'webCompatible' +> & { + // Includes lib mode support + build?: BuildOptions +} -export interface UserConfig { +export interface UserConfig extends DefaultEnvironmentOptions { /** * Project root directory. Can be an absolute path, or a path relative from * the location of the config file itself. @@ -173,10 +317,6 @@ export interface UserConfig { * Array of vite plugins to use. */ plugins?: PluginOption[] - /** - * Configure resolver - */ - resolve?: ResolveOptions & { alias?: AliasOptions } /** * HTML related options */ @@ -199,25 +339,17 @@ export interface UserConfig { */ assetsInclude?: string | RegExp | (string | RegExp)[] /** - * Server specific options, e.g. host, port, https... + * Builder specific options */ - server?: ServerOptions + builder?: BuilderOptions /** - * Build specific options + * Server specific options, e.g. host, port, https... */ - build?: BuildOptions + server?: ServerOptions /** * Preview specific options, e.g. host, port, https... */ preview?: PreviewOptions - /** - * Dep optimization options - */ - optimizeDeps?: DepOptimizationOptions - /** - * SSR specific options - */ - ssr?: SSROptions /** * Experimental features * @@ -280,6 +412,20 @@ export interface UserConfig { 'plugins' | 'input' | 'onwarn' | 'preserveEntrySignatures' > } + /** + * Dep optimization options + */ + optimizeDeps?: DepOptimizationOptions + /** + * SSR specific options + * We could make SSROptions be a EnvironmentOptions if we can abstract + * external/noExternal for environments in general. + */ + ssr?: SSROptions + /** + * Environment overrides + */ + environments?: Record /** * Whether your application is a Single Page Application (SPA), * a Multi-Page Application (MPA), or Custom Application (SSR @@ -357,7 +503,14 @@ export interface InlineConfig extends UserConfig { export type ResolvedConfig = Readonly< Omit< UserConfig, - 'plugins' | 'css' | 'assetsInclude' | 'optimizeDeps' | 'worker' | 'build' + | 'plugins' + | 'css' + | 'assetsInclude' + | 'optimizeDeps' + | 'worker' + | 'build' + | 'dev' + | 'environments' > & { configFile: string | undefined configFileDependencies: string[] @@ -386,6 +539,8 @@ export type ResolvedConfig = Readonly< css: ResolvedCSSOptions esbuild: ESBuildOptions | false server: ResolvedServerOptions + dev: ResolvedDevEnvironmentOptions + builder: ResolvedBuilderOptions build: ResolvedBuildOptions preview: ResolvedPreviewOptions ssr: ResolvedSSROptions @@ -398,9 +553,92 @@ export type ResolvedConfig = Readonly< worker: ResolvedWorkerOptions appType: AppType experimental: ExperimentalOptions + environments: Record } & PluginHookUtils > +export function resolveDevEnvironmentOptions( + dev: DevEnvironmentOptions | undefined, + preserverSymlinks: boolean, + environmentName: string | undefined, + // Backward compatibility + skipSsrTransform?: boolean, +): ResolvedDevEnvironmentOptions { + return { + sourcemap: dev?.sourcemap ?? { js: true }, + sourcemapIgnoreList: + dev?.sourcemapIgnoreList === false + ? () => false + : dev?.sourcemapIgnoreList || isInNodeModules, + preTransformRequests: + dev?.preTransformRequests ?? environmentName === 'client', + warmup: dev?.warmup ?? [], + optimizeDeps: resolveOptimizeDepsConfig( + dev?.optimizeDeps, + preserverSymlinks, + ), + createEnvironment: dev?.createEnvironment, + recoverable: dev?.recoverable ?? environmentName === 'client', + moduleRunnerTransform: + dev?.moduleRunnerTransform ?? + (skipSsrTransform !== undefined && environmentName === 'ssr' + ? skipSsrTransform + : environmentName !== 'client'), + } +} + +function resolveEnvironmentOptions( + options: EnvironmentOptions, + resolvedRoot: string, + logger: Logger, + environmentName: string, + // Backward compatibility + skipSsrTransform?: boolean, +): ResolvedEnvironmentOptions { + const resolve = resolveEnvironmentResolveOptions(options.resolve, logger) + return { + resolve, + nodeCompatible: options.nodeCompatible ?? environmentName !== 'client', + webCompatible: options.webCompatible ?? environmentName === 'client', + injectInvalidationTimestamp: options.injectInvalidationTimestamp ?? true, + dev: resolveDevEnvironmentOptions( + options.dev, + resolve.preserveSymlinks, + environmentName, + skipSsrTransform, + ), + build: resolveBuildEnvironmentOptions( + options.build ?? {}, + logger, + resolvedRoot, + environmentName, + ), + } +} + +export function getDefaultEnvironmentOptions( + config: UserConfig, +): EnvironmentOptions { + return { + resolve: config.resolve, + dev: config.dev, + build: config.build, + } +} + +export function getDefaultResolvedEnvironmentOptions( + config: ResolvedConfig, +): ResolvedEnvironmentOptions { + return { + resolve: config.resolve, + nodeCompatible: true, + webCompatible: false, + injectInvalidationTimestamp: true, + dev: config.dev, + build: config.build, + } +} + export interface PluginHookUtils { getSortedPlugins: ( hookName: K, @@ -445,6 +683,75 @@ function checkBadCharactersInPath(path: string, logger: Logger): void { } } +const clientAlias = [ + { + find: /^\/?@vite\/env/, + replacement: path.posix.join(FS_PREFIX, normalizePath(ENV_ENTRY)), + }, + { + find: /^\/?@vite\/client/, + replacement: path.posix.join(FS_PREFIX, normalizePath(CLIENT_ENTRY)), + }, +] + +function resolveEnvironmentResolveOptions( + resolve: EnvironmentResolveOptions | undefined, + logger: Logger, +): ResolvedConfig['resolve'] { + // resolve alias with internal client alias + const resolvedAlias = normalizeAlias( + mergeAlias(clientAlias, resolve?.alias || []), + ) + + const resolvedResolve: ResolvedConfig['resolve'] = { + mainFields: resolve?.mainFields ?? DEFAULT_MAIN_FIELDS, + conditions: resolve?.conditions ?? [], + externalConditions: resolve?.externalConditions ?? [], + external: resolve?.external ?? [], + noExternal: resolve?.noExternal ?? [], + extensions: resolve?.extensions ?? DEFAULT_EXTENSIONS, + dedupe: resolve?.dedupe ?? [], + preserveSymlinks: resolve?.preserveSymlinks ?? false, + alias: resolvedAlias, + } + + if ( + // @ts-expect-error removed field + resolve?.browserField === false && + resolvedResolve.mainFields.includes('browser') + ) { + logger.warn( + colors.yellow( + `\`resolve.browserField\` is set to false, but the option is removed in favour of ` + + `the 'browser' string in \`resolve.mainFields\`. You may want to update \`resolve.mainFields\` ` + + `to remove the 'browser' string and preserve the previous browser behaviour.`, + ), + ) + } + return resolvedResolve +} + +// TODO: Introduce ResolvedDepOptimizationConfig +function resolveOptimizeDepsConfig( + optimizeDeps: DepOptimizationConfig | undefined, + preserveSymlinks: boolean, +): DepOptimizationConfig { + optimizeDeps ??= {} + return { + include: optimizeDeps.include ?? [], + exclude: optimizeDeps.exclude ?? [], + needsInterop: optimizeDeps.needsInterop ?? [], + extensions: optimizeDeps.extensions ?? [], + noDiscovery: optimizeDeps.noDiscovery ?? false, + holdUntilCrawlEnd: optimizeDeps.holdUntilCrawlEnd ?? true, + esbuildOptions: { + preserveSymlinks, // TODO: ? + ...optimizeDeps.esbuildOptions, + }, + disabled: optimizeDeps.disabled, + } +} + export async function resolveConfig( inlineConfig: InlineConfig, command: 'build' | 'serve', @@ -511,6 +818,27 @@ export async function resolveConfig( const [prePlugins, normalPlugins, postPlugins] = sortUserPlugins(rawUserPlugins) + const isBuild = command === 'build' + + // Ensure default client and ssr environments + // If there are present, ensure order { client, ssr, ...custom } + config.environments ??= {} + if ( + !config.environments.ssr && + (!isBuild || config.ssr || config.build?.ssr) + ) { + // During dev, the ssr environment is always available even if it isn't configure + // There is no perf hit, because the optimizer is initialized only if ssrLoadModule + // is called. + // During build, we only build the ssr environment if it is configured + // through the deprecated ssr top level options or if it is explicitely defined + // in the environments config + config.environments = { ssr: {}, ...config.environments } + } + if (!config.environments.client) { + config.environments = { client: {}, ...config.environments } + } + // run config hooks const userPlugins = [...prePlugins, ...normalPlugins, ...postPlugins] config = await runConfigHook(config, userPlugins, configEnv) @@ -528,45 +856,139 @@ export async function resolveConfig( checkBadCharactersInPath(resolvedRoot, logger) - const clientAlias = [ - { - find: /^\/?@vite\/env/, - replacement: path.posix.join(FS_PREFIX, normalizePath(ENV_ENTRY)), - }, - { - find: /^\/?@vite\/client/, - replacement: path.posix.join(FS_PREFIX, normalizePath(CLIENT_ENTRY)), - }, - ] - - // resolve alias with internal client alias - const resolvedAlias = normalizeAlias( - mergeAlias(clientAlias, config.resolve?.alias || []), + // Backward compatibility: merge optimizeDeps into environments.client.dev.optimizeDeps as defaults + // TODO: should entries and force be in EnvironmentOptions? + const { entries, force, ...deprecatedClientOptimizeDepsConfig } = + config.optimizeDeps ?? {} + let configEnvironmentsClient = config.environments!.client! + configEnvironmentsClient.dev ??= {} + configEnvironmentsClient.dev.optimizeDeps = mergeConfig( + configEnvironmentsClient.dev.optimizeDeps ?? {}, + deprecatedClientOptimizeDepsConfig, ) - const resolveOptions: ResolvedConfig['resolve'] = { - mainFields: config.resolve?.mainFields ?? DEFAULT_MAIN_FIELDS, - conditions: config.resolve?.conditions ?? [], - extensions: config.resolve?.extensions ?? DEFAULT_EXTENSIONS, - dedupe: config.resolve?.dedupe ?? [], - preserveSymlinks: config.resolve?.preserveSymlinks ?? false, - alias: resolvedAlias, + const deprecatedSsrOptimizeDepsConfig = config.ssr?.optimizeDeps ?? {} + let configEnvironmentsSsr = config.environments!.ssr + + // Backward compatibility: server.warmup.clientFiles/ssrFiles -> environment.dev.warmup + const warmupOptions = config.server?.warmup + if (warmupOptions?.clientFiles) { + configEnvironmentsClient.dev.warmup = warmupOptions?.clientFiles + } + if (warmupOptions?.ssrFiles) { + configEnvironmentsSsr ??= {} + configEnvironmentsSsr.dev ??= {} + configEnvironmentsSsr.dev.warmup = warmupOptions?.ssrFiles + } + + // Backward compatibility: merge ssr into environments.ssr.config as defaults + if (configEnvironmentsSsr) { + configEnvironmentsSsr.dev ??= {} + configEnvironmentsSsr.dev.optimizeDeps = mergeConfig( + configEnvironmentsSsr.dev.optimizeDeps ?? {}, + deprecatedSsrOptimizeDepsConfig, + ) + // TODO: should we merge here? + configEnvironmentsSsr.resolve ??= {} + configEnvironmentsSsr.resolve.conditions ??= config.ssr?.resolve?.conditions + configEnvironmentsSsr.resolve.externalConditions ??= + config.ssr?.resolve?.externalConditions + configEnvironmentsSsr.resolve.external ??= config.ssr?.external + configEnvironmentsSsr.resolve.noExternal ??= config.ssr?.noExternal + + if (config.ssr?.target === 'webworker') { + configEnvironmentsSsr.webCompatible = true + } } + // The client and ssr environment configs can't be removed by the user in the config hook if ( - // @ts-expect-error removed field - config.resolve?.browserField === false && - resolveOptions.mainFields.includes('browser') + !config.environments || + !config.environments.client || + (!config.environments.ssr && !isBuild) ) { - logger.warn( - colors.yellow( - `\`resolve.browserField\` is set to false, but the option is removed in favour of ` + - `the 'browser' string in \`resolve.mainFields\`. You may want to update \`resolve.mainFields\` ` + - `to remove the 'browser' string and preserve the previous browser behaviour.`, - ), + throw new Error( + 'Required environments configuration were stripped out in the config hook', ) } + // Merge default environment config values + const defaultEnvironmentOptions = getDefaultEnvironmentOptions(config) + for (const name of Object.keys(config.environments)) { + config.environments[name] = mergeConfig( + defaultEnvironmentOptions, + config.environments[name], + ) + } + + await runConfigEnvironmentHook(config.environments, userPlugins, configEnv) + + const resolvedEnvironments: Record = {} + for (const name of Object.keys(config.environments)) { + resolvedEnvironments[name] = resolveEnvironmentOptions( + config.environments[name], + resolvedRoot, + logger, + name, + config.experimental?.skipSsrTransform, + ) + } + + const resolvedDefaultEnvironmentResolve = resolveEnvironmentResolveOptions( + config.resolve, + logger, + ) + + // Backward compatibility: merge environments.client.dev.optimizeDeps back into optimizeDeps + configEnvironmentsClient = resolvedEnvironments.client + const patchedOptimizeDeps = mergeConfig( + configEnvironmentsClient.dev?.optimizeDeps ?? {}, + config.optimizeDeps ?? {}, + ) + const backwardCompatibleOptimizeDeps = { + holdUntilCrawlEnd: true, + ...patchedOptimizeDeps, + esbuildOptions: { + preserveSymlinks: resolvedDefaultEnvironmentResolve.preserveSymlinks, + ...patchedOptimizeDeps.esbuildOptions, + }, + } + + // TODO: Deprecate and remove resolve, dev and build options at the root level of the resolved config + + const resolvedDevEnvironmentOptions = resolveDevEnvironmentOptions( + config.dev, + resolvedDefaultEnvironmentResolve.preserveSymlinks, + undefined, // default environment + ) + + const resolvedBuildOptions = resolveBuildOptions( + config.build ?? {}, + logger, + resolvedRoot, + ) + + // Backward compatibility: merge config.environments.ssr back into config.ssr + // so ecosystem SSR plugins continue to work if only environments.ssr is configured + const patchedConfigSsr = { + ...config.ssr, + external: resolvedEnvironments.ssr?.resolve.external, + noExternal: resolvedEnvironments.ssr?.resolve.noExternal, + optimizeDeps: mergeConfig( + config.ssr?.optimizeDeps ?? {}, + resolvedEnvironments.ssr?.dev?.optimizeDeps ?? {}, + ), + resolve: { + ...config.ssr?.resolve, + conditions: resolvedEnvironments.ssr?.resolve.conditions, + externalConditions: resolvedEnvironments.ssr?.resolve.externalConditions, + }, + } + const ssr = resolveSSROptions( + patchedConfigSsr, + resolvedDefaultEnvironmentResolve.preserveSymlinks, + ) + // load .env files const envDir = config.envDir ? normalizePath(path.resolve(resolvedRoot, config.envDir)) @@ -595,7 +1017,6 @@ export async function resolveConfig( const isProduction = process.env.NODE_ENV === 'production' // resolve public base url - const isBuild = command === 'build' const relativeBaseShortcut = config.base === '' || config.base === './' // During dev, we ignore relative base and fallback to '/' @@ -607,12 +1028,6 @@ export async function resolveConfig( : './' : resolveBaseUrl(config.base, isBuild, logger) ?? '/' - const resolvedBuildOptions = resolveBuildOptions( - config.build, - logger, - resolvedRoot, - ) - // resolve cache directory const pkgDir = findNearestPackageData(resolvedRoot, packageCache)?.dir const cacheDir = normalizePath( @@ -629,52 +1044,6 @@ export async function resolveConfig( ? createFilter(config.assetsInclude) : () => false - // create an internal resolver to be used in special scenarios, e.g. - // optimizer & handling css @imports - const createResolver: ResolvedConfig['createResolver'] = (options) => { - let aliasContainer: PluginContainer | undefined - let resolverContainer: PluginContainer | undefined - return async (id, importer, aliasOnly, ssr) => { - let container: PluginContainer - if (aliasOnly) { - container = - aliasContainer || - (aliasContainer = await createPluginContainer({ - ...resolved, - plugins: [aliasPlugin({ entries: resolved.resolve.alias })], - })) - } else { - container = - resolverContainer || - (resolverContainer = await createPluginContainer({ - ...resolved, - plugins: [ - aliasPlugin({ entries: resolved.resolve.alias }), - resolvePlugin({ - ...resolved.resolve, - root: resolvedRoot, - isProduction, - isBuild: command === 'build', - ssrConfig: resolved.ssr, - asSrc: true, - preferRelative: false, - tryIndex: true, - ...options, - idOnly: true, - fsUtils: getFsUtils(resolved), - }), - ], - })) - } - return ( - await container.resolveId(id, importer, { - ssr, - scan: options?.scan, - }) - )?.id - } - } - const { publicDir } = config const resolvedPublicDir = publicDir !== false && publicDir !== '' @@ -687,9 +1056,8 @@ export async function resolveConfig( : '' const server = resolveServerOptions(resolvedRoot, config.server, logger) - const ssr = resolveSSROptions(config.ssr, resolveOptions.preserveSymlinks) - const optimizeDeps = config.optimizeDeps || {} + const builder = resolveBuilderOptions(config.builder) const BASE_URL = resolvedBase @@ -772,12 +1140,10 @@ export async function resolveConfig( root: resolvedRoot, base: withTrailingSlash(resolvedBase), rawBase: resolvedBase, - resolve: resolveOptions, publicDir: resolvedPublicDir, cacheDir, command, mode, - ssr, isWorker: false, mainConfig: null, bundleChain: [], @@ -792,7 +1158,7 @@ export async function resolveConfig( ...config.esbuild, }, server, - build: resolvedBuildOptions, + builder, preview: resolvePreviewOptions(config.preview, server), envDir, env: { @@ -807,15 +1173,6 @@ export async function resolveConfig( }, logger, packageCache, - createResolver, - optimizeDeps: { - holdUntilCrawlEnd: true, - ...optimizeDeps, - esbuildOptions: { - preserveSymlinks: resolveOptions.preserveSymlinks, - ...optimizeDeps.esbuildOptions, - }, - }, worker: resolvedWorkerOptions, appType: config.appType ?? 'spa', experimental: { @@ -823,8 +1180,98 @@ export async function resolveConfig( hmrPartialAccept: false, ...config.experimental, }, + + // Backward compatibility, users should use environment.config.dev.optimizeDeps + optimizeDeps: backwardCompatibleOptimizeDeps, + ssr, + + // TODO: deprecate and later remove from ResolvedConfig? + resolve: resolvedDefaultEnvironmentResolve, + dev: resolvedDevEnvironmentOptions, + build: resolvedBuildOptions, + + environments: resolvedEnvironments, + getSortedPlugins: undefined!, getSortedPluginHooks: undefined!, + + // createResolver is deprecated. It only works for the client and ssr + // environments. The `aliasOnly` option is also not being used any more + // Plugins should move to createIdResolver(environment) instead. + // create an internal resolver to be used in special scenarios, e.g. + // optimizer & handling css @imports + createResolver(options) { + const alias: { + client?: BoundedPluginContainer + ssr?: BoundedPluginContainer + } = {} + const resolver: { + client?: BoundedPluginContainer + ssr?: BoundedPluginContainer + } = {} + const environments = this.environments ?? resolvedEnvironments + const createPluginContainer = async ( + environmentName: string, + plugins: Plugin[], + ) => { + // The used alias and resolve plugins only use configuration options from the + // environment so we can safely cast to a base Environment instance to a + // PluginEnvironment here + const environment = new Environment(environmentName, this) + const pluginContainer = await createBoundedPluginContainer( + environment as PluginEnvironment, + plugins, + ) + await pluginContainer.buildStart({}) + return pluginContainer + } + async function resolve( + id: string, + importer?: string, + aliasOnly?: boolean, + ssr?: boolean, + ): Promise { + const environmentName = ssr ? 'ssr' : 'client' + let container: BoundedPluginContainer + if (aliasOnly) { + let aliasContainer = alias[environmentName] + if (!aliasContainer) { + aliasContainer = alias[environmentName] = + await createPluginContainer(environmentName, [ + aliasPlugin({ entries: resolved.resolve.alias }), + ]) + } + container = aliasContainer + } else { + let resolverContainer = resolver[environmentName] + if (!resolverContainer) { + resolverContainer = resolver[environmentName] = + await createPluginContainer(environmentName, [ + aliasPlugin({ entries: resolved.resolve.alias }), + resolvePlugin( + { + ...resolved.resolve, + root: resolvedRoot, + isProduction, + isBuild: command === 'build', + asSrc: true, + preferRelative: false, + tryIndex: true, + ...options, + idOnly: true, + fsUtils: getFsUtils(resolved), + }, + environments, + ), + ]) + } + container = resolverContainer + } + return await container.resolveId(id, importer, { scan: options?.scan }) + } + return async (id, importer, aliasOnly, ssr) => + (await resolve(id, importer, aliasOnly, ssr))?.id + }, } resolved = { ...config, @@ -863,20 +1310,6 @@ export async function resolveConfig( // validate config - if ( - config.build?.terserOptions && - config.build.minify && - config.build.minify !== 'terser' - ) { - logger.warn( - colors.yellow( - `build.terserOptions is specified but build.minify is not set to use Terser. ` + - `Note Vite now defaults to use esbuild for minification. If you still ` + - `prefer Terser, set build.minify to "terser".`, - ), - ) - } - // Check if all assetFileNames have the same reference. // If not, display a warn for user. const outputOption = config.build?.rollupOptions?.output ?? [] @@ -1098,26 +1531,26 @@ async function bundleConfigFile( importer: string, isRequire: boolean, ) => { - return tryNodeResolve( - id, - importer, - { - root: path.dirname(fileName), - isBuild: true, - isProduction: true, - preferRelative: false, - tryIndex: true, - mainFields: [], - conditions: [], - overrideConditions: ['node'], - dedupe: [], - extensions: DEFAULT_EXTENSIONS, - preserveSymlinks: false, - packageCache, - isRequire, - }, - false, - )?.id + 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, + nodeCompatible: true, + })?.id } // externalize bare imports @@ -1287,6 +1720,26 @@ async function runConfigHook( return conf } +async function runConfigEnvironmentHook( + environments: Record, + plugins: Plugin[], + configEnv: ConfigEnv, +): Promise { + const environmentNames = Object.keys(environments) + for (const p of getSortedPluginsByHook('configEnvironment', plugins)) { + const hook = p.configEnvironment + const handler = getHookHandler(hook) + if (handler) { + for (const name of environmentNames) { + const res = await handler(name, environments[name], configEnv) + if (res) { + environments[name] = mergeConfig(environments[name], res) + } + } + } + } +} + export function getDepOptimizationConfig( config: ResolvedConfig, ssr: boolean, diff --git a/packages/vite/src/node/environment.ts b/packages/vite/src/node/environment.ts new file mode 100644 index 00000000000000..7b5b71b61a98ff --- /dev/null +++ b/packages/vite/src/node/environment.ts @@ -0,0 +1,93 @@ +import colors from 'picocolors' +import type { Logger } from './logger' +import type { ResolvedConfig, ResolvedEnvironmentOptions } from './config' +import type { BoundedPlugin } from './plugin' + +export class Environment { + name: string + + config: ResolvedConfig + options: ResolvedEnvironmentOptions + + get plugins(): BoundedPlugin[] { + if (!this._plugins) + throw new Error( + `${this.name} environment.plugins called before initialized`, + ) + return this._plugins + } + /** + * @internal + */ + _plugins: BoundedPlugin[] | undefined + /** + * @internal + */ + _inited: boolean = false + + #logger: Logger | undefined + get logger(): Logger { + if (this.#logger) { + return this.#logger + } + const environment = colors.dim(`(${this.name})`) + const colorIndex = + Number([...environment].map((c) => c.charCodeAt(0))) % + environmentColors.length + const infoColor = environmentColors[colorIndex || 0] + const logger = this.config.logger + this.#logger = { + get hasWarned() { + return logger.hasWarned + }, + info(msg, opts) { + return logger.info(msg, { + ...opts, + environment: infoColor(environment), + }) + }, + warn(msg, opts) { + return logger.warn(msg, { + ...opts, + environment: colors.yellow(environment), + }) + }, + warnOnce(msg, opts) { + return logger.warnOnce(msg, { + ...opts, + environment: colors.yellow(environment), + }) + }, + error(msg, opts) { + return logger.error(msg, { + ...opts, + environment: colors.red(environment), + }) + }, + clearScreen(type) { + return logger.clearScreen(type) + }, + hasErrorLogged(error) { + return logger.hasErrorLogged(error) + }, + } + return this.#logger + } + + constructor( + name: string, + config: ResolvedConfig, + options: ResolvedEnvironmentOptions = config.environments[name], + ) { + this.name = name + this.config = config + this.options = options + } +} + +const environmentColors = [ + colors.blue, + colors.magenta, + colors.green, + colors.gray, +] diff --git a/packages/vite/src/node/ssr/ssrExternal.ts b/packages/vite/src/node/external.ts similarity index 56% rename from packages/vite/src/node/ssr/ssrExternal.ts rename to packages/vite/src/node/external.ts index 5681e000502a5f..6d593cbb0d42b5 100644 --- a/packages/vite/src/node/ssr/ssrExternal.ts +++ b/packages/vite/src/node/external.ts @@ -1,53 +1,74 @@ import path from 'node:path' -import type { InternalResolveOptions } from '../plugins/resolve' -import { tryNodeResolve } from '../plugins/resolve' +import type { InternalResolveOptions } from './plugins/resolve' +import { tryNodeResolve } from './plugins/resolve' import { bareImportRE, createDebugger, createFilter, getNpmPackageName, isBuiltin, -} from '../utils' -import type { ResolvedConfig } from '..' +} from './utils' +import type { Environment } from './environment' -const debug = createDebugger('vite:ssr-external') +const debug = createDebugger('vite:external') -const isSsrExternalCache = new WeakMap< - ResolvedConfig, +const isExternalCache = new WeakMap< + Environment, (id: string, importer?: string) => boolean | undefined >() -export function shouldExternalizeForSSR( +export function shouldExternalize( + environment: Environment, id: string, importer: string | undefined, - config: ResolvedConfig, ): boolean | undefined { - let isSsrExternal = isSsrExternalCache.get(config) - if (!isSsrExternal) { - isSsrExternal = createIsSsrExternal(config) - isSsrExternalCache.set(config, isSsrExternal) + let isExternal = isExternalCache.get(environment) + if (!isExternal) { + isExternal = createIsExternal(environment) + isExternalCache.set(environment, isExternal) } - return isSsrExternal(id, importer) + return isExternal(id, importer) } -export function createIsConfiguredAsSsrExternal( - config: ResolvedConfig, +const isConfiguredAsExternalCache = new WeakMap< + Environment, + (id: string, importer?: string) => boolean +>() + +export function isConfiguredAsExternal( + environment: Environment, + id: string, + importer?: string, +): boolean { + let isExternal = isConfiguredAsExternalCache.get(environment) + if (!isExternal) { + isExternal = createIsConfiguredAsExternal(environment) + isConfiguredAsExternalCache.set(environment, isExternal) + } + return isExternal(id, importer) +} + +export function createIsConfiguredAsExternal( + environment: Environment, ): (id: string, importer?: string) => boolean { - const { ssr, root } = config - const noExternal = ssr?.noExternal + const { config, options } = environment + const { root } = config + const { external, noExternal } = options.resolve const noExternalFilter = - noExternal !== 'undefined' && typeof noExternal !== 'boolean' && + !(Array.isArray(noExternal) && noExternal.length === 0) && createFilter(undefined, noExternal, { resolve: false }) - const targetConditions = config.ssr.resolve?.externalConditions || [] + const targetConditions = options.resolve?.externalConditions || [] const resolveOptions: InternalResolveOptions = { - ...config.resolve, + ...options.resolve, root, isProduction: false, isBuild: true, conditions: targetConditions, + webCompatible: options.webCompatible, + nodeCompatible: options.nodeCompatible, } const isExternalizable = ( @@ -65,7 +86,6 @@ export function createIsConfiguredAsSsrExternal( // unresolvable from root (which would be unresolvable from output bundles also) config.command === 'build' ? undefined : importer, resolveOptions, - ssr?.target === 'webworker', undefined, true, // try to externalize, will return undefined or an object without @@ -89,9 +109,9 @@ export function createIsConfiguredAsSsrExternal( return (id: string, importer?: string) => { if ( // If this id is defined as external, force it as external - // Note that individual package entries are allowed in ssr.external - ssr.external !== true && - ssr.external?.includes(id) + // Note that individual package entries are allowed in `external` + external !== true && + external.includes(id) ) { return true } @@ -102,8 +122,8 @@ export function createIsConfiguredAsSsrExternal( if ( // A package name in ssr.external externalizes every // externalizable package entry - ssr.external !== true && - ssr.external?.includes(pkgName) + external !== true && + external.includes(pkgName) ) { return isExternalizable(id, importer, true) } @@ -113,28 +133,28 @@ export function createIsConfiguredAsSsrExternal( if (noExternalFilter && !noExternalFilter(pkgName)) { return false } - // If `ssr.external: true`, all will be externalized by default, regardless if + // If external is true, all will be externalized by default, regardless if // it's a linked package - return isExternalizable(id, importer, ssr.external === true) + return isExternalizable(id, importer, external === true) } } -function createIsSsrExternal( - config: ResolvedConfig, +function createIsExternal( + environment: Environment, ): (id: string, importer?: string) => boolean | undefined { const processedIds = new Map() - const isConfiguredAsExternal = createIsConfiguredAsSsrExternal(config) + const isConfiguredAsExternal = createIsConfiguredAsExternal(environment) return (id: string, importer?: string) => { if (processedIds.has(id)) { return processedIds.get(id) } - let external = false + let isExternal = false if (id[0] !== '.' && !path.isAbsolute(id)) { - external = isBuiltin(id) || isConfiguredAsExternal(id, importer) + isExternal = isBuiltin(id) || isConfiguredAsExternal(id, importer) } - processedIds.set(id, external) - return external + processedIds.set(id, isExternal) + return isExternal } } diff --git a/packages/vite/src/node/idResolver.ts b/packages/vite/src/node/idResolver.ts new file mode 100644 index 00000000000000..33e5b53a67ddd9 --- /dev/null +++ b/packages/vite/src/node/idResolver.ts @@ -0,0 +1,90 @@ +import type { PartialResolvedId } from 'rollup' +import aliasPlugin from '@rollup/plugin-alias' +import type { ResolvedConfig } from './config' +import type { Environment } from './environment' +import type { PluginEnvironment } from './plugin' +import type { BoundedPluginContainer } from './server/pluginContainer' +import { createBoundedPluginContainer } from './server/pluginContainer' +import { resolvePlugin } from './plugins/resolve' +import type { InternalResolveOptions } from './plugins/resolve' +import { getFsUtils } from './fsUtils' + +export type ResolveIdFn = ( + environment: Environment, + id: string, + importer?: string, + aliasOnly?: boolean, +) => Promise + +/** + * Create an internal resolver to be used in special scenarios, e.g. + * optimizer and handling css @imports + */ +export function createIdResolver( + config: ResolvedConfig, + options: Partial, +): ResolveIdFn { + const scan = options?.scan + + const pluginContainerMap = new Map() + async function resolve( + environment: PluginEnvironment, + id: string, + importer?: string, + ): Promise { + let pluginContainer = pluginContainerMap.get(environment) + if (!pluginContainer) { + pluginContainer = await createBoundedPluginContainer(environment, [ + aliasPlugin({ entries: config.resolve.alias }), // TODO: resolve.alias per environment? + resolvePlugin( + { + root: config.root, + isProduction: config.isProduction, + isBuild: config.command === 'build', + asSrc: true, + preferRelative: false, + tryIndex: true, + ...options, + fsUtils: getFsUtils(config), + // Ignore sideEffects and other computations as we only need the id + idOnly: true, + }, + config.environments, + ), + ]) + pluginContainerMap.set(environment, pluginContainer) + } + return await pluginContainer.resolveId(id, importer, { scan }) + } + + const aliasOnlyPluginContainerMap = new Map< + Environment, + BoundedPluginContainer + >() + async function resolveAlias( + environment: PluginEnvironment, + id: string, + importer?: string, + ): Promise { + let pluginContainer = aliasOnlyPluginContainerMap.get(environment) + if (!pluginContainer) { + pluginContainer = await createBoundedPluginContainer(environment, [ + aliasPlugin({ entries: config.resolve.alias }), // TODO: resolve.alias per environment? + ]) + aliasOnlyPluginContainerMap.set(environment, pluginContainer) + } + return await pluginContainer.resolveId(id, importer, { scan }) + } + + return async (environment, id, importer, aliasOnly) => { + const resolveFn = aliasOnly ? resolveAlias : resolve + // aliasPlugin and resolvePlugin are implemented to function with a Environment only, + // we cast it as PluginEnvironment to be able to use the pluginContainer + const resolved = await resolveFn( + environment as PluginEnvironment, + id, + importer, + ) + return resolved?.id + } +} diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index 3b84c34b0626a8..3cedcfc2563d2e 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -10,13 +10,24 @@ export { } from './config' export { createServer } from './server' export { preview } from './preview' -export { build } from './build' -export { optimizeDeps } from './optimizer' +export { build, createViteBuilder } from './build' + +// TODO: Can we remove this? +// export { optimizeDeps } from './optimizer' + export { formatPostcssSourceMap, preprocessCSS } from './plugins/css' export { transformWithEsbuild } from './plugins/esbuild' export { buildErrorMessage } from './server/middlewares/error' -export { fetchModule } from './ssr/fetchModule' -export type { FetchModuleOptions } from './ssr/fetchModule' + +export { RemoteEnvironmentTransport } from './server/environmentTransport' +export { createNodeDevEnvironment } from './server/environments/nodeEnvironment' +export { DevEnvironment, type DevEnvironmentSetup } from './server/environment' +export { BuildEnvironment } from './build' + +export { fetchModule, type FetchModuleOptions } from './ssr/fetchModule' +export { createServerModuleRunner } from './ssr/runtime/serverModuleRunner' +export { ServerHMRConnector } from './ssr/runtime/serverHmrConnector' + export * from './publicUtils' // additional types @@ -28,7 +39,6 @@ export type { InlineConfig, LegacyOptions, PluginHookUtils, - PluginOption, ResolveFn, ResolvedWorkerOptions, ResolvedConfig, @@ -38,6 +48,7 @@ export type { UserConfigFnObject, UserConfigFnPromise, } from './config' +export type { PluginOption } from './plugin' export type { FilterPattern } from './utils' export type { CorsOptions, CorsOrigin, CommonServerOptions } from './http' export type { @@ -50,10 +61,12 @@ export type { } from './server' export type { BuildOptions, + BuildEnvironmentOptions, LibraryOptions, LibraryFormats, RenderBuiltAssetUrl, ResolvedBuildOptions, + ResolvedBuildEnvironmentOptions, ModulePreloadOptions, ResolvedModulePreloadOptions, ResolveModulePreloadDependenciesFn, @@ -113,14 +126,18 @@ export type { WebSocketCustomListener, } from './server/ws' export type { PluginContainer } from './server/pluginContainer' -export type { ModuleGraph, ModuleNode, ResolvedUrl } from './server/moduleGraph' +export type { + EnvironmentModuleGraph, + EnvironmentModuleNode, + ResolvedUrl, +} from './server/moduleGraph' export type { SendOptions } from './server/send' export type { ProxyOptions } from './server/middlewares/proxy' export type { TransformOptions, TransformResult, } from './server/transformRequest' -export type { HmrOptions, HmrContext } from './server/hmr' +export type { HmrOptions, HmrContext, HotUpdateContext } from './server/hmr' export type { HMRBroadcaster, @@ -129,10 +146,8 @@ export type { HMRBroadcasterClient, } from './server/hmr' -export type { FetchFunction } from '../runtime/index' -export { createViteRuntime } from './ssr/runtime/mainThreadRuntime' -export type { MainThreadRuntimeOptions } from './ssr/runtime/mainThreadRuntime' -export { ServerHMRConnector } from './ssr/runtime/serverHmrConnector' +export type { FetchFunction, FetchResult } from 'vite/module-runner' +export type { ServerModuleRunnerOptions } from './ssr/runtime/serverModuleRunner' export type { BindCLIShortcutsOptions, CLIShortcut } from './shortcuts' @@ -180,3 +195,6 @@ export type { RollupCommonJSOptions } from 'dep-types/commonjs' export type { RollupDynamicImportVarsOptions } from 'dep-types/dynamicImportVars' export type { Matcher, AnymatchPattern, AnymatchFn } from 'dep-types/anymatch' export type { LightningCSSOptions } from 'dep-types/lightningcss' + +// Backward compatibility +export type { ModuleGraph, ModuleNode } from './server/mixedModuleGraph' diff --git a/packages/vite/src/node/logger.ts b/packages/vite/src/node/logger.ts index 8600228e305de1..b41ad8f4d6cca9 100644 --- a/packages/vite/src/node/logger.ts +++ b/packages/vite/src/node/logger.ts @@ -20,6 +20,7 @@ export interface Logger { export interface LogOptions { clear?: boolean timestamp?: boolean + environment?: string } export interface LogErrorOptions extends LogOptions { @@ -80,15 +81,17 @@ export function createLogger( function format(type: LogType, msg: string, options: LogErrorOptions = {}) { if (options.timestamp) { - const tag = + const color = type === 'info' - ? colors.cyan(colors.bold(prefix)) + ? colors.cyan : type === 'warn' - ? colors.yellow(colors.bold(prefix)) - : colors.red(colors.bold(prefix)) + ? colors.yellow + : colors.red + const tag = color(colors.bold(prefix)) + const environment = options.environment ? options.environment + ' ' : '' return `${colors.dim( getTimeFormatter().format(new Date()), - )} ${tag} ${msg}` + )} ${tag} ${environment}${msg}` } else { return msg } diff --git a/packages/vite/src/node/optimizer/esbuildDepPlugin.ts b/packages/vite/src/node/optimizer/esbuildDepPlugin.ts index 08b1abb72d48e3..768e14d5555927 100644 --- a/packages/vite/src/node/optimizer/esbuildDepPlugin.ts +++ b/packages/vite/src/node/optimizer/esbuildDepPlugin.ts @@ -2,8 +2,6 @@ import path from 'node:path' import type { ImportKind, Plugin } from 'esbuild' import { KNOWN_ASSET_TYPES } from '../constants' import type { PackageCache } from '../packages' -import { getDepOptimizationConfig } from '../config' -import type { ResolvedConfig } from '../config' import { escapeRegex, flattenId, @@ -14,6 +12,8 @@ import { } from '../utils' import { browserExternalId, optionalPeerDepId } from '../plugins/resolve' import { isCSSRequest, isModuleCSSRequest } from '../plugins/css' +import type { Environment } from '../environment' +import { createIdResolver } from '../idResolver' const externalWithConversionNamespace = 'vite:dep-pre-bundle:external-conversion' @@ -48,12 +48,12 @@ const externalTypes = [ ] export function esbuildDepPlugin( + environment: Environment, qualified: Record, external: string[], - config: ResolvedConfig, - ssr: boolean, ): Plugin { - const { extensions } = getDepOptimizationConfig(config, ssr) + const { config } = environment + const { extensions } = environment.options.dev.optimizeDeps // remove optimizable extensions from `externalTypes` list const allExternalTypes = extensions @@ -66,14 +66,14 @@ export function esbuildDepPlugin( const cjsPackageCache: PackageCache = new Map() // default resolver which prefers ESM - const _resolve = config.createResolver({ + const _resolve = createIdResolver(config, { asSrc: false, scan: true, packageCache: esmPackageCache, }) // cjs resolver that prefers Node - const _resolveRequire = config.createResolver({ + const _resolveRequire = createIdResolver(config, { asSrc: false, isRequire: true, scan: true, @@ -96,7 +96,7 @@ export function esbuildDepPlugin( _importer = importer in qualified ? qualified[importer] : importer } const resolver = kind.startsWith('require') ? _resolveRequire : _resolve - return resolver(id, _importer, undefined, ssr) + return resolver(environment, id, _importer) } const resolveResult = (id: string, resolved: string) => { @@ -112,6 +112,7 @@ export function esbuildDepPlugin( namespace: 'optional-peer-dep', } } + const ssr = environment.name !== 'client' // TODO:depsOptimizer how to avoid depending on environment name? if (ssr && isBuiltin(resolved)) { return } @@ -209,7 +210,7 @@ export function esbuildDepPlugin( if (!importer) { if ((entry = resolveEntry(id))) return entry // check if this is aliased to an entry - also return entry namespace - const aliased = await _resolve(id, undefined, true) + const aliased = await _resolve(environment, id, undefined, true) if (aliased && (entry = resolveEntry(aliased))) { return entry } diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index f8028c070a9c34..a339e88eefa6ac 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -8,7 +8,6 @@ import type { BuildContext, BuildOptions as EsbuildBuildOptions } from 'esbuild' import esbuild, { build } from 'esbuild' import { init, parse } from 'es-module-lexer' import glob from 'fast-glob' -import { getDepOptimizationConfig } from '../config' import type { ResolvedConfig } from '../config' import { createDebugger, @@ -28,14 +27,10 @@ import { } from '../plugins/esbuild' import { ESBUILD_MODULES_TARGET, METADATA_FILENAME } from '../constants' import { isWindows } from '../../shared/utils' +import type { Environment } from '../environment' import { esbuildCjsExternalPlugin, esbuildDepPlugin } from './esbuildDepPlugin' -import { scanImports } from './scan' +import { ScanEnvironment, scanImports } from './scan' import { createOptimizeDepsIncludeResolver, expandGlobIds } from './resolve' -export { - initDepsOptimizer, - initDevSsrDepsOptimizer, - getDepsOptimizer, -} from './optimizer' const debug = createDebugger('vite:deps') @@ -51,6 +46,8 @@ export type ExportsData = { } export interface DepsOptimizer { + init: () => Promise + metadata: DepOptimizationMetadata scanProcessing?: Promise registerMissingImport: (id: string, resolved: string) => OptimizedDepInfo @@ -166,6 +163,9 @@ export type DepOptimizationOptions = DepOptimizationConfig & { force?: boolean } +// TODO: We first need to define if entries and force should be per-environment +// export type ResolvedDepOptimizationConfig = Required + export interface DepOptimizationResult { metadata: DepOptimizationMetadata /** @@ -240,17 +240,22 @@ export interface DepOptimizationMetadata { * Scan and optimize dependencies within a project. * Used by Vite CLI when running `vite optimize`. */ +// TODO: do we need this? It is exposed for the CLI command `vite optimize` + export async function optimizeDeps( config: ResolvedConfig, force = config.optimizeDeps.force, asCommand = false, ): Promise { const log = asCommand ? config.logger.info : debug - const ssr = false + + // TODO: Could we avoid the creation of a DevEnvironment moving the plugin resolving to + // the Environment base class? + const environment = new ScanEnvironment('client', config) + await environment.init() const cachedMetadata = await loadCachedDepOptimizationMetadata( - config, - ssr, + environment, force, asCommand, ) @@ -258,30 +263,28 @@ export async function optimizeDeps( return cachedMetadata } - const deps = await discoverProjectDependencies(config).result + const deps = await discoverProjectDependencies(environment).result const depsString = depsLogString(Object.keys(deps)) log?.(colors.green(`Optimizing dependencies:\n ${depsString}`)) - await addManuallyIncludedOptimizeDeps(deps, config, ssr) + await addManuallyIncludedOptimizeDeps(environment, deps) - const depsInfo = toDiscoveredDependencies(config, deps, ssr) + const depsInfo = toDiscoveredDependencies(environment, deps) - const result = await runOptimizeDeps(config, depsInfo, ssr).result + const result = await runOptimizeDeps(environment, depsInfo).result await result.commit() return result.metadata } -export async function optimizeServerSsrDeps( - config: ResolvedConfig, +export async function optimizeExplicitEnvironmentDeps( + environment: Environment, ): Promise { - const ssr = true const cachedMetadata = await loadCachedDepOptimizationMetadata( - config, - ssr, - config.optimizeDeps.force, + environment, + environment.config.optimizeDeps?.force ?? false, // TODO: should force be per-environment? false, ) if (cachedMetadata) { @@ -290,11 +293,11 @@ export async function optimizeServerSsrDeps( const deps: Record = {} - await addManuallyIncludedOptimizeDeps(deps, config, ssr) + await addManuallyIncludedOptimizeDeps(environment, deps) - const depsInfo = toDiscoveredDependencies(config, deps, ssr) + const depsInfo = toDiscoveredDependencies(environment, deps) - const result = await runOptimizeDeps(config, depsInfo, ssr).result + const result = await runOptimizeDeps(environment, depsInfo).result await result.commit() @@ -302,11 +305,10 @@ export async function optimizeServerSsrDeps( } export function initDepsOptimizerMetadata( - config: ResolvedConfig, - ssr: boolean, + environment: Environment, timestamp?: string, ): DepOptimizationMetadata { - const { lockfileHash, configHash, hash } = getDepHash(config, ssr) + const { lockfileHash, configHash, hash } = getDepHash(environment) return { hash, lockfileHash, @@ -336,20 +338,19 @@ let firstLoadCachedDepOptimizationMetadata = true * if it exists and pre-bundling isn't forced */ export async function loadCachedDepOptimizationMetadata( - config: ResolvedConfig, - ssr: boolean, - force = config.optimizeDeps.force, + environment: Environment, + force = environment.config.optimizeDeps?.force ?? false, asCommand = false, ): Promise { - const log = asCommand ? config.logger.info : debug + const log = asCommand ? environment.logger.info : debug if (firstLoadCachedDepOptimizationMetadata) { firstLoadCachedDepOptimizationMetadata = false // Fire up a clean up of stale processing deps dirs if older process exited early - setTimeout(() => cleanupDepsCacheStaleDirs(config), 0) + setTimeout(() => cleanupDepsCacheStaleDirs(environment.config), 0) } - const depsCacheDir = getDepsCacheDir(config, ssr) + const depsCacheDir = getDepsCacheDir(environment) if (!force) { let cachedMetadata: DepOptimizationMetadata | undefined @@ -362,12 +363,12 @@ export async function loadCachedDepOptimizationMetadata( } catch (e) {} // hash is consistent, no need to re-bundle if (cachedMetadata) { - if (cachedMetadata.lockfileHash !== getLockfileHash(config, ssr)) { - config.logger.info( + if (cachedMetadata.lockfileHash !== getLockfileHash(environment)) { + environment.logger.info( 'Re-optimizing dependencies because lockfile has changed', ) - } else if (cachedMetadata.configHash !== getConfigHash(config, ssr)) { - config.logger.info( + } else if (cachedMetadata.configHash !== getConfigHash(environment)) { + environment.logger.info( 'Re-optimizing dependencies because vite config has changed', ) } else { @@ -378,7 +379,7 @@ export async function loadCachedDepOptimizationMetadata( } } } else { - config.logger.info('Forced re-optimization of dependencies') + environment.logger.info('Forced re-optimization of dependencies') } // Start with a fresh cache @@ -390,11 +391,13 @@ export async function loadCachedDepOptimizationMetadata( * Initial optimizeDeps at server start. Perform a fast scan using esbuild to * find deps to pre-bundle and include user hard-coded dependencies */ -export function discoverProjectDependencies(config: ResolvedConfig): { +export function discoverProjectDependencies(environment: ScanEnvironment): { cancel: () => Promise result: Promise> } { - const { cancel, result } = scanImports(config) + // Should the scanner be per-environment? + // we only use it for the client right now + const { cancel, result } = scanImports(environment) return { cancel, @@ -419,13 +422,12 @@ export function discoverProjectDependencies(config: ResolvedConfig): { } export function toDiscoveredDependencies( - config: ResolvedConfig, + environment: Environment, deps: Record, - ssr: boolean, timestamp?: string, ): Record { const browserHash = getOptimizedBrowserHash( - getDepHash(config, ssr).hash, + getDepHash(environment).hash, deps, timestamp, ) @@ -434,10 +436,10 @@ export function toDiscoveredDependencies( const src = deps[id] discovered[id] = { id, - file: getOptimizedDepPath(id, config, ssr), + file: getOptimizedDepPath(environment, id), src, browserHash: browserHash, - exportsData: extractExportsData(src, config, ssr), + exportsData: extractExportsData(environment, src), } } return discovered @@ -452,9 +454,8 @@ export function depsLogString(qualifiedIds: string[]): string { * the metadata and start the server without waiting for the optimizeDeps processing to be completed */ export function runOptimizeDeps( - resolvedConfig: ResolvedConfig, + environment: Environment, depsInfo: Record, - ssr: boolean, ): { cancel: () => Promise result: Promise @@ -462,12 +463,12 @@ export function runOptimizeDeps( const optimizerContext = { cancelled: false } const config: ResolvedConfig = { - ...resolvedConfig, + ...environment.config, command: 'build', } - const depsCacheDir = getDepsCacheDir(resolvedConfig, ssr) - const processingCacheDir = getProcessingDepsCacheDir(resolvedConfig, ssr) + const depsCacheDir = getDepsCacheDir(environment) + const processingCacheDir = getProcessingDepsCacheDir(environment) // Create a temporary directory so we don't need to delete optimized deps // until they have been processed. This also avoids leaving the deps cache @@ -482,7 +483,7 @@ export function runOptimizeDeps( `{\n "type": "module"\n}\n`, ) - const metadata = initDepsOptimizerMetadata(config, ssr) + const metadata = initDepsOptimizerMetadata(environment) metadata.browserHash = getOptimizedBrowserHash( metadata.hash, @@ -594,9 +595,8 @@ export function runOptimizeDeps( const start = performance.now() const preparedRun = prepareEsbuildOptimizerRun( - resolvedConfig, + environment, depsInfo, - ssr, processingCacheDir, optimizerContext, ) @@ -644,8 +644,7 @@ export function runOptimizeDeps( // After bundling we have more information and can warn the user about legacy packages // that require manual configuration needsInterop: needsInterop( - config, - ssr, + environment, id, idToExports[id], output, @@ -658,7 +657,7 @@ export function runOptimizeDeps( const id = path .relative(processingCacheDirOutputPath, o) .replace(jsExtensionRE, '') - const file = getOptimizedDepPath(id, resolvedConfig, ssr) + const file = getOptimizedDepPath(environment, id) if ( !findOptimizedDepInfoInRecord( metadata.optimized, @@ -711,9 +710,8 @@ export function runOptimizeDeps( } async function prepareEsbuildOptimizerRun( - resolvedConfig: ResolvedConfig, + environment: Environment, depsInfo: Record, - ssr: boolean, processingCacheDir: string, optimizerContext: { cancelled: boolean }, ): Promise<{ @@ -721,7 +719,7 @@ async function prepareEsbuildOptimizerRun( idToExports: Record }> { const config: ResolvedConfig = { - ...resolvedConfig, + ...environment.config, command: 'build', } @@ -734,7 +732,7 @@ async function prepareEsbuildOptimizerRun( const flatIdDeps: Record = {} const idToExports: Record = {} - const optimizeDeps = getDepOptimizationConfig(config, ssr) + const { optimizeDeps } = environment.options.dev const { plugins: pluginsFromConfig = [], ...esbuildOptions } = optimizeDeps?.esbuildOptions ?? {} @@ -743,7 +741,7 @@ async function prepareEsbuildOptimizerRun( Object.keys(depsInfo).map(async (id) => { const src = depsInfo[id].src! const exportsData = await (depsInfo[id].exportsData ?? - extractExportsData(src, config, ssr)) + extractExportsData(environment, src)) if (exportsData.jsxLoader && !esbuildOptions.loader?.['.js']) { // Ensure that optimization won't fail by defaulting '.js' to the JSX parser. // This is useful for packages such as Gatsby. @@ -764,8 +762,7 @@ async function prepareEsbuildOptimizerRun( 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || config.mode), } - const platform = - ssr && config.ssr?.target !== 'webworker' ? 'node' : 'browser' + const platform = environment.options.webCompatible ? 'browser' : 'node' const external = [...(optimizeDeps?.exclude ?? [])] @@ -773,7 +770,7 @@ async function prepareEsbuildOptimizerRun( if (external.length) { plugins.push(esbuildCjsExternalPlugin(external, platform)) } - plugins.push(esbuildDepPlugin(flatIdDeps, external, config, ssr)) + plugins.push(esbuildDepPlugin(environment, flatIdDeps, external)) const context = await esbuild.context({ absWorkingDir: process.cwd(), @@ -812,20 +809,17 @@ async function prepareEsbuildOptimizerRun( } export async function addManuallyIncludedOptimizeDeps( + environment: Environment, deps: Record, - config: ResolvedConfig, - ssr: boolean, ): Promise { - const { logger } = config - const optimizeDeps = getDepOptimizationConfig(config, ssr) + const { logger } = environment + const { optimizeDeps } = environment.options.dev const optimizeDepsInclude = optimizeDeps?.include ?? [] if (optimizeDepsInclude.length) { const unableToOptimize = (id: string, msg: string) => { if (optimizeDepsInclude.includes(id)) { logger.warn( - `${msg}: ${colors.cyan(id)}, present in '${ - ssr ? 'ssr.' : '' - }optimizeDeps.include'`, + `${msg}: ${colors.cyan(id)}, present in ${environment.name} 'optimizeDeps.include'`, ) } } @@ -834,13 +828,13 @@ export async function addManuallyIncludedOptimizeDeps( for (let i = 0; i < includes.length; i++) { const id = includes[i] if (glob.isDynamicPattern(id)) { - const globIds = expandGlobIds(id, config) + const globIds = expandGlobIds(id, environment.config) includes.splice(i, 1, ...globIds) i += globIds.length - 1 } } - const resolve = createOptimizeDepsIncludeResolver(config, ssr) + const resolve = createOptimizeDepsIncludeResolver(environment) for (const id of includes) { // normalize 'foo >bar` as 'foo > bar' to prevent same id being added // and for pretty printing @@ -875,26 +869,27 @@ export function depsFromOptimizedDepInfo( } export function getOptimizedDepPath( + environment: Environment, id: string, - config: ResolvedConfig, - ssr: boolean, ): string { return normalizePath( - path.resolve(getDepsCacheDir(config, ssr), flattenId(id) + '.js'), + path.resolve(getDepsCacheDir(environment), flattenId(id) + '.js'), ) } -function getDepsCacheSuffix(ssr: boolean): string { - return ssr ? '_ssr' : '' +function getDepsCacheSuffix(environment: Environment): string { + return environment.name === 'client' ? '' : `_${environment.name}` } -export function getDepsCacheDir(config: ResolvedConfig, ssr: boolean): string { - return getDepsCacheDirPrefix(config) + getDepsCacheSuffix(ssr) +export function getDepsCacheDir(environment: Environment): string { + return getDepsCacheDirPrefix(environment) + getDepsCacheSuffix(environment) } -function getProcessingDepsCacheDir(config: ResolvedConfig, ssr: boolean) { +function getProcessingDepsCacheDir(environment: Environment) { return ( - getDepsCacheDirPrefix(config) + getDepsCacheSuffix(ssr) + getTempSuffix() + getDepsCacheDirPrefix(environment) + + getDepsCacheSuffix(environment) + + getTempSuffix() ) } @@ -909,22 +904,22 @@ function getTempSuffix() { ) } -function getDepsCacheDirPrefix(config: ResolvedConfig): string { - return normalizePath(path.resolve(config.cacheDir, 'deps')) +function getDepsCacheDirPrefix(environment: Environment): string { + return normalizePath(path.resolve(environment.config.cacheDir, 'deps')) } export function createIsOptimizedDepFile( - config: ResolvedConfig, + environment: Environment, ): (id: string) => boolean { - const depsCacheDirPrefix = getDepsCacheDirPrefix(config) + const depsCacheDirPrefix = getDepsCacheDirPrefix(environment) return (id) => id.startsWith(depsCacheDirPrefix) } export function createIsOptimizedDepUrl( - config: ResolvedConfig, + environment: Environment, ): (url: string) => boolean { - const { root } = config - const depsCacheDir = getDepsCacheDirPrefix(config) + const { root } = environment.config + const depsCacheDir = getDepsCacheDirPrefix(environment) // determine the url prefix of files inside cache directory const depsCacheDirRelative = normalizePath(path.relative(root, depsCacheDir)) @@ -1060,13 +1055,12 @@ function esbuildOutputFromId( } export async function extractExportsData( + environment: Environment, filePath: string, - config: ResolvedConfig, - ssr: boolean, ): Promise { await init - const optimizeDeps = getDepOptimizationConfig(config, ssr) + const { optimizeDeps } = environment.options.dev const esbuildOptions = optimizeDeps?.esbuildOptions ?? {} if (optimizeDeps.extensions?.some((ext) => filePath.endsWith(ext))) { @@ -1114,13 +1108,12 @@ export async function extractExportsData( } function needsInterop( - config: ResolvedConfig, - ssr: boolean, + environmet: Environment, id: string, exportsData: ExportsData, output?: { exports: string[] }, ): boolean { - if (getDepOptimizationConfig(config, ssr)?.needsInterop?.includes(id)) { + if (environmet.options.dev.optimizeDeps?.needsInterop?.includes(id)) { return true } const { hasModuleSyntax, exports } = exportsData @@ -1160,10 +1153,11 @@ const lockfileFormats = [ }) const lockfileNames = lockfileFormats.map((l) => l.name) -function getConfigHash(config: ResolvedConfig, ssr: boolean): string { +function getConfigHash(environment: Environment): string { // Take config into account // only a subset of config options that can affect dep optimization - const optimizeDeps = getDepOptimizationConfig(config, ssr) + const { optimizeDeps } = environment.options.dev + const { config } = environment const content = JSON.stringify( { mode: process.env.NODE_ENV || config.mode, @@ -1194,8 +1188,8 @@ function getConfigHash(config: ResolvedConfig, ssr: boolean): string { return getHash(content) } -function getLockfileHash(config: ResolvedConfig, ssr: boolean): string { - const lockfilePath = lookupFile(config.root, lockfileNames) +function getLockfileHash(environment: Environment): string { + const lockfilePath = lookupFile(environment.config.root, lockfileNames) let content = lockfilePath ? fs.readFileSync(lockfilePath, 'utf-8') : '' if (lockfilePath) { const lockfileName = path.basename(lockfilePath) @@ -1214,12 +1208,13 @@ function getLockfileHash(config: ResolvedConfig, ssr: boolean): string { return getHash(content) } -function getDepHash( - config: ResolvedConfig, - ssr: boolean, -): { lockfileHash: string; configHash: string; hash: string } { - const lockfileHash = getLockfileHash(config, ssr) - const configHash = getConfigHash(config, ssr) +function getDepHash(environment: Environment): { + lockfileHash: string + configHash: string + hash: string +} { + const lockfileHash = getLockfileHash(environment) + const configHash = getConfigHash(environment) const hash = getHash(lockfileHash + configHash) return { hash, @@ -1265,17 +1260,15 @@ function findOptimizedDepInfoInRecord( } export async function optimizedDepNeedsInterop( + environment: Environment, metadata: DepOptimizationMetadata, file: string, - config: ResolvedConfig, - ssr: boolean, ): Promise { const depInfo = optimizedDepInfoFromFile(metadata, file) if (depInfo?.src && depInfo.needsInterop === undefined) { - depInfo.exportsData ??= extractExportsData(depInfo.src, config, ssr) + depInfo.exportsData ??= extractExportsData(environment, depInfo.src) depInfo.needsInterop = needsInterop( - config, - ssr, + environment, depInfo.id, await depInfo.exportsData, ) diff --git a/packages/vite/src/node/optimizer/optimizer.ts b/packages/vite/src/node/optimizer/optimizer.ts index 096d0bef2cdd54..3a8808345bade9 100644 --- a/packages/vite/src/node/optimizer/optimizer.ts +++ b/packages/vite/src/node/optimizer/optimizer.ts @@ -1,8 +1,8 @@ import colors from 'picocolors' import { createDebugger, getHash, promiseWithResolvers } from '../utils' import type { PromiseWithResolvers } from '../utils' -import { getDepOptimizationConfig } from '../config' -import type { ResolvedConfig, ViteDevServer } from '..' +import type { DevEnvironment } from '../server/environment' +import { devToScanEnvironment } from './scan' import { addManuallyIncludedOptimizeDeps, addOptimizedDepInfo, @@ -15,11 +15,16 @@ import { getOptimizedDepPath, initDepsOptimizerMetadata, loadCachedDepOptimizationMetadata, - optimizeServerSsrDeps, + optimizeExplicitEnvironmentDeps, runOptimizeDeps, toDiscoveredDependencies, -} from '.' -import type { DepOptimizationResult, DepsOptimizer, OptimizedDepInfo } from '.' +} from './index' +import type { + DepOptimizationMetadata, + DepOptimizationResult, + DepsOptimizer, + OptimizedDepInfo, +} from './index' const debug = createDebugger('vite:deps') @@ -29,95 +34,45 @@ const debug = createDebugger('vite:deps') */ const debounceMs = 100 -const depsOptimizerMap = new WeakMap() -const devSsrDepsOptimizerMap = new WeakMap() - -export function getDepsOptimizer( - config: ResolvedConfig, - ssr?: boolean, -): DepsOptimizer | undefined { - return (ssr ? devSsrDepsOptimizerMap : depsOptimizerMap).get(config) -} - -export async function initDepsOptimizer( - config: ResolvedConfig, - server: ViteDevServer, -): Promise { - if (!getDepsOptimizer(config, false)) { - await createDepsOptimizer(config, server) - } -} - -let creatingDevSsrOptimizer: Promise | undefined -export async function initDevSsrDepsOptimizer( - config: ResolvedConfig, - server: ViteDevServer, -): Promise { - if (getDepsOptimizer(config, true)) { - // ssr - return - } - if (creatingDevSsrOptimizer) { - return creatingDevSsrOptimizer - } - creatingDevSsrOptimizer = (async function () { - // Important: scanning needs to be done before starting the SSR dev optimizer - // If ssrLoadModule is called before server.listen(), the main deps optimizer - // will not be yet created - const ssr = false - if (!getDepsOptimizer(config, ssr)) { - await initDepsOptimizer(config, server) - } - await getDepsOptimizer(config, ssr)!.scanProcessing - - await createDevSsrDepsOptimizer(config) - creatingDevSsrOptimizer = undefined - })() - return await creatingDevSsrOptimizer -} - -async function createDepsOptimizer( - config: ResolvedConfig, - server: ViteDevServer, -): Promise { - const { logger } = config - const ssr = false +export function createDepsOptimizer( + environment: DevEnvironment, +): DepsOptimizer { + const { logger } = environment const sessionTimestamp = Date.now().toString() - const cachedMetadata = await loadCachedDepOptimizationMetadata(config, ssr) - let debounceProcessingHandle: NodeJS.Timeout | undefined let closed = false - let metadata = - cachedMetadata || initDepsOptimizerMetadata(config, ssr, sessionTimestamp) - - const options = getDepOptimizationConfig(config, ssr) + const options = environment.options.dev.optimizeDeps const { noDiscovery, holdUntilCrawlEnd } = options + let metadata: DepOptimizationMetadata = initDepsOptimizerMetadata( + environment, + sessionTimestamp, + ) + const depsOptimizer: DepsOptimizer = { + init, metadata, registerMissingImport, run: () => debouncedProcessing(0), - isOptimizedDepFile: createIsOptimizedDepFile(config), - isOptimizedDepUrl: createIsOptimizedDepUrl(config), + isOptimizedDepFile: createIsOptimizedDepFile(environment), + isOptimizedDepUrl: createIsOptimizedDepUrl(environment), getOptimizedDepId: (depInfo: OptimizedDepInfo) => `${depInfo.file}?v=${depInfo.browserHash}`, close, options, } - depsOptimizerMap.set(config, depsOptimizer) - let newDepsDiscovered = false let newDepsToLog: string[] = [] let newDepsToLogHandle: NodeJS.Timeout | undefined const logNewlyDiscoveredDeps = () => { if (newDepsToLog.length) { - config.logger.info( + logger.info( colors.green( `✨ new dependencies optimized: ${depsLogString(newDepsToLog)}`, ), @@ -132,7 +87,7 @@ async function createDepsOptimizer( let discoveredDepsWhileScanning: string[] = [] const logDiscoveredDepsWhileScanning = () => { if (discoveredDepsWhileScanning.length) { - config.logger.info( + logger.info( colors.green( `✨ discovered while scanning: ${depsLogString( discoveredDepsWhileScanning, @@ -159,7 +114,7 @@ async function createDepsOptimizer( let enqueuedRerun: (() => void) | undefined let currentlyProcessing = false - let firstRunCalled = !!cachedMetadata + let firstRunCalled = false let warnAboutMissedDependencies = false // If this is a cold run, we wait for static imports discovered @@ -167,10 +122,6 @@ async function createDepsOptimizer( // On warm start or after the first optimization is run, we use a simpler // debounce strategy each time a new dep is discovered. let waitingForCrawlEnd = false - if (!cachedMetadata) { - server._onCrawlEnd(onCrawlEnd) - waitingForCrawlEnd = true - } let optimizationResult: | { @@ -195,96 +146,113 @@ async function createDepsOptimizer( ]) } - if (!cachedMetadata) { - // Enter processing state until crawl of static imports ends - currentlyProcessing = true + let inited = false + async function init() { + if (inited) return + inited = true - // Initialize discovered deps with manually added optimizeDeps.include info + const cachedMetadata = await loadCachedDepOptimizationMetadata(environment) - const manuallyIncludedDeps: Record = {} - await addManuallyIncludedOptimizeDeps(manuallyIncludedDeps, config, ssr) + firstRunCalled = !!cachedMetadata - const manuallyIncludedDepsInfo = toDiscoveredDependencies( - config, - manuallyIncludedDeps, - ssr, - sessionTimestamp, - ) + metadata = depsOptimizer.metadata = + cachedMetadata || initDepsOptimizerMetadata(environment, sessionTimestamp) - for (const depInfo of Object.values(manuallyIncludedDepsInfo)) { - addOptimizedDepInfo(metadata, 'discovered', { - ...depInfo, - processing: depOptimizationProcessing.promise, - }) - newDepsDiscovered = true - } + if (!cachedMetadata) { + environment._onCrawlEnd(onCrawlEnd) + waitingForCrawlEnd = true - if (noDiscovery) { - // We don't need to scan for dependencies or wait for the static crawl to end - // Run the first optimization run immediately - runOptimizer() - } else { - // Important, the scanner is dev only - depsOptimizer.scanProcessing = new Promise((resolve) => { - // Runs in the background in case blocking high priority tasks - ;(async () => { - try { - debug?.(colors.green(`scanning for dependencies...`)) - - discover = discoverProjectDependencies(config) - const deps = await discover.result - discover = undefined - - const manuallyIncluded = Object.keys(manuallyIncludedDepsInfo) - discoveredDepsWhileScanning.push( - ...Object.keys(metadata.discovered).filter( - (dep) => !deps[dep] && !manuallyIncluded.includes(dep), - ), - ) + // Enter processing state until crawl of static imports ends + currentlyProcessing = true + + // Initialize discovered deps with manually added optimizeDeps.include info + + const manuallyIncludedDeps: Record = {} + await addManuallyIncludedOptimizeDeps(environment, manuallyIncludedDeps) - // Add these dependencies to the discovered list, as these are currently - // used by the preAliasPlugin to support aliased and optimized deps. - // This is also used by the CJS externalization heuristics in legacy mode - for (const id of Object.keys(deps)) { - if (!metadata.discovered[id]) { - addMissingDep(id, deps[id]) + const manuallyIncludedDepsInfo = toDiscoveredDependencies( + environment, + manuallyIncludedDeps, + sessionTimestamp, + ) + + for (const depInfo of Object.values(manuallyIncludedDepsInfo)) { + addOptimizedDepInfo(metadata, 'discovered', { + ...depInfo, + processing: depOptimizationProcessing.promise, + }) + newDepsDiscovered = true + } + + if (noDiscovery) { + // We don't need to scan for dependencies or wait for the static crawl to end + // Run the first optimization run immediately + runOptimizer() + } else { + // Important, the scanner is dev only + depsOptimizer.scanProcessing = new Promise((resolve) => { + // Runs in the background in case blocking high priority tasks + ;(async () => { + try { + debug?.(colors.green(`scanning for dependencies...`)) + + discover = discoverProjectDependencies( + devToScanEnvironment(environment), + ) + const deps = await discover.result + discover = undefined + + const manuallyIncluded = Object.keys(manuallyIncludedDepsInfo) + discoveredDepsWhileScanning.push( + ...Object.keys(metadata.discovered).filter( + (dep) => !deps[dep] && !manuallyIncluded.includes(dep), + ), + ) + + // Add these dependencies to the discovered list, as these are currently + // used by the preAliasPlugin to support aliased and optimized deps. + // This is also used by the CJS externalization heuristics in legacy mode + for (const id of Object.keys(deps)) { + if (!metadata.discovered[id]) { + addMissingDep(id, deps[id]) + } } - } - const knownDeps = prepareKnownDeps() - startNextDiscoveredBatch() - - // For dev, we run the scanner and the first optimization - // run on the background - optimizationResult = runOptimizeDeps(config, knownDeps, ssr) - - // If the holdUntilCrawlEnd stratey is used, we wait until crawling has - // ended to decide if we send this result to the browser or we need to - // do another optimize step - if (!holdUntilCrawlEnd) { - // If not, we release the result to the browser as soon as the scanner - // is done. If the scanner missed any dependency, and a new dependency - // is discovered while crawling static imports, then there will be a - // full-page reload if new common chunks are generated between the old - // and new optimized deps. - optimizationResult.result.then((result) => { - // Check if the crawling of static imports has already finished. In that - // case, the result is handled by the onCrawlEnd callback - if (!waitingForCrawlEnd) return - - optimizationResult = undefined // signal that we'll be using the result - - runOptimizer(result) - }) + const knownDeps = prepareKnownDeps() + startNextDiscoveredBatch() + + // For dev, we run the scanner and the first optimization + // run on the background + optimizationResult = runOptimizeDeps(environment, knownDeps) + + // If the holdUntilCrawlEnd stratey is used, we wait until crawling has + // ended to decide if we send this result to the browser or we need to + // do another optimize step + if (!holdUntilCrawlEnd) { + // If not, we release the result to the browser as soon as the scanner + // is done. If the scanner missed any dependency, and a new dependency + // is discovered while crawling static imports, then there will be a + // full-page reload if new common chunks are generated between the old + // and new optimized deps. + optimizationResult.result.then((result) => { + // Check if the crawling of static imports has already finished. In that + // case, the result is handled by the onCrawlEnd callback + if (!waitingForCrawlEnd) return + + optimizationResult = undefined // signal that we'll be using the result + + runOptimizer(result) + }) + } + } catch (e) { + logger.error(e.stack || e.message) + } finally { + resolve() + depsOptimizer.scanProcessing = undefined } - } catch (e) { - logger.error(e.stack || e.message) - } finally { - resolve() - depsOptimizer.scanProcessing = undefined - } - })() - }) + })() + }) + } } } @@ -303,6 +271,7 @@ async function createDepsOptimizer( function prepareKnownDeps() { const knownDeps: Record = {} // Clone optimized info objects, fileHash, browserHash may be changed for them + const metadata = depsOptimizer.metadata! for (const dep of Object.keys(metadata.optimized)) { knownDeps[dep] = { ...metadata.optimized[dep] } } @@ -351,7 +320,7 @@ async function createDepsOptimizer( const knownDeps = prepareKnownDeps() startNextDiscoveredBatch() - optimizationResult = runOptimizeDeps(config, knownDeps, ssr) + optimizationResult = runOptimizeDeps(environment, knownDeps) processingResult = await optimizationResult.result optimizationResult = undefined } @@ -443,7 +412,7 @@ async function createDepsOptimizer( logNewlyDiscoveredDeps() if (warnAboutMissedDependencies) { logDiscoveredDepsWhileScanning() - config.logger.info( + logger.info( colors.magenta( `❗ add these dependencies to optimizeDeps.include to speed up cold start`, ), @@ -485,7 +454,7 @@ async function createDepsOptimizer( logNewlyDiscoveredDeps() if (warnAboutMissedDependencies) { logDiscoveredDepsWhileScanning() - config.logger.info( + logger.info( colors.magenta( `❗ add these dependencies to optimizeDeps.include to avoid a full page reload during cold start`, ), @@ -502,7 +471,7 @@ async function createDepsOptimizer( }, ) if (needsInteropMismatch.length > 0) { - config.logger.warn( + logger.warn( `Mixed ESM and CJS detected in ${colors.yellow( needsInteropMismatch.join(', '), )}, add ${ @@ -537,9 +506,9 @@ async function createDepsOptimizer( // Cached transform results have stale imports (resolved to // old locations) so they need to be invalidated before the page is // reloaded. - server.moduleGraph.invalidateAll() + environment.moduleGraph.invalidateAll() - server.hot.send({ + environment.hot.send({ type: 'full-reload', path: '*', }) @@ -607,7 +576,7 @@ async function createDepsOptimizer( return addOptimizedDepInfo(metadata, 'discovered', { id, - file: getOptimizedDepPath(id, config, ssr), + file: getOptimizedDepPath(environment, id), src: resolved, // Adding a browserHash to this missing dependency that is unique to // the current state of known + missing deps. If its optimizeDeps run @@ -621,7 +590,7 @@ async function createDepsOptimizer( // loading of this pre-bundled dep needs to await for its processing // promise to be resolved processing: depOptimizationProcessing.promise, - exportsData: extractExportsData(resolved, config, ssr), + exportsData: extractExportsData(environment, resolved), }) } @@ -657,7 +626,7 @@ async function createDepsOptimizer( // It normally should be over by the time crawling of user code ended await depsOptimizer.scanProcessing - if (optimizationResult && !config.optimizeDeps.noDiscovery) { + if (optimizationResult && !options.noDiscovery) { // In the holdUntilCrawlEnd strategy, we don't release the result of the // post-scanner optimize step to the browser until we reach this point // If there are new dependencies, we do another optimize run, if not, we @@ -754,33 +723,43 @@ async function createDepsOptimizer( debouncedProcessing(0) } } -} -async function createDevSsrDepsOptimizer( - config: ResolvedConfig, -): Promise { - const metadata = await optimizeServerSsrDeps(config) + return depsOptimizer +} +export function createExplicitDepsOptimizer( + environment: DevEnvironment, +): DepsOptimizer { const depsOptimizer = { - metadata, - isOptimizedDepFile: createIsOptimizedDepFile(config), - isOptimizedDepUrl: createIsOptimizedDepUrl(config), + metadata: initDepsOptimizerMetadata(environment), + isOptimizedDepFile: createIsOptimizedDepFile(environment), + isOptimizedDepUrl: createIsOptimizedDepUrl(environment), getOptimizedDepId: (depInfo: OptimizedDepInfo) => `${depInfo.file}?v=${depInfo.browserHash}`, registerMissingImport: () => { throw new Error( - 'Vite Internal Error: registerMissingImport is not supported in dev SSR', + `Vite Internal Error: registerMissingImport is not supported in dev ${environment.name}`, ) }, + init, // noop, there is no scanning during dev SSR // the optimizer blocks the server start run: () => {}, close: async () => {}, - options: config.ssr.optimizeDeps, + options: environment.options.dev.optimizeDeps, } - devSsrDepsOptimizerMap.set(config, depsOptimizer) + + let inited = false + async function init() { + if (inited) return + inited = true + + depsOptimizer.metadata = await optimizeExplicitEnvironmentDeps(environment) + } + + return depsOptimizer } function findInteropMismatches( diff --git a/packages/vite/src/node/optimizer/resolve.ts b/packages/vite/src/node/optimizer/resolve.ts index b76634dd8ae8cf..552d22da615e76 100644 --- a/packages/vite/src/node/optimizer/resolve.ts +++ b/packages/vite/src/node/optimizer/resolve.ts @@ -5,22 +5,23 @@ import type { ResolvedConfig } from '../config' import { escapeRegex, getNpmPackageName } from '../utils' import { resolvePackageData } from '../packages' import { slash } from '../../shared/utils' +import type { Environment } from '../environment' +import { createIdResolver } from '../idResolver' export function createOptimizeDepsIncludeResolver( - config: ResolvedConfig, - ssr: boolean, + environment: Environment, ): (id: string) => Promise { - const resolve = config.createResolver({ + const { config } = environment + const resolve = createIdResolver(config, { asSrc: false, scan: true, - ssrOptimizeCheck: ssr, - ssrConfig: config.ssr, + ssrOptimizeCheck: environment.name !== 'client', // TODO:depsOptimizer packageCache: new Map(), }) return async (id: string) => { const lastArrowIndex = id.lastIndexOf('>') if (lastArrowIndex === -1) { - return await resolve(id, undefined, undefined, ssr) + return await resolve(environment, id, undefined) } // split nested selected id by last '>', for example: // 'foo > bar > baz' => 'foo > bar' & 'baz' @@ -32,10 +33,9 @@ export function createOptimizeDepsIncludeResolver( config.resolve.preserveSymlinks, ) return await resolve( + environment, nestedPath, path.resolve(basedir, 'package.json'), - undefined, - ssr, ) } } diff --git a/packages/vite/src/node/optimizer/scan.ts b/packages/vite/src/node/optimizer/scan.ts index 1cdef6c339c103..5eef2dde658b8d 100644 --- a/packages/vite/src/node/optimizer/scan.ts +++ b/packages/vite/src/node/optimizer/scan.ts @@ -11,6 +11,7 @@ import type { Plugin, } from 'esbuild' import esbuild, { formatMessages, transform } from 'esbuild' +import type { PartialResolvedId } from 'rollup' import colors from 'picocolors' import type { ResolvedConfig } from '..' import { @@ -34,13 +35,75 @@ import { virtualModulePrefix, virtualModuleRE, } from '../utils' -import type { PluginContainer } from '../server/pluginContainer' -import { createPluginContainer } from '../server/pluginContainer' +import { resolveBoundedPlugins } from '../plugin' +import type { BoundedPluginContainer } from '../server/pluginContainer' +import { createBoundedPluginContainer } from '../server/pluginContainer' +import { Environment } from '../environment' +import type { DevEnvironment } from '../server/environment' import { transformGlobImport } from '../plugins/importMetaGlob' import { cleanUrl } from '../../shared/utils' import { loadTsconfigJsonForFile } from '../plugins/esbuild' -type ResolveIdOptions = Parameters[2] +export class ScanEnvironment extends Environment { + mode = 'scan' as const + + get pluginContainer(): BoundedPluginContainer { + if (!this._pluginContainer) + throw new Error( + `${this.name} environment.pluginContainer called before initialized`, + ) + return this._pluginContainer + } + /** + * @internal + */ + _pluginContainer: BoundedPluginContainer | undefined + + async init(): Promise { + if (this._inited) { + return + } + this._inited = true + this._plugins = await resolveBoundedPlugins(this) + this._pluginContainer = await createBoundedPluginContainer( + this, + this.plugins, + ) + await this._pluginContainer.buildStart({}) + } +} + +// Restric access to the module graph and the server while scanning +export function devToScanEnvironment( + environment: DevEnvironment, +): ScanEnvironment { + return { + mode: 'scan', + get name() { + return environment.name + }, + get config() { + return environment.config + }, + get options() { + return environment.options + }, + get logger() { + return environment.logger + }, + get pluginContainer() { + return environment.pluginContainer + }, + get plugins() { + return environment.plugins + }, + } as unknown as ScanEnvironment +} + +type ResolveIdOptions = Omit< + Parameters[2], + 'environment' +> const debug = createDebugger('vite:deps') @@ -57,7 +120,7 @@ const htmlTypesRE = /\.(html|vue|svelte|astro|imba)$/ export const importsRE = /(? Promise result: Promise<{ deps: Record @@ -74,13 +137,16 @@ export function scanImports(config: ResolvedConfig): { const scanContext = { cancelled: false } const esbuildContext: Promise = computeEntries( - config, + environment.config, ).then((computedEntries) => { entries = computedEntries if (!entries.length) { - if (!config.optimizeDeps.entries && !config.optimizeDeps.include) { - config.logger.warn( + if ( + !environment.config.optimizeDeps.entries && + !environment.options.dev.optimizeDeps.include + ) { + environment.logger.warn( colors.yellow( '(!) Could not auto-determine entry point from rollupOptions or html files ' + 'and there are no explicit optimizeDeps.include patterns. ' + @@ -97,14 +163,22 @@ export function scanImports(config: ResolvedConfig): { .map((entry) => `\n ${colors.dim(entry)}`) .join('')}`, ) - return prepareEsbuildScanner(config, entries, deps, missing, scanContext) + return prepareEsbuildScanner( + environment, + entries, + deps, + missing, + scanContext, + ) }) const result = esbuildContext .then((context) => { function disposeContext() { return context?.dispose().catch((e) => { - config.logger.error('Failed to dispose esbuild context', { error: e }) + environment.logger.error('Failed to dispose esbuild context', { + error: e, + }) }) } if (!context || scanContext?.cancelled) { @@ -171,6 +245,7 @@ export function scanImports(config: ResolvedConfig): { async function computeEntries(config: ResolvedConfig) { let entries: string[] = [] + // TODO: Should entries be per-environment? const explicitEntryPatterns = config.optimizeDeps.entries const buildInput = config.build.rollupOptions?.input @@ -203,20 +278,18 @@ async function computeEntries(config: ResolvedConfig) { } async function prepareEsbuildScanner( - config: ResolvedConfig, + environment: ScanEnvironment, entries: string[], deps: Record, missing: Record, scanContext?: { cancelled: boolean }, ): Promise { - const container = await createPluginContainer(config) - if (scanContext?.cancelled) return - const plugin = esbuildScanPlugin(config, container, deps, missing, entries) + const plugin = esbuildScanPlugin(environment, deps, missing, entries) const { plugins = [], ...esbuildOptions } = - config.optimizeDeps?.esbuildOptions ?? {} + environment.options.dev.optimizeDeps.esbuildOptions ?? {} // The plugin pipeline automatically loads the closest tsconfig.json. // But esbuild doesn't support reading tsconfig.json if the plugin has resolved the path (https://github.com/evanw/esbuild/issues/2265). @@ -226,7 +299,7 @@ async function prepareEsbuildScanner( let tsconfigRaw = esbuildOptions.tsconfigRaw if (!tsconfigRaw && !esbuildOptions.tsconfig) { const tsconfigResult = await loadTsconfigJsonForFile( - path.join(config.root, '_dummy.js'), + path.join(environment.config.root, '_dummy.js'), ) if (tsconfigResult.compilerOptions?.experimentalDecorators) { tsconfigRaw = { compilerOptions: { experimentalDecorators: true } } @@ -287,24 +360,18 @@ const langRE = /\blang\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s'">]+))/i const contextRE = /\bcontext\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s'">]+))/i function esbuildScanPlugin( - config: ResolvedConfig, - container: PluginContainer, + environment: ScanEnvironment, depImports: Record, missing: Record, entries: string[], ): Plugin { const seen = new Map() - - const resolve = async ( + async function resolveId( id: string, importer?: string, options?: ResolveIdOptions, - ) => { - const key = id + (importer && path.dirname(importer)) - if (seen.has(key)) { - return seen.get(key) - } - const resolved = await container.resolveId( + ): Promise { + return environment.pluginContainer.resolveId( id, importer && normalizePath(importer), { @@ -312,14 +379,26 @@ function esbuildScanPlugin( scan: true, }, ) + } + const resolve = async ( + id: string, + importer?: string, + options?: ResolveIdOptions, + ) => { + const key = id + (importer && path.dirname(importer)) + if (seen.has(key)) { + return seen.get(key) + } + const resolved = await resolveId(id, importer, options) const res = resolved?.id seen.set(key, res) return res } - const include = config.optimizeDeps?.include + const optimizeDepsOptions = environment.options.dev.optimizeDeps + const include = optimizeDepsOptions.include const exclude = [ - ...(config.optimizeDeps?.exclude || []), + ...(optimizeDepsOptions.exclude ?? []), '@vite/client', '@vite/env', ] @@ -347,7 +426,7 @@ function esbuildScanPlugin( const result = await transformGlobImport( transpiledContents, id, - config.root, + environment.config.root, resolve, ) @@ -393,7 +472,7 @@ function esbuildScanPlugin( // bare import resolve, and recorded as optimization dep. if ( isInNodeModules(resolved) && - isOptimizable(resolved, config.optimizeDeps) + isOptimizable(resolved, optimizeDepsOptions) ) return return { @@ -547,11 +626,11 @@ function esbuildScanPlugin( } if (isInNodeModules(resolved) || include?.includes(id)) { // dependency or forced included, externalize and stop crawling - if (isOptimizable(resolved, config.optimizeDeps)) { + if (isOptimizable(resolved, optimizeDepsOptions)) { depImports[id] = resolved } return externalUnlessEntry({ path: id }) - } else if (isScannable(resolved, config.optimizeDeps.extensions)) { + } else if (isScannable(resolved, optimizeDepsOptions.extensions)) { const namespace = htmlTypesRE.test(resolved) ? 'html' : undefined // linked package, keep crawling return { @@ -612,7 +691,7 @@ function esbuildScanPlugin( if (resolved) { if ( shouldExternalizeDep(resolved, id) || - !isScannable(resolved, config.optimizeDeps.extensions) + !isScannable(resolved, optimizeDepsOptions.extensions) ) { return externalUnlessEntry({ path: id }) } @@ -637,13 +716,15 @@ function esbuildScanPlugin( let ext = path.extname(id).slice(1) if (ext === 'mjs') ext = 'js' + // TODO: Why are we using config.esbuild instead of config.optimizeDeps.esbuildOptions here? + const esbuildConfig = environment.config.esbuild let contents = await fsp.readFile(id, 'utf-8') - if (ext.endsWith('x') && config.esbuild && config.esbuild.jsxInject) { - contents = config.esbuild.jsxInject + `\n` + contents + if (ext.endsWith('x') && esbuildConfig && esbuildConfig.jsxInject) { + contents = esbuildConfig.jsxInject + `\n` + contents } const loader = - config.optimizeDeps?.esbuildOptions?.loader?.[`.${ext}`] || + optimizeDepsOptions.esbuildOptions?.loader?.[`.${ext}`] ?? (ext as Loader) if (contents.includes('import.meta.glob')) { diff --git a/packages/vite/src/node/packages.ts b/packages/vite/src/node/packages.ts index 5af667d2417cc9..b03ecf37525db3 100644 --- a/packages/vite/src/node/packages.ts +++ b/packages/vite/src/node/packages.ts @@ -260,7 +260,7 @@ export function watchPackageDataPlugin(packageCache: PackageCache): Plugin { invalidatePackageData(packageCache, path.normalize(id)) } }, - handleHotUpdate({ file }) { + hotUpdate({ file }) { if (file.endsWith('/package.json')) { invalidatePackageData(packageCache, path.normalize(file)) } diff --git a/packages/vite/src/node/plugin.ts b/packages/vite/src/node/plugin.ts index bf6eb069067bdc..4c4bf72bbeb663 100644 --- a/packages/vite/src/node/plugin.ts +++ b/packages/vite/src/node/plugin.ts @@ -2,19 +2,27 @@ import type { CustomPluginOptions, LoadResult, ObjectHook, - PluginContext, ResolveIdResult, Plugin as RollupPlugin, - TransformPluginContext, + PluginContext as RollupPluginContext, + TransformPluginContext as RollupTransformPluginContext, TransformResult, } from 'rollup' -export type { PluginContext } from 'rollup' -import type { ConfigEnv, ResolvedConfig, UserConfig } from './config' -import type { ServerHook } from './server' +import type { + ConfigEnv, + EnvironmentOptions, + ResolvedConfig, + UserConfig, +} from './config' +import type { ServerHook, ViteDevServer } from './server' import type { IndexHtmlTransform } from './plugins/html' -import type { ModuleNode } from './server/moduleGraph' -import type { HmrContext } from './server/hmr' +import type { EnvironmentModuleNode } from './server/moduleGraph' +import type { ModuleNode } from './server/mixedModuleGraph' +import type { HmrContext, HotUpdateContext } from './server/hmr' import type { PreviewServerHook } from './preview' +import type { DevEnvironment } from './server/environment' +import type { BuildEnvironment } from './build' +import type { ScanEnvironment } from './optimizer/scan' /** * Vite plugins extends the Rollup plugin interface with a few extra @@ -36,8 +44,134 @@ import type { PreviewServerHook } from './preview' * * If a plugin should be applied only for server or build, a function format * config file can be used to conditional determine the plugins to use. + * + * The current module environment can be accessed from the context for the + * buildStart, resolveId, transform, load, and buildEnd, hooks + * + * The current environment can be accessed from the context for the + * buildStart, resolveId, transform, load, and buildEnd, hooks. It can be a dev + * or a build environment. Plugins can use this.environment.mode === 'dev' to + * check if they have access to dev specific APIs. */ -export interface Plugin extends RollupPlugin { + +export type PluginEnvironment = + | DevEnvironment + | BuildEnvironment + | ScanEnvironment + +export interface PluginContext extends RollupPluginContext { + environment?: PluginEnvironment +} + +export interface ResolveIdPluginContext extends RollupPluginContext { + environment?: PluginEnvironment +} + +export interface TransformPluginContext extends RollupTransformPluginContext { + environment?: PluginEnvironment +} + +/** + * There are two types of plugins in Vite. App plugins and environment plugins. + * Environment Plugins are defined by a constructor function that will be called + * once per each environment allowing users to have completely different plugins + * for each of them. The constructor gets the resolved environment after the server + * and builder has already been created simplifying config access and cache + * managementfor for environment specific plugins. + * Environment Plugins are closer to regular rollup plugins. They can't define + * app level hooks (like config, configResolved, configureServer, etc). + */ + +export interface BasePlugin extends RollupPlugin { + /** + * Perform custom handling of HMR updates. + * The handler receives a context containing changed filename, timestamp, a + * list of modules affected by the file change, and the dev server instance. + * + * - The hook can return a filtered list of modules to narrow down the update. + * e.g. for a Vue SFC, we can narrow down the part to update by comparing + * the descriptors. + * + * - The hook can also return an empty array and then perform custom updates + * by sending a custom hmr payload via server.hot.send(). + * + * - If the hook doesn't return a value, the hmr update will be performed as + * normal. + */ + hotUpdate?: ObjectHook< + ( + this: void, + ctx: HotUpdateContext, + ) => + | Array + | void + | Promise | void> + > + + /** + * extend hooks with ssr flag + */ + resolveId?: ObjectHook< + ( + this: ResolveIdPluginContext, + source: string, + importer: string | undefined, + options: { + attributes: Record + custom?: CustomPluginOptions + /** + * @deprecated use this.environment + */ + ssr?: boolean + /** + * @internal + */ + scan?: boolean + isEntry: boolean + }, + ) => Promise | ResolveIdResult + > + load?: ObjectHook< + ( + this: PluginContext, + id: string, + options?: { + /** + * @deprecated use this.environment + */ + ssr?: boolean + /** + * @internal + */ + html?: boolean + }, + ) => Promise | LoadResult + > + transform?: ObjectHook< + ( + this: TransformPluginContext, + code: string, + id: string, + options?: { + /** + * @deprecated use this.environment + */ + ssr?: boolean + }, + ) => Promise | TransformResult + > +} + +export type BoundedPlugin = BasePlugin + +export interface Plugin extends BasePlugin { + /** + * Split the plugin into multiple plugins based on the environment. + * This hook is called when the config has already been resolved, allowing to + * create per environment plugin pipelines or easily inject plugins for a + * only specific environments. + */ + split?: (environment: PluginEnvironment) => BoundedPluginOption /** * Enforce plugin invocation tier similar to webpack loaders. Hooks ordering * is still subject to the `order` property in the hook object. @@ -78,6 +212,28 @@ export interface Plugin extends RollupPlugin { | void | Promise | null | void> > + /** + * Modify environment configs before it's resolved. The hook can either mutate the + * passed-in environment config directly, or return a partial config object that will be + * deeply merged into existing config. + * This hook is called for each environment with a partially resolved environment config + * that already accounts for the default environment config values set at the root level. + * If plugins need to modify the config of a given environment, they should do it in this + * hook instead of the config hook. Leaving the config hook only for modifying the root + * default environment config. + */ + configEnvironment?: ObjectHook< + ( + this: void, + name: string, + config: EnvironmentOptions, + env: ConfigEnv, + ) => + | EnvironmentOptions + | null + | void + | Promise + > /** * Use this hook to read and store the final resolved vite config. */ @@ -120,20 +276,11 @@ export interface Plugin extends RollupPlugin { * `{ order: 'pre', handler: hook }` */ transformIndexHtml?: IndexHtmlTransform + /** - * Perform custom handling of HMR updates. - * The handler receives a context containing changed filename, timestamp, a - * list of modules affected by the file change, and the dev server instance. - * - * - The hook can return a filtered list of modules to narrow down the update. - * e.g. for a Vue SFC, we can narrow down the part to update by comparing - * the descriptors. - * - * - The hook can also return an empty array and then perform custom updates - * by sending a custom hmr payload via server.hot.send(). - * - * - If the hook doesn't return a value, the hmr update will be performed as - * normal. + * @deprecated + * Compat support, ctx.modules is a backward compatible ModuleNode array + * with the mixed client and ssr moduleGraph. Use hotUpdate instead */ handleHotUpdate?: ObjectHook< ( @@ -141,42 +288,6 @@ export interface Plugin extends RollupPlugin { ctx: HmrContext, ) => Array | void | Promise | void> > - - /** - * extend hooks with ssr flag - */ - resolveId?: ObjectHook< - ( - this: PluginContext, - source: string, - importer: string | undefined, - options: { - attributes: Record - custom?: CustomPluginOptions - ssr?: boolean - /** - * @internal - */ - scan?: boolean - isEntry: boolean - }, - ) => Promise | ResolveIdResult - > - load?: ObjectHook< - ( - this: PluginContext, - id: string, - options?: { ssr?: boolean }, - ) => Promise | LoadResult - > - transform?: ObjectHook< - ( - this: TransformPluginContext, - code: string, - id: string, - options?: { ssr?: boolean }, - ) => Promise | TransformResult - > } export type HookHandler = T extends ObjectHook ? H : T @@ -184,3 +295,61 @@ export type HookHandler = T extends ObjectHook ? H : T export type PluginWithRequiredHook = Plugin & { [P in K]: NonNullable } + +export type BoundedPluginConstructor = ( + Environment: PluginEnvironment, +) => BoundedPluginOption + +export type MaybeBoundedPlugin = BoundedPlugin | false | null | undefined + +export type BoundedPluginOption = + | MaybeBoundedPlugin + | BoundedPluginOption[] + | Promise + +export type MaybePlugin = Plugin | false | null | undefined + +export type PluginOption = + | MaybePlugin + | PluginOption[] + | Promise + +export async function resolveBoundedPlugins( + environment: PluginEnvironment, +): Promise { + const resolvedPlugins: BoundedPlugin[] = [] + for (const plugin of environment.config.plugins) { + if (plugin.split) { + const boundedPlugin = await plugin.split(environment) + if (boundedPlugin) { + const flatPlugins = await asyncFlattenBoundedPlugin( + environment, + boundedPlugin, + ) + resolvedPlugins.push(...flatPlugins) + } + } else { + resolvedPlugins.push(plugin) + } + } + return resolvedPlugins +} + +async function asyncFlattenBoundedPlugin( + environment: PluginEnvironment, + plugins: BoundedPluginOption, +): Promise { + if (!Array.isArray(plugins)) { + plugins = [plugins] + } + do { + plugins = ( + await Promise.all( + plugins.map((p: any) => (p && p.split ? p.split(environment) : p)), + ) + ) + .flat(Infinity) + .filter(Boolean) as BoundedPluginOption[] + } while (plugins.some((v: any) => v?.then || v?.split)) + return plugins as BoundedPlugin[] +} diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index 2ff5b101529982..1a15022e03d423 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -29,7 +29,6 @@ import { urlRE, } from '../utils' import { DEFAULT_ASSETS_INLINE_LIMIT, FS_PREFIX } from '../constants' -import type { ModuleGraph } from '../server/moduleGraph' import { cleanUrl, withTrailingSlash } from '../../shared/utils' // referenceId is base64url but replaces - with $ @@ -142,8 +141,6 @@ const viteBuildPublicIdPrefix = '\0vite:asset:public' export function assetPlugin(config: ResolvedConfig): Plugin { registerCustomMime() - let moduleGraph: ModuleGraph | undefined - return { name: 'vite:asset', @@ -152,10 +149,6 @@ export function assetPlugin(config: ResolvedConfig): Plugin { generatedAssets.set(config, new Map()) }, - configureServer(server) { - moduleGraph = server.moduleGraph - }, - resolveId(id) { if (!config.assetsInclude(cleanUrl(id)) && !urlRE.test(id)) { return @@ -170,7 +163,7 @@ export function assetPlugin(config: ResolvedConfig): Plugin { } }, - async load(id) { + async load(id, options) { if (id.startsWith(viteBuildPublicIdPrefix)) { id = id.slice(viteBuildPublicIdPrefix.length) } @@ -199,11 +192,12 @@ export function assetPlugin(config: ResolvedConfig): Plugin { let url = await fileToUrl(id, config, this) // Inherit HMR timestamp if this asset was invalidated - if (moduleGraph) { - const mod = moduleGraph.getModuleById(id) - if (mod && mod.lastHMRTimestamp > 0) { - url = injectQuery(url, `t=${mod.lastHMRTimestamp}`) - } + const environment = this.environment + const mod = + environment?.mode === 'dev' && + environment?.moduleGraph.getModuleById(id) + if (mod && mod.lastHMRTimestamp > 0) { + url = injectQuery(url, `t=${mod.lastHMRTimestamp}`) } return { @@ -250,7 +244,7 @@ export function assetPlugin(config: ResolvedConfig): Plugin { if ( config.command === 'build' && config.build.ssr && - !config.build.ssrEmitAssets + !config.build.emitAssets ) { for (const file in bundle) { if ( diff --git a/packages/vite/src/node/plugins/assetImportMetaUrl.ts b/packages/vite/src/node/plugins/assetImportMetaUrl.ts index fb0b45e9d2a937..fd5b4a8e379e8e 100644 --- a/packages/vite/src/node/plugins/assetImportMetaUrl.ts +++ b/packages/vite/src/node/plugins/assetImportMetaUrl.ts @@ -3,10 +3,11 @@ import MagicString from 'magic-string' import { stripLiteral } from 'strip-literal' import type { Plugin } from '../plugin' import type { ResolvedConfig } from '../config' -import type { ResolveFn } from '../' import { injectQuery, isParentDirectory, transformStableResult } from '../utils' import { CLIENT_ENTRY } from '../constants' import { slash } from '../../shared/utils' +import { createIdResolver } from '../idResolver' +import type { ResolveIdFn } from '../idResolver' import { fileToUrl } from './asset' import { preloadHelperId } from './importAnalysisBuild' import type { InternalResolveOptions } from './resolve' @@ -24,7 +25,7 @@ import { tryFsResolve } from './resolve' */ export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin { const { publicDir } = config - let assetResolver: ResolveFn + let assetResolver: ResolveIdFn const fsResolveOptions: InternalResolveOptions = { ...config.resolve, @@ -32,15 +33,17 @@ export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin { isProduction: config.isProduction, isBuild: config.command === 'build', packageCache: config.packageCache, - ssrConfig: config.ssr, asSrc: true, } return { name: 'vite:asset-import-meta-url', async transform(code, id, options) { + const { environment } = this if ( - !options?.ssr && + environment && + // TODO: Should this be done only for the client or for any webCompatible environment? + environment.name === 'client' && id !== preloadHelperId && id !== CLIENT_ENTRY && code.includes('new URL') && @@ -103,13 +106,13 @@ export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin { file = slash(path.resolve(path.dirname(id), url)) file = tryFsResolve(file, fsResolveOptions) ?? file } else { - assetResolver ??= config.createResolver({ + assetResolver ??= createIdResolver(config, { extensions: [], mainFields: [], tryIndex: false, preferRelative: true, }) - file = await assetResolver(url, id) + file = await assetResolver(environment, url, id) file ??= url[0] === '/' ? slash(path.join(publicDir, url)) diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 0e97c247cf01f8..8304306ad2d896 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -26,8 +26,7 @@ import { formatMessages, transform } from 'esbuild' import type { RawSourceMap } from '@ampproject/remapping' import { WorkerWithFallback } from 'artichokie' import { getCodeWithSourcemap, injectSourcesContent } from '../server/sourcemap' -import type { ModuleNode } from '../server/moduleGraph' -import type { ResolveFn, ViteDevServer } from '../' +import type { EnvironmentModuleNode } from '../server/moduleGraph' import { createToImportMetaURLBasedRelativeRuntime, resolveUserExternal, @@ -42,6 +41,7 @@ import { } from '../constants' import type { ResolvedConfig } from '../config' import type { Plugin } from '../plugin' +import { Environment } from '../environment' import { checkPublicFile } from '../publicDir' import { arraify, @@ -69,6 +69,8 @@ import { } from '../utils' import type { Logger } from '../logger' import { cleanUrl, slash } from '../../shared/utils' +import { createIdResolver } from '../idResolver' +import type { ResolveIdFn } from '../idResolver' import { addToHTMLProxyTransformResult } from './html' import { assetUrlRE, @@ -122,6 +124,7 @@ export interface CSSOptions { * Enables css sourcemaps during dev * @default false * @experimental + * @deprecated use dev.sourcemap instead */ devSourcemap?: boolean @@ -254,7 +257,7 @@ export function cssPlugin(config: ResolvedConfig): Plugin { const isBuild = config.command === 'build' let moduleCache: Map> - const resolveUrl = config.createResolver({ + const idResolver = createIdResolver(config, { preferRelative: true, tryIndex: false, extensions: [], @@ -317,13 +320,18 @@ export function cssPlugin(config: ResolvedConfig): Plugin { }, async transform(raw, id) { + const { environment } = this if ( + !environment || !isCSSRequest(id) || commonjsProxyRE.test(id) || SPECIAL_QUERY_RE.test(id) ) { return } + const resolveUrl = (url: string, importer?: string) => + idResolver(environment, url, importer) + const urlReplacer: CssUrlReplacer = async (url, importer) => { const decodedUrl = decodeURI(url) if (checkPublicFile(decodedUrl, config)) { @@ -363,9 +371,9 @@ export function cssPlugin(config: ResolvedConfig): Plugin { deps, map, } = await compileCSS( + environment, id, raw, - config, preprocessorWorkerController!, urlReplacer, ) @@ -494,7 +502,7 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { if (isDirectCSSRequest(id)) { return null } - // server only + // server only, TODO: environment if (options?.ssr) { return modulesCode || `export default ${JSON.stringify(css)}` } @@ -932,15 +940,9 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { } export function cssAnalysisPlugin(config: ResolvedConfig): Plugin { - let server: ViteDevServer - return { name: 'vite:css-analysis', - configureServer(_server) { - server = _server - }, - async transform(_, id, options) { if ( !isCSSRequest(id) || @@ -950,9 +952,10 @@ export function cssAnalysisPlugin(config: ResolvedConfig): Plugin { return } - const ssr = options?.ssr === true - const { moduleGraph } = server - const thisModule = moduleGraph.getModuleById(id) + const environment = this.environment + const moduleGraph = + environment?.mode === 'dev' ? environment.moduleGraph : undefined + const thisModule = moduleGraph?.getModuleById(id) // Handle CSS @import dependency HMR and other added modules via this.addWatchFile. // JS-related HMR is handled in the import-analysis plugin. @@ -969,22 +972,21 @@ export function cssAnalysisPlugin(config: ResolvedConfig): Plugin { if (pluginImports) { // record deps in the module graph so edits to @import css can trigger // main import to hot update - const depModules = new Set() + const depModules = new Set() const devBase = config.base for (const file of pluginImports) { depModules.add( isCSSRequest(file) - ? moduleGraph.createFileOnlyEntry(file) - : await moduleGraph.ensureEntryFromUrl( + ? moduleGraph!.createFileOnlyEntry(file) + : await moduleGraph!.ensureEntryFromUrl( stripBase( await fileToUrl(file, config, this), (config.server?.origin ?? '') + devBase, ), - ssr, ), ) } - moduleGraph.updateModuleInfo( + moduleGraph!.updateModuleInfo( thisModule, depModules, null, @@ -993,7 +995,6 @@ export function cssAnalysisPlugin(config: ResolvedConfig): Plugin { new Set(), null, isSelfAccepting, - ssr, ) } else { thisModule.isSelfAccepting = isSelfAccepting @@ -1039,54 +1040,45 @@ export function getEmptyChunkReplacer( } interface CSSAtImportResolvers { - css: ResolveFn - sass: ResolveFn - less: ResolveFn + css: ResolveIdFn + sass: ResolveIdFn + less: ResolveIdFn } function createCSSResolvers(config: ResolvedConfig): CSSAtImportResolvers { - let cssResolve: ResolveFn | undefined - let sassResolve: ResolveFn | undefined - let lessResolve: ResolveFn | undefined + let cssResolve: ResolveIdFn | undefined + let sassResolve: ResolveIdFn | undefined + let lessResolve: ResolveIdFn | undefined return { get css() { - return ( - cssResolve || - (cssResolve = config.createResolver({ - extensions: ['.css'], - mainFields: ['style'], - conditions: ['style'], - tryIndex: false, - preferRelative: true, - })) - ) + return (cssResolve ??= createIdResolver(config, { + extensions: ['.css'], + mainFields: ['style'], + conditions: ['style'], + tryIndex: false, + preferRelative: true, + })) }, get sass() { - return ( - sassResolve || - (sassResolve = config.createResolver({ - extensions: ['.scss', '.sass', '.css'], - mainFields: ['sass', 'style'], - conditions: ['sass', 'style'], - tryIndex: true, - tryPrefix: '_', - preferRelative: true, - })) - ) + return (sassResolve ??= createIdResolver(config, { + extensions: ['.scss', '.sass', '.css'], + mainFields: ['sass', 'style'], + conditions: ['sass', 'style'], + tryIndex: true, + tryPrefix: '_', + preferRelative: true, + })) }, get less() { - return ( - lessResolve || - (lessResolve = config.createResolver({ - extensions: ['.less', '.css'], - mainFields: ['less', 'style'], - conditions: ['less', 'style'], - tryIndex: false, - preferRelative: true, - })) - ) + return (lessResolve ??= createIdResolver(config, { + extensions: ['.less', '.css'], + mainFields: ['less', 'style'], + conditions: ['less', 'style'], + tryIndex: false, + preferRelative: true, + })) }, } } @@ -1098,12 +1090,13 @@ function getCssResolversKeys( } async function compileCSSPreprocessors( + environment: Environment, id: string, lang: PreprocessLang, code: string, - config: ResolvedConfig, workerController: PreprocessorWorkerController, ): Promise<{ code: string; map?: ExistingRawSourceMap; deps?: Set }> { + const { config } = environment const { preprocessorOptions, devSourcemap } = config.css ?? {} const atImportResolvers = getAtImportResolvers(config) @@ -1133,6 +1126,7 @@ async function compileCSSPreprocessors( opts.enableSourcemap = devSourcemap ?? false const preprocessResult = await preProcessor( + environment, code, config.root, opts, @@ -1178,9 +1172,9 @@ function getAtImportResolvers(config: ResolvedConfig) { } async function compileCSS( + environment: Environment, id: string, code: string, - config: ResolvedConfig, workerController: PreprocessorWorkerController, urlReplacer?: CssUrlReplacer, ): Promise<{ @@ -1190,8 +1184,9 @@ async function compileCSS( modules?: Record deps?: Set }> { + const { config } = environment if (config.css?.transformer === 'lightningcss') { - return compileLightningCSS(id, code, config, urlReplacer) + return compileLightningCSS(id, code, environment, urlReplacer) } const { modules: modulesOptions, devSourcemap } = config.css || {} @@ -1221,10 +1216,10 @@ async function compileCSS( let preprocessorMap: ExistingRawSourceMap | undefined if (isPreProcessor(lang)) { const preprocessorResult = await compileCSSPreprocessors( + environment, id, lang, code, - config, workerController, ) code = preprocessorResult.code @@ -1249,6 +1244,7 @@ async function compileCSS( } const resolved = await atImportResolvers.css( + environment, id, path.join(basedir, '*'), ) @@ -1275,10 +1271,10 @@ async function compileCSS( const lang = id.match(CSS_LANGS_RE)?.[1] as CssLang | undefined if (isPreProcessor(lang)) { const result = await compileCSSPreprocessors( + environment, id, lang, code, - config, workerController, ) result.deps?.forEach((dep) => deps.add(dep)) @@ -1320,7 +1316,11 @@ async function compileCSS( }, async resolve(id: string, importer: string) { for (const key of getCssResolversKeys(atImportResolvers)) { - const resolved = await atImportResolvers[key](id, importer) + const resolved = await atImportResolvers[key]( + environment, + id, + importer, + ) if (resolved) { return path.resolve(resolved) } @@ -1478,6 +1478,10 @@ export async function preprocessCSS( code: string, filename: string, config: ResolvedConfig, + // Backward compatibility, only the name is needed for the alias and resolve plugins used in the resolvers + // TODO: Should we use environmentName instead of environment for these APIs? + // Should the signature be preprocessCSS(code, filename, environment) or preprocessCSS(code, filename, config, environmentName)? + environment: Environment = new Environment('client', config), ): Promise { let workerController = preprocessorWorkerControllerCache.get(config) @@ -1489,7 +1493,7 @@ export async function preprocessCSS( workerController = alwaysFakeWorkerWorkerControllerCache } - return await compileCSS(filename, code, config, workerController) + return await compileCSS(environment, filename, code, workerController) } export async function formatPostcssSourceMap( @@ -1926,6 +1930,7 @@ type StylusStylePreprocessorOptions = StylePreprocessorOptions & { type StylePreprocessor = { process: ( + environment: Environment, source: string, root: string, options: StylePreprocessorOptions, @@ -1936,6 +1941,7 @@ type StylePreprocessor = { type SassStylePreprocessor = { process: ( + environment: Environment, source: string, root: string, options: SassStylePreprocessorOptions, @@ -1946,6 +1952,7 @@ type SassStylePreprocessor = { type StylusStylePreprocessor = { process: ( + environment: Environment, source: string, root: string, options: StylusStylePreprocessorOptions, @@ -2042,6 +2049,7 @@ function fixScssBugImportValue( // .scss/.sass processor const makeScssWorker = ( + environment: Environment, resolvers: CSSAtImportResolvers, alias: Alias[], maxWorkers: number | undefined, @@ -2052,10 +2060,11 @@ const makeScssWorker = ( filename: string, ) => { importer = cleanScssBugUrl(importer) - const resolved = await resolvers.sass(url, importer) + const resolved = await resolvers.sass(environment, url, importer) if (resolved) { try { const data = await rebaseUrls( + environment, resolved, filename, alias, @@ -2162,13 +2171,13 @@ const scssProcessor = ( worker.stop() } }, - async process(source, root, options, resolvers) { + async process(environment, source, root, options, resolvers) { const sassPath = loadPreprocessorPath(PreprocessLang.sass, root) if (!workerMap.has(options.alias)) { workerMap.set( options.alias, - makeScssWorker(resolvers, options.alias, maxWorkers), + makeScssWorker(environment, resolvers, options.alias, maxWorkers), ) } const worker = workerMap.get(options.alias)! @@ -2217,11 +2226,12 @@ const scssProcessor = ( * root file as base. */ async function rebaseUrls( + environment: Environment, file: string, rootFile: string, alias: Alias[], variablePrefix: string, - resolver: ResolveFn, + resolver: ResolveIdFn, ): Promise { file = path.resolve(file) // ensure os-specific flashes // in the same dir, no need to rebase @@ -2256,7 +2266,8 @@ async function rebaseUrls( return url } } - const absolute = (await resolver(url, file)) || path.resolve(fileDir, url) + const absolute = + (await resolver(environment, url, file)) || path.resolve(fileDir, url) const relative = path.relative(rootDir, absolute) return normalizePath(relative) } @@ -2282,6 +2293,7 @@ async function rebaseUrls( // .less const makeLessWorker = ( + environment: Environment, resolvers: CSSAtImportResolvers, alias: Alias[], maxWorkers: number | undefined, @@ -2291,10 +2303,15 @@ const makeLessWorker = ( dir: string, rootFile: string, ) => { - const resolved = await resolvers.less(filename, path.join(dir, '*')) + const resolved = await resolvers.less( + environment, + filename, + path.join(dir, '*'), + ) if (!resolved) return undefined const result = await rebaseUrls( + environment, resolved, rootFile, alias, @@ -2412,13 +2429,13 @@ const lessProcessor = (maxWorkers: number | undefined): StylePreprocessor => { worker.stop() } }, - async process(source, root, options, resolvers) { + async process(environment, source, root, options, resolvers) { const lessPath = loadPreprocessorPath(PreprocessLang.less, root) if (!workerMap.has(options.alias)) { workerMap.set( options.alias, - makeLessWorker(resolvers, options.alias, maxWorkers), + makeLessWorker(environment, resolvers, options.alias, maxWorkers), ) } const worker = workerMap.get(options.alias)! @@ -2532,7 +2549,7 @@ const stylProcessor = ( worker.stop() } }, - async process(source, root, options, resolvers) { + async process(environment, source, root, options, resolvers) { const stylusPath = loadPreprocessorPath(PreprocessLang.stylus, root) if (!workerMap.has(options.alias)) { @@ -2640,12 +2657,14 @@ const createPreprocessorWorkerController = (maxWorkers: number | undefined) => { const styl = stylProcessor(maxWorkers) const sassProcess: StylePreprocessor['process'] = ( + environment, source, root, options, resolvers, ) => { return scss.process( + environment, source, root, { ...options, indentedSyntax: true }, @@ -2696,9 +2715,10 @@ const importLightningCSS = createCachedImport(() => import('lightningcss')) async function compileLightningCSS( id: string, src: string, - config: ResolvedConfig, + environment: Environment, urlReplacer?: CssUrlReplacer, ): ReturnType { + const { config } = environment const deps = new Set() // Relative path is needed to get stable hash when using CSS modules const filename = cleanUrl(path.relative(config.root, id)) @@ -2736,6 +2756,7 @@ async function compileLightningCSS( } const resolved = await getAtImportResolvers(config).css( + environment, id, toAbsolute(from), ) diff --git a/packages/vite/src/node/plugins/define.ts b/packages/vite/src/node/plugins/define.ts index 786a1038505c00..607eb8d4a43a55 100644 --- a/packages/vite/src/node/plugins/define.ts +++ b/packages/vite/src/node/plugins/define.ts @@ -3,6 +3,7 @@ import { TraceMap, decodedMap, encodedMap } from '@jridgewell/trace-mapping' import type { ResolvedConfig } from '../config' import type { Plugin } from '../plugin' import { escapeRegex, getHash } from '../utils' +import type { Environment } from '../environment' import { isCSSRequest } from './css' import { isHTMLRequest } from './html' @@ -54,8 +55,13 @@ export function definePlugin(config: ResolvedConfig): Plugin { } } - function generatePattern(ssr: boolean) { - const replaceProcessEnv = !ssr || config.ssr?.target === 'webworker' + function generatePattern(environment: Environment) { + // This is equivalent to the old `!ssr || config.ssr?.target === 'webworker'` + // TODO: We shouldn't keep options.nodeCompatible and options.webCompatible + // This is a place where using `!options.nodeCompatible` fails and it is confusing why + // Do we need a per-environment replaceProcessEnv option? + // Is it useful to have define be configured per-environment? + const replaceProcessEnv = environment.options.webCompatible const define: Record = { ...(replaceProcessEnv ? processEnv : {}), @@ -65,6 +71,11 @@ export function definePlugin(config: ResolvedConfig): Plugin { } // Additional define fixes based on `ssr` value + // Backward compatibility. Any non client environment will get import.meta.env.SSR = true + // TODO: Check if we should only do this for the SSR environment and how to abstract + // maybe we need import.meta.env.environmentName ? + const ssr = environment.name !== 'client' + if ('import.meta.env.SSR' in define) { define['import.meta.env.SSR'] = ssr + '' } @@ -91,15 +102,29 @@ export function definePlugin(config: ResolvedConfig): Plugin { return [define, pattern] as const } - const defaultPattern = generatePattern(false) - const ssrPattern = generatePattern(true) + const patternsCache = new WeakMap< + Environment, + readonly [Record, RegExp | null] + >() + function getPattern(environment: Environment) { + let pattern = patternsCache.get(environment) + if (!pattern) { + pattern = generatePattern(environment) + patternsCache.set(environment, pattern) + } + return pattern + } return { name: 'vite:define', - async transform(code, id, options) { - const ssr = options?.ssr === true - if (!ssr && !isBuild) { + async transform(code, id) { + const { environment } = this + if (!environment) { + return + } + + if (environment.name === 'client' && !isBuild) { // for dev we inject actual global defines in the vite client to // avoid the transform cost. see the `clientInjection` and // `importAnalysis` plugin. @@ -116,7 +141,7 @@ export function definePlugin(config: ResolvedConfig): Plugin { return } - const [define, pattern] = ssr ? ssrPattern : defaultPattern + const [define, pattern] = getPattern(environment) if (!pattern) return // Check if our code needs any replacements before running esbuild diff --git a/packages/vite/src/node/plugins/dynamicImportVars.ts b/packages/vite/src/node/plugins/dynamicImportVars.ts index a92992800a7473..aed55cb0bc0488 100644 --- a/packages/vite/src/node/plugins/dynamicImportVars.ts +++ b/packages/vite/src/node/plugins/dynamicImportVars.ts @@ -7,6 +7,7 @@ import { dynamicImportToGlob } from '@rollup/plugin-dynamic-import-vars' import type { Plugin } from '../plugin' import type { ResolvedConfig } from '../config' import { CLIENT_ENTRY } from '../constants' +import { createIdResolver } from '../idResolver' import { createFilter, normalizePath, @@ -152,7 +153,7 @@ export async function transformDynamicImport( } export function dynamicImportVarsPlugin(config: ResolvedConfig): Plugin { - const resolve = config.createResolver({ + const resolve = createIdResolver(config, { preferRelative: true, tryIndex: false, extensions: [], @@ -177,7 +178,9 @@ export function dynamicImportVarsPlugin(config: ResolvedConfig): Plugin { }, async transform(source, importer) { + const { environment } = this if ( + !environment || !filter(importer) || importer === CLIENT_ENTRY || !hasDynamicImportRE.test(source) @@ -225,7 +228,7 @@ export function dynamicImportVarsPlugin(config: ResolvedConfig): Plugin { result = await transformDynamicImport( source.slice(start, end), importer, - resolve, + (id, importer) => resolve(environment, id, importer), config.root, ) } catch (error) { diff --git a/packages/vite/src/node/plugins/esbuild.ts b/packages/vite/src/node/plugins/esbuild.ts index 54f1796afd6381..0107a7a30d5b50 100644 --- a/packages/vite/src/node/plugins/esbuild.ts +++ b/packages/vite/src/node/plugins/esbuild.ts @@ -251,6 +251,8 @@ export function esbuildPlugin(config: ResolvedConfig): Plugin { return { name: 'vite:esbuild', + // TODO: Decouple server, the resolved config should be enough + // We may need a `configureWatcher` hook configureServer(_server) { server = _server server.watcher @@ -491,7 +493,9 @@ async function reloadOnTsconfigChange(changedFile: string) { ) // clear module graph to remove code compiled with outdated config - server.moduleGraph.invalidateAll() + for (const environment of Object.values(server.environments)) { + environment.moduleGraph.invalidateAll() + } // reset tsconfck so that recompile works with up2date configs tsconfckCache?.clear() diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index a4c1b9fd38a033..b6e71d2c235510 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -12,7 +12,6 @@ import { parse as parseJS } from 'acorn' import type { Node } from 'estree' import { findStaticImports, parseStaticImport } from 'mlly' import { makeLegalIdentifier } from '@rollup/pluginutils' -import type { ViteDevServer } from '..' import { CLIENT_DIR, CLIENT_PUBLIC_PATH, @@ -54,8 +53,10 @@ import { checkPublicFile } from '../publicDir' import { getDepOptimizationConfig } from '../config' import type { ResolvedConfig } from '../config' import type { Plugin } from '../plugin' -import { shouldExternalizeForSSR } from '../ssr/ssrExternal' -import { getDepsOptimizer, optimizedDepNeedsInterop } from '../optimizer' +import type { DevEnvironment } from '../server/environment' +import { addSafeModulePath } from '../server/middlewares/static' +import { shouldExternalize } from '../external' +import { optimizedDepNeedsInterop } from '../optimizer' import { cleanUrl, unwrapId, @@ -139,7 +140,7 @@ function extractImportedBindings( } /** - * Server-only plugin that lexes, resolves, rewrites and analyzes url imports. + * Dev-only plugin that lexes, resolves, rewrites and analyzes url imports. * * - Imports are resolved to ensure they exist on disk * @@ -173,7 +174,6 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { const clientPublicPath = path.posix.join(base, CLIENT_PUBLIC_PATH) const enablePartialAccept = config.experimental?.hmrPartialAccept const matchAlias = getAliasPatternMatcher(config.resolve.alias) - let server: ViteDevServer let _env: string | undefined let _ssrEnv: string | undefined @@ -204,18 +204,15 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { return { name: 'vite:import-analysis', - configureServer(_server) { - server = _server - }, - async transform(source, importer, options) { - // In a real app `server` is always defined, but it is undefined when - // running src/node/server/__tests__/pluginContainer.spec.ts - if (!server) { - return null + const ssr = options?.ssr === true + + const environment = this.environment as DevEnvironment | undefined + if (!environment) { + return } - const ssr = options?.ssr === true + const moduleGraph = environment.moduleGraph if (canSkipImportAnalysis(importer)) { debug?.(colors.dim(`[skipped] ${prettifyUrl(importer, root)}`)) @@ -238,12 +235,11 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { this.error(message, showCodeFrame ? e.idx : undefined) } - const depsOptimizer = getDepsOptimizer(config, ssr) + const depsOptimizer = environment.depsOptimizer - const { moduleGraph } = server // since we are already in the transform phase of the importer, it must // have been loaded so its entry is guaranteed in the module graph. - const importerModule = moduleGraph.getModuleById(importer)! + const importerModule = moduleGraph.getModuleById(importer) if (!importerModule) { // This request is no longer valid. It could happen for optimized deps // requests. A full reload is going to request this id again. @@ -355,8 +351,8 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { url = wrapId(resolved.id) } - // make the URL browser-valid if not SSR - if (!ssr) { + // make the URL browser-valid + if (environment.options.injectInvalidationTimestamp) { // mark non-js/css imports with `?import` if (isExplicitImportRequired(url)) { url = injectQuery(url, 'import') @@ -383,7 +379,6 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { // We use an internal function to avoid resolving the url again const depModule = await moduleGraph._ensureEntryFromUrl( unwrapId(url), - ssr, canSkipImportAnalysis(url) || forceSkipImportAnalysis, resolved, ) @@ -490,7 +485,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { } // skip ssr external if (ssr && !matchAlias(specifier)) { - if (shouldExternalizeForSSR(specifier, importer, config)) { + if (shouldExternalize(environment, specifier, importer)) { return } if (isBuiltin(specifier)) { @@ -528,9 +523,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { // record as safe modules // safeModulesPath should not include the base prefix. // See https://github.com/vitejs/vite/issues/9438#issuecomment-1465270409 - server?.moduleGraph.safeModulesPath.add( - fsPathFromUrl(stripBase(url, base)), - ) + addSafeModulePath(config, fsPathFromUrl(stripBase(url, base))) if (url !== specifier) { let rewriteDone = false @@ -546,10 +539,9 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { const file = cleanUrl(resolvedId) // Remove ?v={hash} const needsInterop = await optimizedDepNeedsInterop( + environment, depsOptimizer.metadata, file, - config, - ssr, ) if (needsInterop === undefined) { @@ -623,13 +615,13 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { if ( !isDynamicImport && isLocalImport && - config.server.preTransformRequests + environment.options.dev.preTransformRequests ) { // pre-transform known direct imports // These requests will also be registered in transformRequest to be awaited // by the deps optimizer const url = removeImportQuery(hmrUrl) - server.warmupRequest(url, { ssr }) + environment.warmupRequest(url) } } else if (!importer.startsWith(withTrailingSlash(clientDir))) { if (!isInNodeModules(importer)) { @@ -730,10 +722,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { // normalize and rewrite accepted urls const normalizedAcceptedUrls = new Set() for (const { url, start, end } of acceptedUrls) { - const [normalized] = await moduleGraph.resolveUrl( - toAbsoluteUrl(url), - ssr, - ) + const [normalized] = await moduleGraph.resolveUrl(toAbsoluteUrl(url)) normalizedAcceptedUrls.add(normalized) str().overwrite(start, end, JSON.stringify(normalized), { contentOnly: true, @@ -778,11 +767,10 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { normalizedAcceptedUrls, isPartiallySelfAccepting ? acceptedExports : null, isSelfAccepting, - ssr, staticImportedUrls, ) if (hasHMR && prunedImports) { - handlePrunedModules(prunedImports, server) + handlePrunedModules(prunedImports, environment) } } diff --git a/packages/vite/src/node/plugins/importMetaGlob.ts b/packages/vite/src/node/plugins/importMetaGlob.ts index 413c60f785a514..157d7a91ec8e4f 100644 --- a/packages/vite/src/node/plugins/importMetaGlob.ts +++ b/packages/vite/src/node/plugins/importMetaGlob.ts @@ -21,12 +21,12 @@ import fg from 'fast-glob' import { stringifyQuery } from 'ufo' import type { GeneralImportGlobOptions } from 'types/importGlob' import type { Plugin } from '../plugin' -import type { ViteDevServer } from '../server' -import type { ModuleNode } from '../server/moduleGraph' +import type { EnvironmentModuleNode } from '../server/moduleGraph' import type { ResolvedConfig } from '../config' import { evalValue, normalizePath, transformStableResult } from '../utils' import type { Logger } from '../logger' import { slash } from '../../shared/utils' +import type { Environment } from '../environment' const { isMatch, scan } = micromatch @@ -44,40 +44,18 @@ interface ParsedGeneralImportGlobOptions extends GeneralImportGlobOptions { query?: string } -export function getAffectedGlobModules( - file: string, - server: ViteDevServer, -): ModuleNode[] { - const modules: ModuleNode[] = [] - for (const [id, allGlobs] of server._importGlobMap!) { - // (glob1 || glob2) && !glob3 && !glob4... - if ( - allGlobs.some( - ({ affirmed, negated }) => - (!affirmed.length || affirmed.some((glob) => isMatch(file, glob))) && - (!negated.length || negated.every((glob) => isMatch(file, glob))), - ) - ) { - const mod = server.moduleGraph.getModuleById(id) - if (mod) modules.push(mod) - } - } - modules.forEach((i) => { - if (i?.file) server.moduleGraph.onFileChange(i.file) - }) - return modules -} - export function importGlobPlugin(config: ResolvedConfig): Plugin { - let server: ViteDevServer | undefined + const importGlobMaps = new Map< + Environment, + Map + >() return { name: 'vite:import-glob', - configureServer(_server) { - server = _server - server._importGlobMap.clear() + configureServer() { + importGlobMaps.clear() }, - async transform(code, id) { + async transform(code, id, options) { if (!code.includes('import.meta.glob')) return const result = await transformGlobImport( code, @@ -89,9 +67,12 @@ export function importGlobPlugin(config: ResolvedConfig): Plugin { config.logger, ) if (result) { - if (server) { + if (this.environment) { const allGlobs = result.matches.map((i) => i.globsResolved) - server._importGlobMap.set( + if (!importGlobMaps.has(this.environment)) { + importGlobMaps.set(this.environment, new Map()) + } + importGlobMaps.get(this.environment)!.set( id, allGlobs.map((globs) => { const affirmed: string[] = [] @@ -107,6 +88,29 @@ export function importGlobPlugin(config: ResolvedConfig): Plugin { return transformStableResult(result.s, id, config) } }, + hotUpdate({ type, file, modules: oldModules, environment }) { + if (type === 'update') return + + const importGlobMap = importGlobMaps.get(environment) + if (!importGlobMap) return + + const modules: EnvironmentModuleNode[] = [] + for (const [id, allGlobs] of importGlobMap) { + // (glob1 || glob2) && !glob3 && !glob4... + if ( + allGlobs.some( + ({ affirmed, negated }) => + (!affirmed.length || + affirmed.some((glob) => isMatch(file, glob))) && + (!negated.length || negated.every((glob) => isMatch(file, glob))), + ) + ) { + const mod = environment.moduleGraph.getModuleById(id) + if (mod) modules.push(mod) + } + } + return modules.length > 0 ? [...oldModules, ...modules] : undefined + }, } } diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index fc230c686641b1..1a4a8f68792694 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -3,8 +3,6 @@ import type { ObjectHook } from 'rollup' import type { PluginHookUtils, ResolvedConfig } from '../config' import { isDepsOptimizerEnabled } from '../config' import type { HookHandler, Plugin, PluginWithRequiredHook } from '../plugin' -import { getDepsOptimizer } from '../optimizer' -import { shouldExternalizeForSSR } from '../ssr/ssrExternal' import { watchPackageDataPlugin } from '../packages' import { getFsUtils } from '../fsUtils' import { jsonPlugin } from './json' @@ -26,6 +24,7 @@ import { assetImportMetaUrlPlugin } from './assetImportMetaUrl' import { metadataPlugin } from './metadata' import { dynamicImportVarsPlugin } from './dynamicImportVars' import { importGlobPlugin } from './importMetaGlob' +// TODO: import { loadFallbackPlugin } from './loadFallback' export async function resolvePlugins( config: ResolvedConfig, @@ -56,23 +55,19 @@ export async function resolvePlugins( modulePreload !== false && modulePreload.polyfill ? modulePreloadPolyfillPlugin(config) : null, - resolvePlugin({ - ...config.resolve, - root: config.root, - isProduction: config.isProduction, - isBuild, - packageCache: config.packageCache, - ssrConfig: config.ssr, - asSrc: true, - fsUtils: getFsUtils(config), - getDepsOptimizer: isBuild - ? undefined - : (ssr: boolean) => getDepsOptimizer(config, ssr), - shouldExternalize: - isBuild && config.build.ssr - ? (id, importer) => shouldExternalizeForSSR(id, importer, config) - : undefined, - }), + resolvePlugin( + { + root: config.root, + isProduction: config.isProduction, + isBuild, + packageCache: config.packageCache, + asSrc: true, + fsUtils: getFsUtils(config), + optimizeDeps: true, + externalize: isBuild && !!config.build.ssr, // TODO: should we do this for all environments? + }, + config.environments, + ), htmlInlineProxyPlugin(config), cssPlugin(config), config.esbuild !== false ? esbuildPlugin(config) : null, @@ -105,6 +100,7 @@ export async function resolvePlugins( clientInjectionsPlugin(config), cssAnalysisPlugin(config), importAnalysisPlugin(config), + // TODO: loadFallbackPlugin(config), ]), ].filter(Boolean) as Plugin[] } diff --git a/packages/vite/src/node/plugins/loadFallback.ts b/packages/vite/src/node/plugins/loadFallback.ts index 7d56797e48e681..8b289944ac378e 100644 --- a/packages/vite/src/node/plugins/loadFallback.ts +++ b/packages/vite/src/node/plugins/loadFallback.ts @@ -1,13 +1,111 @@ import fsp from 'node:fs/promises' -import type { Plugin } from '..' +import path from 'node:path' +import type { SourceMap } from 'rollup' import { cleanUrl } from '../../shared/utils' +import type { ResolvedConfig } from '../config' +import type { Plugin } from '../plugin' +import { extractSourcemapFromFile } from '../server/sourcemap' +import { isFileLoadingAllowed } from '../server/middlewares/static' +import type { DevEnvironment } from '../server/environment' +import type { EnvironmentModuleNode } from '../server/moduleGraph' +import { ensureWatchedFile } from '../utils' +import { checkPublicFile } from '../publicDir' /** * A plugin to provide build load fallback for arbitrary request with queries. + * + * TODO: This plugin isn't currently being use. The idea is to consolidate the way + * we handle the fallback during build (also with a plugin) instead of handling this + * in transformRequest(). There are some CI fails right now with the current + * implementation. Reverting for now to be able to merge the other changes. */ -export function loadFallbackPlugin(): Plugin { +export function loadFallbackPlugin(config: ResolvedConfig): Plugin { return { name: 'vite:load-fallback', + async load(id, options) { + const environment = this.environment as DevEnvironment + if (!environment) { + return + } + + let code: string | null = null + let map: SourceMap | null = null + + // if this is an html request and there is no load result, skip ahead to + // SPA fallback. + if (options?.html && !id.endsWith('.html')) { + return null + } + // try fallback loading it from fs as string + // if the file is a binary, there should be a plugin that already loaded it + // as string + // only try the fallback if access is allowed, skip for out of root url + // like /service-worker.js or /api/users + const file = cleanUrl(id) + if ( + environment.options.nodeCompatible || + isFileLoadingAllowed(config, file) // Do we need fsPathFromId here? + ) { + try { + code = await fsp.readFile(file, 'utf-8') + } catch (e) { + if (e.code !== 'ENOENT') { + if (e.code === 'EISDIR') { + e.message = `${e.message} ${file}` + } + throw e + } + } + if (code != null && environment.watcher) { + ensureWatchedFile(environment.watcher, file, config.root) + } + } + if (code) { + try { + const extracted = await extractSourcemapFromFile(code, file) + if (extracted) { + code = extracted.code + map = extracted.map + } + } catch (e) { + environment.logger.warn( + `Failed to load source map for ${file}.\n${e}`, + { + timestamp: true, + }, + ) + } + return { code, map } + } + + const isPublicFile = checkPublicFile(id, config) + let publicDirName = path.relative(config.root, config.publicDir) + if (publicDirName[0] !== '.') publicDirName = '/' + publicDirName + const msg = isPublicFile + ? `This file is in ${publicDirName} and will be copied as-is during ` + + `build without going through the plugin transforms, and therefore ` + + `should not be imported from source code. It can only be referenced ` + + `via HTML tags.` + : `Does the file exist?` + const importerMod: EnvironmentModuleNode | undefined = + environment.moduleGraph.idToModuleMap + .get(id) + ?.importers.values() + .next().value + const importer = importerMod?.file || importerMod?.url + environment.logger.warn( + `Failed to load ${id}${importer ? ` in ${importer}` : ''}. ${msg}`, + ) + }, + } +} + +/** + * A plugin to provide build load fallback for arbitrary request with queries. + */ +export function buildLoadFallbackPlugin(): Plugin { + return { + name: 'vite:build-load-fallback', async load(id) { try { const cleanedId = cleanUrl(id) diff --git a/packages/vite/src/node/plugins/optimizedDeps.ts b/packages/vite/src/node/plugins/optimizedDeps.ts index 6d6a8d22eb9468..5d6a76c90e62e8 100644 --- a/packages/vite/src/node/plugins/optimizedDeps.ts +++ b/packages/vite/src/node/plugins/optimizedDeps.ts @@ -1,10 +1,10 @@ import fsp from 'node:fs/promises' import colors from 'picocolors' -import type { ResolvedConfig } from '..' +import type { DevEnvironment, ResolvedConfig } from '..' import type { Plugin } from '../plugin' import { DEP_VERSION_RE } from '../constants' import { createDebugger } from '../utils' -import { getDepsOptimizer, optimizedDepInfoFromFile } from '../optimizer' +import { optimizedDepInfoFromFile } from '../optimizer' import { cleanUrl } from '../../shared/utils' export const ERR_OPTIMIZE_DEPS_PROCESSING_ERROR = @@ -19,8 +19,9 @@ export function optimizedDepsPlugin(config: ResolvedConfig): Plugin { return { name: 'vite:optimized-deps', - resolveId(id, source, { ssr }) { - if (getDepsOptimizer(config, ssr)?.isOptimizedDepFile(id)) { + resolveId(id) { + const environment = this.environment as DevEnvironment + if (environment?.depsOptimizer?.isOptimizedDepFile(id)) { return id } }, @@ -29,9 +30,9 @@ export function optimizedDepsPlugin(config: ResolvedConfig): Plugin { // The logic to register an id to wait until it is processed // is in importAnalysis, see call to delayDepsOptimizerUntil - async load(id, options) { - const ssr = options?.ssr === true - const depsOptimizer = getDepsOptimizer(config, ssr) + async load(id) { + const environment = this.environment as DevEnvironment + const depsOptimizer = environment?.depsOptimizer if (depsOptimizer?.isOptimizedDepFile(id)) { const metadata = depsOptimizer.metadata const file = cleanUrl(id) diff --git a/packages/vite/src/node/plugins/preAlias.ts b/packages/vite/src/node/plugins/preAlias.ts index eaefdb7e6eb65d..1e9edf0991915d 100644 --- a/packages/vite/src/node/plugins/preAlias.ts +++ b/packages/vite/src/node/plugins/preAlias.ts @@ -6,7 +6,7 @@ import type { ResolvedConfig, } from '..' import type { Plugin } from '../plugin' -import { createIsConfiguredAsSsrExternal } from '../ssr/ssrExternal' +import { isConfiguredAsExternal } from '../external' import { bareImportRE, isInNodeModules, @@ -14,7 +14,6 @@ import { moduleListContains, } from '../utils' import { getFsUtils } from '../fsUtils' -import { getDepsOptimizer } from '../optimizer' import { cleanUrl, withTrailingSlash } from '../../shared/utils' import { tryOptimizedResolve } from './resolve' @@ -23,15 +22,17 @@ import { tryOptimizedResolve } from './resolve' */ export function preAliasPlugin(config: ResolvedConfig): Plugin { const findPatterns = getAliasPatterns(config.resolve.alias) - const isConfiguredAsExternal = createIsConfiguredAsSsrExternal(config) const isBuild = config.command === 'build' const fsUtils = getFsUtils(config) return { name: 'vite:pre-alias', async resolveId(id, importer, options) { + const { environment } = this const ssr = options?.ssr === true - const depsOptimizer = !isBuild && getDepsOptimizer(config, ssr) + const depsOptimizer = + environment?.mode === 'dev' ? environment.depsOptimizer : undefined if ( + environment && importer && depsOptimizer && bareImportRE.test(id) && @@ -69,7 +70,11 @@ export function preAliasPlugin(config: ResolvedConfig): Plugin { (isInNodeModules(resolvedId) || optimizeDeps.include?.includes(id)) && isOptimizable(resolvedId, optimizeDeps) && - !(isBuild && ssr && isConfiguredAsExternal(id, importer)) && + !( + isBuild && + ssr && + isConfiguredAsExternal(environment, id, importer) + ) && (!ssr || optimizeAliasReplacementForSSR(resolvedId, optimizeDeps)) ) { // aliased dep has not yet been optimized @@ -87,6 +92,7 @@ export function preAliasPlugin(config: ResolvedConfig): Plugin { } } +// TODO: environment? function optimizeAliasReplacementForSSR( id: string, optimizeDeps: DepOptimizationOptions, diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index ccffd1c152972c..aa486f0aee425d 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -35,12 +35,14 @@ import { safeRealpathSync, tryStatSync, } from '../utils' +import type { ResolvedEnvironmentOptions } from '../config' import { optimizedDepInfoFromFile, optimizedDepInfoFromId } from '../optimizer' import type { DepsOptimizer } from '../optimizer' -import type { SSROptions } from '..' +import type { DepOptimizationConfig, SSROptions } from '..' import type { PackageCache, PackageData } from '../packages' import type { FsUtils } from '../fsUtils' import { commonFsUtils } from '../fsUtils' +import { shouldExternalize } from '../external' import { findNearestMainPackageData, findNearestPackageData, @@ -79,6 +81,7 @@ export interface ResolveOptions { */ mainFields?: string[] conditions?: string[] + externalConditions?: string[] /** * @default ['.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx', '.json'] */ @@ -88,13 +91,19 @@ export interface ResolveOptions { * @default false */ preserveSymlinks?: boolean + /** + * external/noExternal logic, this only works for certain environments + * Previously this was ssr.external/ssr.noExternal + * TODO: better abstraction that works for the client environment too? + */ + noExternal?: string | RegExp | (string | RegExp)[] | true + external?: string[] | true } -export interface InternalResolveOptions extends Required { +interface ResolvePluginOptions { root: string isBuild: boolean isProduction: boolean - ssrConfig?: SSROptions packageCache?: PackageCache fsUtils?: FsUtils /** @@ -107,6 +116,8 @@ export interface InternalResolveOptions extends Required { tryPrefix?: string preferRelative?: boolean isRequire?: boolean + nodeCompatible?: boolean + webCompatible?: boolean // #3040 // when the importer is a ts module, // if the specifier requests a non-existent `.js/jsx/mjs/cjs` file, @@ -117,8 +128,31 @@ export interface InternalResolveOptions extends Required { scan?: boolean // Appends ?__vite_skip_optimization to the resolved id if shouldn't be optimized ssrOptimizeCheck?: boolean - // Resolve using esbuild deps optimization + + /** + * Optimize deps during dev, defaults to false // TODO: Review default + * @internal + */ + optimizeDeps?: boolean + + /** + * externalize using external/noExternal, defaults to false // TODO: Review default + * @internal + */ + externalize?: boolean + + /** + * Previous deps optimizer logic + * @internal + * @deprecated + */ getDepsOptimizer?: (ssr: boolean) => DepsOptimizer | undefined + + /** + * Externalize logic for SSR builds + * @internal + * @deprecated + */ shouldExternalize?: (id: string, importer?: string) => boolean | undefined /** @@ -127,22 +161,36 @@ export interface InternalResolveOptions extends Required { * @internal */ idOnly?: boolean + + /** + * @deprecated environment.options are used instead + */ + ssrConfig?: SSROptions } -export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { - const { - root, - isProduction, - asSrc, - ssrConfig, - preferRelative = false, - } = resolveOptions - - const { - target: ssrTarget, - noExternal: ssrNoExternal, - external: ssrExternal, - } = ssrConfig ?? {} +export interface InternalResolveOptions + extends Required, + ResolvePluginOptions {} + +// Defined ResolveOptions are used to overwrite the values for all environments +// It is used when creating custom resolvers (for CSS, scanning, etc) +// TODO: It could be more clear to make the plugin constructor be: +// resolvePlugin(pluginOptions: ResolvePluginOptions, overrideResolveOptions?: ResolveOptions) +export interface ResolvePluginOptionsWithOverrides + extends ResolveOptions, + ResolvePluginOptions {} + +export function resolvePlugin( + resolveOptions: ResolvePluginOptionsWithOverrides, + /** + * @internal + * The deprecated config.createResolver creates a pluginContainer before + * environments are created. The resolve plugin is especial as it works without + * environments to enable this use case. It only needs access to the resolve options. + */ + environmentsOptions: Record, +): Plugin { + const { root, isProduction, asSrc, preferRelative = false } = resolveOptions // In unix systems, absolute paths inside root first needs to be checked as an // absolute URL (/root/root/path-to-file) resulting in failed checks before falling @@ -166,39 +214,41 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { const ssr = resolveOpts?.ssr === true - // We need to delay depsOptimizer until here instead of passing it as an option - // the resolvePlugin because the optimizer is created on server listen during dev - const depsOptimizer = resolveOptions.getDepsOptimizer?.(ssr) + // The resolve plugin is used for createIdResolver and the depsOptimizer should be + // disabled in that case, so deps optimization is opt-in when creating the plugin. + const depsOptimizer = + resolveOptions.optimizeDeps && this.environment?.mode === 'dev' + ? this.environment?.depsOptimizer + : undefined if (id.startsWith(browserExternalId)) { return id } - const targetWeb = !ssr || ssrTarget === 'webworker' - // this is passed by @rollup/plugin-commonjs const isRequire: boolean = resolveOpts?.custom?.['node-resolve']?.isRequire ?? false - // end user can configure different conditions for ssr and client. - // falls back to client conditions if no ssr conditions supplied - const ssrConditions = - resolveOptions.ssrConfig?.resolve?.conditions || - resolveOptions.conditions - + const environmentName = this.environment?.name ?? (ssr ? 'ssr' : 'client') + const environmentResolveOptions = + environmentsOptions[environmentName].resolve + if (!environmentResolveOptions) { + throw new Error( + `Missing ResolveOptions for ${environmentName} environment`, + ) + } const options: InternalResolveOptions = { isRequire, - ...resolveOptions, + ...environmentResolveOptions, + nodeCompatible: environmentsOptions[environmentName].nodeCompatible, + webCompatible: environmentsOptions[environmentName].webCompatible, + ...resolveOptions, // plugin options + resolve options overrides scan: resolveOpts?.scan ?? resolveOptions.scan, - conditions: ssr ? ssrConditions : resolveOptions.conditions, } - const resolvedImports = resolveSubpathImports( - id, - importer, - options, - targetWeb, - ) + const depsOptimizerOptions = this.environment?.options.dev.optimizeDeps + + const resolvedImports = resolveSubpathImports(id, importer, options) if (resolvedImports) { id = resolvedImports @@ -238,7 +288,7 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { // always return here even if res doesn't exist since /@fs/ is explicit // if the file doesn't exist it should be a 404. debug?.(`[@fs] ${colors.cyan(id)} -> ${colors.dim(res)}`) - return ensureVersionQuery(res, id, options, depsOptimizer) + return ensureVersionQuery(res, id, options, ssr, depsOptimizer) } // URL @@ -251,7 +301,7 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { const fsPath = path.resolve(root, id.slice(1)) if ((res = tryFsResolve(fsPath, options))) { debug?.(`[url] ${colors.cyan(id)} -> ${colors.dim(res)}`) - return ensureVersionQuery(res, id, options, depsOptimizer) + return ensureVersionQuery(res, id, options, ssr, depsOptimizer) } } @@ -270,10 +320,7 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { if (depsOptimizer?.isOptimizedDepFile(normalizedFsPath)) { // Optimized files could not yet exist in disk, resolve to the full path // Inject the current browserHash version if the path doesn't have one - if ( - !resolveOptions.isBuild && - !DEP_VERSION_RE.test(normalizedFsPath) - ) { + if (!options.isBuild && !DEP_VERSION_RE.test(normalizedFsPath)) { const browserHash = optimizedDepInfoFromFile( depsOptimizer.metadata, normalizedFsPath, @@ -286,15 +333,22 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { } if ( - targetWeb && + options.webCompatible && options.mainFields.includes('browser') && - (res = tryResolveBrowserMapping(fsPath, importer, options, true)) + (res = tryResolveBrowserMapping( + fsPath, + importer, + options, + true, + undefined, + depsOptimizerOptions, + )) ) { return res } if ((res = tryFsResolve(fsPath, options))) { - res = ensureVersionQuery(res, id, options, depsOptimizer) + res = ensureVersionQuery(res, id, options, ssr, depsOptimizer) debug?.(`[relative] ${colors.cyan(id)} -> ${colors.dim(res)}`) // If this isn't a script imported from a .html file, include side effects @@ -326,7 +380,7 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { const fsPath = path.resolve(basedir, id) if ((res = tryFsResolve(fsPath, options))) { debug?.(`[drive-relative] ${colors.cyan(id)} -> ${colors.dim(res)}`) - return ensureVersionQuery(res, id, options, depsOptimizer) + return ensureVersionQuery(res, id, options, ssr, depsOptimizer) } } @@ -336,7 +390,7 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { (res = tryFsResolve(id, options)) ) { debug?.(`[fs] ${colors.cyan(id)} -> ${colors.dim(res)}`) - return ensureVersionQuery(res, id, options, depsOptimizer) + return ensureVersionQuery(res, id, options, ssr, depsOptimizer) } // external @@ -352,7 +406,9 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { // bare package imports, perform node resolve if (bareImportRE.test(id)) { - const external = options.shouldExternalize?.(id, importer) + const external = + options.externalize && + shouldExternalize(this.environment!, id, importer) // TODO if ( !external && asSrc && @@ -370,7 +426,7 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { } if ( - targetWeb && + options.webCompatible && options.mainFields.includes('browser') && (res = tryResolveBrowserMapping( id, @@ -378,6 +434,7 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { options, false, external, + depsOptimizerOptions, )) ) { return res @@ -388,25 +445,26 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { id, importer, options, - targetWeb, depsOptimizer, ssr, external, + undefined, + depsOptimizerOptions, )) ) { return res } // node built-ins. - // externalize if building for SSR, otherwise redirect to empty module + // externalize if building for a node compatible environment, otherwise redirect to empty module if (isBuiltin(id)) { - if (ssr) { + if (options.nodeCompatible) { if ( - targetWeb && - ssrNoExternal === true && + options.webCompatible && + options.noExternal === true && // if both noExternal and external are true, noExternal will take the higher priority and bundle it. // only if the id is explicitly listed in external, we will externalize it and skip this error. - (ssrExternal === true || !ssrExternal?.includes(id)) + (options.external === true || !options.external.includes(id)) ) { let message = `Cannot bundle Node.js built-in "${id}"` if (importer) { @@ -415,7 +473,7 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { importer, )}"` } - message += `. Consider disabling ssr.noExternal or remove the built-in dependency.` + message += `. Consider disabling environments.${environmentName}.noExternal or remove the built-in dependency.` this.error(message) } @@ -474,7 +532,6 @@ function resolveSubpathImports( id: string, importer: string | undefined, options: InternalResolveOptions, - targetWeb: boolean, ) { if (!importer || !id.startsWith(subpathImportsPrefix)) return const basedir = path.dirname(importer) @@ -488,7 +545,6 @@ function resolveSubpathImports( pkgData.data, idWithoutPostfix, options, - targetWeb, 'imports', ) @@ -507,9 +563,11 @@ function ensureVersionQuery( resolved: string, id: string, options: InternalResolveOptions, + ssr: boolean, depsOptimizer?: DepsOptimizer, ): string { if ( + !ssr && !options.isBuild && !options.scan && depsOptimizer && @@ -665,7 +723,7 @@ function tryCleanFsResolve( } // path points to a node package const pkg = loadPackageData(pkgPath) - return resolvePackageEntry(dirPath, pkg, targetWeb, options) + return resolvePackageEntry(dirPath, pkg, options) } } catch (e) { // This check is best effort, so if an entry is not found, skip error for now @@ -699,6 +757,7 @@ function tryCleanFsResolve( export type InternalResolveOptionsWithOverrideConditions = InternalResolveOptions & { /** + * TODO: Is this needed if we have `externalConditions` in `resolve`? * @internal */ overrideConditions?: string[] @@ -708,11 +767,11 @@ export function tryNodeResolve( id: string, importer: string | null | undefined, options: InternalResolveOptionsWithOverrideConditions, - targetWeb: boolean, depsOptimizer?: DepsOptimizer, ssr: boolean = false, externalize?: boolean, allowLinkedExternal: boolean = true, + depsOptimizerOptions?: DepOptimizationConfig, ): PartialResolvedId | undefined { const { root, dedupe, isBuild, preserveSymlinks, packageCache } = options @@ -780,14 +839,14 @@ export function tryNodeResolve( let resolved: string | undefined try { - resolved = resolveId(unresolvedId, pkg, targetWeb, options) + resolved = resolveId(unresolvedId, pkg, options) } catch (err) { if (!options.tryEsmOnly) { throw err } } if (!resolved && options.tryEsmOnly) { - resolved = resolveId(unresolvedId, pkg, targetWeb, { + resolved = resolveId(unresolvedId, pkg, { ...options, isRequire: false, mainFields: DEFAULT_MAIN_FIELDS, @@ -861,8 +920,8 @@ export function tryNodeResolve( let include = depsOptimizer?.options.include if (options.ssrOptimizeCheck) { // we don't have the depsOptimizer - exclude = options.ssrConfig?.optimizeDeps?.exclude - include = options.ssrConfig?.optimizeDeps?.include + exclude = depsOptimizerOptions?.exclude + include = depsOptimizerOptions?.include } const skipOptimization = @@ -978,12 +1037,11 @@ export async function tryOptimizedResolve( export function resolvePackageEntry( id: string, { dir, data, setResolvedCache, getResolvedCache }: PackageData, - targetWeb: boolean, options: InternalResolveOptions, ): string | undefined { const { file: idWithoutPostfix, postfix } = splitFileAndPostfix(id) - const cached = getResolvedCache('.', targetWeb) + const cached = getResolvedCache('.', !!options.webCompatible) if (cached) { return cached + postfix } @@ -994,20 +1052,14 @@ export function resolvePackageEntry( // resolve exports field with highest priority // using https://github.com/lukeed/resolve.exports if (data.exports) { - entryPoint = resolveExportsOrImports( - data, - '.', - options, - targetWeb, - 'exports', - ) + entryPoint = resolveExportsOrImports(data, '.', options, 'exports') } // fallback to mainFields if still not resolved if (!entryPoint) { for (const field of options.mainFields) { if (field === 'browser') { - if (targetWeb) { + if (options.webCompatible) { entryPoint = tryResolveBrowserEntry(dir, data, options) if (entryPoint) { break @@ -1040,7 +1092,7 @@ export function resolvePackageEntry( // resolve object browser field in package.json const { browser: browserField } = data if ( - targetWeb && + options.webCompatible && options.mainFields.includes('browser') && isObject(browserField) ) { @@ -1062,7 +1114,7 @@ export function resolvePackageEntry( resolvedEntryPoint, )}${postfix !== '' ? ` (postfix: ${postfix})` : ''}`, ) - setResolvedCache('.', resolvedEntryPoint, targetWeb) + setResolvedCache('.', resolvedEntryPoint, !!options.webCompatible) return resolvedEntryPoint + postfix } } @@ -1086,7 +1138,6 @@ function resolveExportsOrImports( pkg: PackageData['data'], key: string, options: InternalResolveOptionsWithOverrideConditions, - targetWeb: boolean, type: 'imports' | 'exports', ) { const additionalConditions = new Set( @@ -1110,7 +1161,7 @@ function resolveExportsOrImports( const fn = type === 'imports' ? imports : exports const result = fn(pkg, key, { - browser: targetWeb && !additionalConditions.has('node'), + browser: options.webCompatible && !additionalConditions.has('node'), require: options.isRequire && !additionalConditions.has('import'), conditions, }) @@ -1127,10 +1178,9 @@ function resolveDeepImport( dir, data, }: PackageData, - targetWeb: boolean, options: InternalResolveOptions, ): string | undefined { - const cache = getResolvedCache(id, targetWeb) + const cache = getResolvedCache(id, !!options.webCompatible) if (cache) { return cache } @@ -1143,13 +1193,7 @@ function resolveDeepImport( if (isObject(exportsField) && !Array.isArray(exportsField)) { // resolve without postfix (see #7098) const { file, postfix } = splitFileAndPostfix(relativeId) - const exportsId = resolveExportsOrImports( - data, - file, - options, - targetWeb, - 'exports', - ) + const exportsId = resolveExportsOrImports(data, file, options, 'exports') if (exportsId !== undefined) { relativeId = exportsId + postfix } else { @@ -1166,7 +1210,7 @@ function resolveDeepImport( ) } } else if ( - targetWeb && + options.webCompatible && options.mainFields.includes('browser') && isObject(browserField) ) { @@ -1185,13 +1229,13 @@ function resolveDeepImport( path.join(dir, relativeId), options, !exportsField, // try index only if no exports field - targetWeb, + !!options.webCompatible, ) if (resolved) { debug?.( `[node/deep-import] ${colors.cyan(id)} -> ${colors.dim(resolved)}`, ) - setResolvedCache(id, resolved, targetWeb) + setResolvedCache(id, resolved, !!options.webCompatible) return resolved } } @@ -1203,6 +1247,7 @@ function tryResolveBrowserMapping( options: InternalResolveOptions, isFilePath: boolean, externalize?: boolean, + depsOptimizerOptions?: DepOptimizationConfig, ) { let res: string | undefined const pkg = @@ -1214,7 +1259,16 @@ function tryResolveBrowserMapping( if (browserMappedPath) { if ( (res = bareImportRE.test(browserMappedPath) - ? tryNodeResolve(browserMappedPath, importer, options, true)?.id + ? tryNodeResolve( + browserMappedPath, + importer, + options, + undefined, + undefined, + undefined, + undefined, + depsOptimizerOptions, + )?.id : tryFsResolve(path.join(pkg.dir, browserMappedPath), options)) ) { debug?.(`[browser mapped] ${colors.cyan(id)} -> ${colors.dim(res)}`) diff --git a/packages/vite/src/node/plugins/worker.ts b/packages/vite/src/node/plugins/worker.ts index 4094b581a52b63..2e13ce45c55cc4 100644 --- a/packages/vite/src/node/plugins/worker.ts +++ b/packages/vite/src/node/plugins/worker.ts @@ -13,7 +13,9 @@ import { urlRE, } from '../utils' import { + BuildEnvironment, createToImportMetaURLBasedRelativeRuntime, + injectEnvironmentToHooks, onRollupWarning, toOutputFilePathInJS, } from '../build' @@ -68,10 +70,14 @@ async function bundleWorkerEntry( // bundle the file as entry to support imports const { rollup } = await import('rollup') const { plugins, rollupOptions, format } = config.worker + const workerEnvironment = new BuildEnvironment('client', config) // TODO: should this be 'worker'? + const resolvedPlugins = await plugins(newBundleChain) const bundle = await rollup({ ...rollupOptions, input, - plugins: await plugins(newBundleChain), + plugins: resolvedPlugins.map((p) => + injectEnvironmentToHooks(p, workerEnvironment), + ), onwarn(warning, warn) { onRollupWarning(warning, warn, config) }, @@ -236,7 +242,7 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin { } }, - async transform(raw, id) { + async transform(raw, id, options) { const workerFileMatch = workerFileRE.exec(id) if (workerFileMatch) { // if import worker by worker constructor will have query.type @@ -258,8 +264,10 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin { } else if (server) { // dynamic worker type we can't know how import the env // so we copy /@vite/env code of server transform result into file header - const { moduleGraph } = server - const module = moduleGraph.getModuleById(ENV_ENTRY) + const environment = this.environment + const moduleGraph = + environment?.mode === 'dev' ? environment.moduleGraph : undefined + const module = moduleGraph?.getModuleById(ENV_ENTRY) injectEnv = module?.transformResult?.code || '' } } diff --git a/packages/vite/src/node/plugins/workerImportMetaUrl.ts b/packages/vite/src/node/plugins/workerImportMetaUrl.ts index 0a7b34d4ff3dc8..53f867a004a4b1 100644 --- a/packages/vite/src/node/plugins/workerImportMetaUrl.ts +++ b/packages/vite/src/node/plugins/workerImportMetaUrl.ts @@ -5,7 +5,8 @@ import { stripLiteral } from 'strip-literal' import type { ResolvedConfig } from '../config' import type { Plugin } from '../plugin' import { evalValue, injectQuery, transformStableResult } from '../utils' -import type { ResolveFn } from '..' +import { createIdResolver } from '../idResolver' +import type { ResolveIdFn } from '../idResolver' import { cleanUrl, slash } from '../../shared/utils' import type { WorkerType } from './worker' import { WORKER_FILE_ID, workerFileToUrl } from './worker' @@ -102,7 +103,7 @@ function isIncludeWorkerImportMetaUrl(code: string): boolean { export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin { const isBuild = config.command === 'build' - let workerResolver: ResolveFn + let workerResolver: ResolveIdFn const fsResolveOptions: InternalResolveOptions = { ...config.resolve, @@ -110,7 +111,6 @@ export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin { isProduction: config.isProduction, isBuild: config.command === 'build', packageCache: config.packageCache, - ssrConfig: config.ssr, asSrc: true, } @@ -123,8 +123,14 @@ export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin { } }, - async transform(code, id, options) { - if (!options?.ssr && isIncludeWorkerImportMetaUrl(code)) { + async transform(code, id) { + const { environment } = this + // TODO: environment, same as with assetImportMetaUrlPlugin + if ( + environment && + environment.name === 'client' && + isIncludeWorkerImportMetaUrl(code) + ) { let s: MagicString | undefined const cleanString = stripLiteral(code) const workerImportMetaUrlRE = @@ -153,12 +159,12 @@ export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin { file = path.resolve(path.dirname(id), url) file = tryFsResolve(file, fsResolveOptions) ?? file } else { - workerResolver ??= config.createResolver({ + workerResolver ??= createIdResolver(config, { extensions: [], tryIndex: false, preferRelative: true, }) - file = await workerResolver(url, id) + file = await workerResolver(environment, url, id) file ??= url[0] === '/' ? slash(path.join(config.publicDir, url)) diff --git a/packages/vite/src/node/preview.ts b/packages/vite/src/node/preview.ts index 4d2e1e645bbcdc..566b5d886e790f 100644 --- a/packages/vite/src/node/preview.ts +++ b/packages/vite/src/node/preview.ts @@ -111,7 +111,9 @@ export async function preview( true, ) - const distDir = path.resolve(config.root, config.build.outDir) + const clientOutDir = + config.environments.client.build.outDir ?? config.build.outDir + const distDir = path.resolve(config.root, clientOutDir) if ( !fs.existsSync(distDir) && // error if no plugins implement `configurePreviewServer` @@ -122,7 +124,7 @@ export async function preview( process.argv[2] === 'preview' ) { throw new Error( - `The directory "${config.build.outDir}" does not exist. Did you build your project?`, + `The directory "${clientOutDir}" does not exist. Did you build your project?`, ) } diff --git a/packages/vite/src/node/publicUtils.ts b/packages/vite/src/node/publicUtils.ts index 318c904047b2c0..5c8c0ca99cdbbf 100644 --- a/packages/vite/src/node/publicUtils.ts +++ b/packages/vite/src/node/publicUtils.ts @@ -20,5 +20,7 @@ export { export { send } from './server/send' export { createLogger } from './logger' export { searchForWorkspaceRoot } from './server/searchRoot' + +// TODO: export isFileLoadingAllowed? export { isFileServingAllowed } from './server/middlewares/static' export { loadEnv, resolveEnvPrefix } from './env' diff --git a/packages/vite/src/node/server/__tests__/moduleGraph.spec.ts b/packages/vite/src/node/server/__tests__/moduleGraph.spec.ts index 2285d2fa4fa8b9..91c933c789b54f 100644 --- a/packages/vite/src/node/server/__tests__/moduleGraph.spec.ts +++ b/packages/vite/src/node/server/__tests__/moduleGraph.spec.ts @@ -1,10 +1,14 @@ import { describe, expect, it } from 'vitest' -import { ModuleGraph } from '../moduleGraph' +import { EnvironmentModuleGraph } from '../moduleGraph' +import type { ModuleNode } from '../mixedModuleGraph' +import { ModuleGraph } from '../mixedModuleGraph' describe('moduleGraph', () => { describe('invalidateModule', () => { - it('removes an ssrError', async () => { - const moduleGraph = new ModuleGraph(async (url) => ({ id: url })) + it('removes an ssr error', async () => { + const moduleGraph = new EnvironmentModuleGraph('client', async (url) => ({ + id: url, + })) const entryUrl = '/x.js' const entryModule = await moduleGraph.ensureEntryFromUrl(entryUrl, false) @@ -16,7 +20,7 @@ describe('moduleGraph', () => { }) it('ensureEntryFromUrl should based on resolvedId', async () => { - const moduleGraph = new ModuleGraph(async (url) => { + const moduleGraph = new EnvironmentModuleGraph('client', async (url) => { if (url === '/xx.js') { return { id: '/x.js' } } else { @@ -30,5 +34,75 @@ describe('moduleGraph', () => { const mod2 = await moduleGraph.ensureEntryFromUrl('/xx.js', false) expect(mod2.meta).to.equal(meta) }) + + it('ensure backward compatibility', async () => { + const clientModuleGraph = new EnvironmentModuleGraph( + 'client', + async (url) => ({ id: url }), + ) + const ssrModuleGraph = new EnvironmentModuleGraph('ssr', async (url) => ({ + id: url, + })) + const moduleGraph = new ModuleGraph({ + client: () => clientModuleGraph, + ssr: () => ssrModuleGraph, + }) + + const addBrowserModule = (url: string) => + clientModuleGraph.ensureEntryFromUrl(url) + const getBrowserModule = (url: string) => + clientModuleGraph.getModuleById(url) + + const addServerModule = (url: string) => + ssrModuleGraph.ensureEntryFromUrl(url) + const getServerModule = (url: string) => ssrModuleGraph.getModuleById(url) + + const clientModule1 = await addBrowserModule('/1.js') + const ssrModule1 = await addServerModule('/1.js') + const module1 = moduleGraph.getModuleById('/1.js')! + expect(module1._clientModule).toBe(clientModule1) + expect(module1._ssrModule).toBe(ssrModule1) + + const module2b = await moduleGraph.ensureEntryFromUrl('/b/2.js') + const module2s = await moduleGraph.ensureEntryFromUrl('/s/2.js') + expect(module2b._clientModule).toBe(getBrowserModule('/b/2.js')) + expect(module2s._ssrModule).toBe(getServerModule('/s/2.js')) + + const importersUrls = ['/1/a.js', '/1/b.js', '/1/c.js'] + ;(await Promise.all(importersUrls.map(addBrowserModule))).forEach((mod) => + clientModule1.importers.add(mod), + ) + ;(await Promise.all(importersUrls.map(addServerModule))).forEach((mod) => + ssrModule1.importers.add(mod), + ) + + expect(module1.importers.size).toBe(importersUrls.length) + + const clientModule1importersValues = [...clientModule1.importers] + const ssrModule1importersValues = [...ssrModule1.importers] + + const module1importers = module1.importers + const module1importersValues = [...module1importers.values()] + expect(module1importersValues.length).toBe(importersUrls.length) + expect(module1importersValues[1]._clientModule).toBe( + clientModule1importersValues[1], + ) + expect(module1importersValues[1]._ssrModule).toBe( + ssrModule1importersValues[1], + ) + + const module1importersFromForEach: ModuleNode[] = [] + module1.importers.forEach((imp) => { + moduleGraph.invalidateModule(imp) + module1importersFromForEach.push(imp) + }) + expect(module1importersFromForEach.length).toBe(importersUrls.length) + expect(module1importersFromForEach[1]._clientModule).toBe( + clientModule1importersValues[1], + ) + expect(module1importersFromForEach[1]._ssrModule).toBe( + ssrModule1importersValues[1], + ) + }) }) }) diff --git a/packages/vite/src/node/server/__tests__/pluginContainer.spec.ts b/packages/vite/src/node/server/__tests__/pluginContainer.spec.ts index 070dedd2acb463..a92d38c7f01af9 100644 --- a/packages/vite/src/node/server/__tests__/pluginContainer.spec.ts +++ b/packages/vite/src/node/server/__tests__/pluginContainer.spec.ts @@ -1,20 +1,11 @@ -import { beforeEach, describe, expect, it } from 'vitest' +import { describe, expect, it } from 'vitest' import type { UserConfig } from '../../config' import { resolveConfig } from '../../config' import type { Plugin } from '../../plugin' -import { ModuleGraph } from '../moduleGraph' -import type { PluginContainer } from '../pluginContainer' -import { createPluginContainer } from '../pluginContainer' - -let resolveId: (id: string) => any -let moduleGraph: ModuleGraph +import { DevEnvironment } from '../environment' describe('plugin container', () => { describe('getModuleInfo', () => { - beforeEach(() => { - moduleGraph = new ModuleGraph((id) => resolveId(id)) - }) - it('can pass metadata between hooks', async () => { const entryUrl = '/x.js' @@ -46,26 +37,25 @@ describe('plugin container', () => { return { meta: { x: 3 } } } }, - buildEnd() { - const { meta } = this.getModuleInfo(entryUrl) ?? {} - metaArray.push(meta) - }, } - const container = await getPluginContainer({ + const environment = await getDevEnvironment({ plugins: [plugin], }) - const entryModule = await moduleGraph.ensureEntryFromUrl(entryUrl, false) + const entryModule = await environment.moduleGraph.ensureEntryFromUrl( + entryUrl, + false, + ) expect(entryModule.meta).toEqual({ x: 1 }) - const loadResult: any = await container.load(entryUrl) + const loadResult: any = await environment.pluginContainer.load(entryUrl) expect(loadResult?.meta).toEqual({ x: 2 }) - await container.transform(loadResult.code, entryUrl) - await container.close() + await environment.pluginContainer.transform(loadResult.code, entryUrl) + await environment.pluginContainer.close() - expect(metaArray).toEqual([{ x: 1 }, { x: 2 }, { x: 3 }]) + expect(metaArray).toEqual([{ x: 1 }, { x: 2 }]) }) it('can pass metadata between plugins', async () => { @@ -91,12 +81,12 @@ describe('plugin container', () => { }, } - const container = await getPluginContainer({ + const environment = await getDevEnvironment({ plugins: [plugin1, plugin2], }) - await moduleGraph.ensureEntryFromUrl(entryUrl, false) - await container.load(entryUrl) + await environment.moduleGraph.ensureEntryFromUrl(entryUrl, false) + await environment.pluginContainer.load(entryUrl) expect.assertions(1) }) @@ -137,22 +127,18 @@ describe('plugin container', () => { }, } - const container = await getPluginContainer({ + const environment = await getDevEnvironment({ plugins: [plugin1, plugin2], }) - await moduleGraph.ensureEntryFromUrl(entryUrl, false) - await container.load(entryUrl) + await environment.moduleGraph.ensureEntryFromUrl(entryUrl, false) + await environment.pluginContainer.load(entryUrl) expect.assertions(2) }) }) describe('load', () => { - beforeEach(() => { - moduleGraph = new ModuleGraph((id) => resolveId(id)) - }) - it('can resolve a secondary module', async () => { const entryUrl = '/x.js' @@ -176,12 +162,15 @@ describe('plugin container', () => { }, } - const container = await getPluginContainer({ + const environment = await getDevEnvironment({ plugins: [plugin], }) - await moduleGraph.ensureEntryFromUrl(entryUrl, false) - const loadResult: any = await container.load(entryUrl) - const result: any = await container.transform(loadResult.code, entryUrl) + await environment.moduleGraph.ensureEntryFromUrl(entryUrl, false) + const loadResult: any = await environment.pluginContainer.load(entryUrl) + const result: any = await environment.pluginContainer.transform( + loadResult.code, + entryUrl, + ) expect(result.code).equals('2') }) @@ -208,20 +197,23 @@ describe('plugin container', () => { }, } - const container = await getPluginContainer({ + const environment = await getDevEnvironment({ plugins: [plugin], }) - await moduleGraph.ensureEntryFromUrl(entryUrl, false) - const loadResult: any = await container.load(entryUrl) - const result: any = await container.transform(loadResult.code, entryUrl) + await environment.moduleGraph.ensureEntryFromUrl(entryUrl, false) + const loadResult: any = await environment.pluginContainer.load(entryUrl) + const result: any = await environment.pluginContainer.transform( + loadResult.code, + entryUrl, + ) expect(result.code).equals('3') }) }) }) -async function getPluginContainer( +async function getDevEnvironment( inlineConfig?: UserConfig, -): Promise { +): Promise { const config = await resolveConfig( { configFile: false, ...inlineConfig }, 'serve', @@ -230,7 +222,8 @@ async function getPluginContainer( // @ts-expect-error This plugin requires a ViteDevServer instance. config.plugins = config.plugins.filter((p) => !p.name.includes('pre-alias')) - resolveId = (id) => container.resolveId(id) - const container = await createPluginContainer(config, moduleGraph) - return container + const environment = new DevEnvironment('client', config) + await environment.init() + + return environment } diff --git a/packages/vite/src/node/server/environment.ts b/packages/vite/src/node/server/environment.ts new file mode 100644 index 00000000000000..69d490969c0fed --- /dev/null +++ b/packages/vite/src/node/server/environment.ts @@ -0,0 +1,361 @@ +import type { FetchResult } from 'vite/module-runner' +import type { FSWatcher } from 'dep-types/chokidar' +import colors from 'picocolors' +import { Environment } from '../environment' +import { ERR_OUTDATED_OPTIMIZED_DEP } from '../plugins/optimizedDeps' +import type { + EnvironmentOptions, + ResolvedConfig, + ResolvedEnvironmentOptions, +} from '../config' +import { getDefaultResolvedEnvironmentOptions } from '../config' +import { mergeConfig, promiseWithResolvers } from '../utils' +import type { FetchModuleOptions } from '../ssr/fetchModule' +import { fetchModule } from '../ssr/fetchModule' +import { + createDepsOptimizer, + createExplicitDepsOptimizer, +} from '../optimizer/optimizer' +import { resolveBoundedPlugins } from '../plugin' +import type { DepsOptimizer } from '../optimizer' +import { EnvironmentModuleGraph } from './moduleGraph' +import type { HMRChannel } from './hmr' +import { createNoopHMRChannel, getShortName, updateModules } from './hmr' +import { transformRequest } from './transformRequest' +import type { TransformResult } from './transformRequest' +import { + ERR_CLOSED_SERVER, + createBoundedPluginContainer, +} from './pluginContainer' +import type { RemoteEnvironmentTransport } from './environmentTransport' +import type { BoundedPluginContainer } from './pluginContainer' + +export interface DevEnvironmentSetup { + hot?: false | HMRChannel + watcher?: FSWatcher + options?: EnvironmentOptions + runner?: FetchModuleOptions & { + transport?: RemoteEnvironmentTransport + } + depsOptimizer?: DepsOptimizer +} + +// Maybe we will rename this to DevEnvironment +export class DevEnvironment extends Environment { + mode = 'dev' as const // TODO: should this be 'serve'? + moduleGraph: EnvironmentModuleGraph + + watcher?: FSWatcher + depsOptimizer?: DepsOptimizer + /** + * @internal + */ + _ssrRunnerOptions: FetchModuleOptions | undefined + + get pluginContainer(): BoundedPluginContainer { + if (!this._pluginContainer) + throw new Error( + `${this.name} environment.pluginContainer called before initialized`, + ) + return this._pluginContainer + } + /** + * @internal + */ + _pluginContainer: BoundedPluginContainer | undefined + + /** + * TODO: should this be public? + * @internal + */ + _closing: boolean = false + /** + * @internal + */ + _pendingRequests: Map< + string, + { + request: Promise + timestamp: number + abort: () => void + } + > + /** + * @internal + */ + _onCrawlEndCallbacks: (() => void)[] + /** + * @internal + */ + _crawlEndFinder: CrawlEndFinder + + /** + * HMR channel for this environment. If not provided or disabled, + * it will be a noop channel that does nothing. + * + * @example + * environment.hot.send({ type: 'full-reload' }) + */ + hot: HMRChannel + constructor( + name: string, + config: ResolvedConfig, + setup?: DevEnvironmentSetup, + ) { + let options = + config.environments[name] ?? getDefaultResolvedEnvironmentOptions(config) + if (setup?.options) { + options = mergeConfig( + options, + setup?.options, + ) as ResolvedEnvironmentOptions + } + super(name, config, options) + + this._pendingRequests = new Map() + + this.moduleGraph = new EnvironmentModuleGraph(name, (url: string) => + this.pluginContainer!.resolveId(url, undefined), + ) + + this.hot = setup?.hot || createNoopHMRChannel() + this.watcher = setup?.watcher + + this._onCrawlEndCallbacks = [] + this._crawlEndFinder = setupOnCrawlEnd(() => { + this._onCrawlEndCallbacks.forEach((cb) => cb()) + }) + + const ssrRunnerOptions = setup?.runner || {} + this._ssrRunnerOptions = ssrRunnerOptions + setup?.runner?.transport?.register(this) + + this.hot.on('vite:invalidate', async ({ path, message }) => { + invalidateModule(this, { + path, + message, + }) + }) + + const { optimizeDeps } = this.options.dev + if (setup?.depsOptimizer) { + this.depsOptimizer = setup?.depsOptimizer + } else if ( + optimizeDeps?.noDiscovery && + optimizeDeps?.include?.length === 0 + ) { + this.depsOptimizer = undefined + } else { + // We only support auto-discovery for the client environment, for all other + // environments `noDiscovery` has no effect and an simpler explicit deps + // optimizer is used that only optimizes explicitely included dependencies + // so it doesn't need to reload the environment. Now that we have proper HMR + // and full reload for general environments, we can enable autodiscovery for + // them in the future + this.depsOptimizer = ( + optimizeDeps.noDiscovery || name !== 'client' + ? createExplicitDepsOptimizer + : createDepsOptimizer + )(this) + } + } + + async init(): Promise { + if (this._inited) { + return + } + this._inited = true + this._plugins = await resolveBoundedPlugins(this) + this._pluginContainer = await createBoundedPluginContainer( + this, + this._plugins, + ) + + // TODO: Should buildStart be called here? It break backward compatibility if we do, + // and it may be better to delay it for performance + + // The deps optimizer init is delayed. TODO: add internal option? + + // TODO: move warmup here + } + + fetchModule(id: string, importer?: string): Promise { + return fetchModule(this, id, importer, this._ssrRunnerOptions) + } + + transformRequest(url: string): Promise { + return transformRequest(this, url) + } + + async warmupRequest(url: string): Promise { + await transformRequest(this, url).catch((e) => { + if ( + e?.code === ERR_OUTDATED_OPTIMIZED_DEP || + e?.code === ERR_CLOSED_SERVER + ) { + // these are expected errors + return + } + // Unexpected error, log the issue but avoid an unhandled exception + this.logger.error(`Pre-transform error: ${e.message}`, { + error: e, + timestamp: true, + }) + }) + } + + async close(): Promise { + this._closing = true + + await Promise.allSettled([ + this.pluginContainer.close(), + this._crawlEndFinder?.cancel(), + this.depsOptimizer?.close(), + (async () => { + while (this._pendingRequests.size > 0) { + await Promise.allSettled( + [...this._pendingRequests.values()].map( + (pending) => pending.request, + ), + ) + } + })(), + ]) + } + + /** + * Calling `await environment.waitForRequestsIdle(id)` will wait until all static imports + * are processed after the first transformRequest call. If called from a load or transform + * plugin hook, the id needs to be passed as a parameter to avoid deadlocks. + * Calling this function after the first static imports section of the module graph has been + * processed will resolve immediately. + * @experimental + */ + waitForRequestsIdle(ignoredId?: string): Promise { + return this._crawlEndFinder.waitForRequestsIdle(ignoredId) + } + + /** + * @internal + */ + _registerRequestProcessing(id: string, done: () => Promise): void { + this._crawlEndFinder.registerRequestProcessing(id, done) + } + /** + * @internal + * TODO: use waitForRequestsIdle in the optimizer instead of this function + */ + _onCrawlEnd(cb: () => void): void { + this._onCrawlEndCallbacks.push(cb) + } +} + +function invalidateModule( + environment: DevEnvironment, + m: { + path: string + message?: string + }, +) { + const mod = environment.moduleGraph.urlToModuleMap.get(m.path) + if ( + mod && + mod.isSelfAccepting && + mod.lastHMRTimestamp > 0 && + !mod.lastHMRInvalidationReceived + ) { + mod.lastHMRInvalidationReceived = true + environment.logger.info( + colors.yellow(`hmr invalidate `) + + colors.dim(m.path) + + (m.message ? ` ${m.message}` : ''), + { timestamp: true }, + ) + const file = getShortName(mod.file!, environment.config.root) + updateModules( + environment, + file, + [...mod.importers], + mod.lastHMRTimestamp, + true, + ) + } +} + +const callCrawlEndIfIdleAfterMs = 50 + +interface CrawlEndFinder { + registerRequestProcessing: (id: string, done: () => Promise) => void + waitForRequestsIdle: (ignoredId?: string) => Promise + cancel: () => void +} + +function setupOnCrawlEnd(onCrawlEnd: () => void): CrawlEndFinder { + const registeredIds = new Set() + const seenIds = new Set() + const onCrawlEndPromiseWithResolvers = promiseWithResolvers() + + let timeoutHandle: NodeJS.Timeout | undefined + + let cancelled = false + function cancel() { + cancelled = true + } + + let crawlEndCalled = false + function callOnCrawlEnd() { + if (!cancelled && !crawlEndCalled) { + crawlEndCalled = true + onCrawlEnd() + } + onCrawlEndPromiseWithResolvers.resolve() + } + + function registerRequestProcessing( + id: string, + done: () => Promise, + ): void { + if (!seenIds.has(id)) { + seenIds.add(id) + registeredIds.add(id) + done() + .catch(() => {}) + .finally(() => markIdAsDone(id)) + } + } + + function waitForRequestsIdle(ignoredId?: string): Promise { + if (ignoredId) { + seenIds.add(ignoredId) + markIdAsDone(ignoredId) + } + return onCrawlEndPromiseWithResolvers.promise + } + + function markIdAsDone(id: string): void { + if (registeredIds.has(id)) { + registeredIds.delete(id) + checkIfCrawlEndAfterTimeout() + } + } + + function checkIfCrawlEndAfterTimeout() { + if (cancelled || registeredIds.size > 0) return + + if (timeoutHandle) clearTimeout(timeoutHandle) + timeoutHandle = setTimeout( + callOnCrawlEndWhenIdle, + callCrawlEndIfIdleAfterMs, + ) + } + async function callOnCrawlEndWhenIdle() { + if (cancelled || registeredIds.size > 0) return + callOnCrawlEnd() + } + + return { + registerRequestProcessing, + waitForRequestsIdle, + cancel, + } +} diff --git a/packages/vite/src/node/server/environmentTransport.ts b/packages/vite/src/node/server/environmentTransport.ts new file mode 100644 index 00000000000000..4340c144adc615 --- /dev/null +++ b/packages/vite/src/node/server/environmentTransport.ts @@ -0,0 +1,38 @@ +import type { DevEnvironment } from './environment' + +export class RemoteEnvironmentTransport { + constructor( + private readonly options: { + send: (data: any) => void + onMessage: (handler: (data: any) => void) => void + }, + ) {} + + register(environment: DevEnvironment): void { + this.options.onMessage(async (data) => { + if (typeof data !== 'object' || !data || !data.__v) return + + const method = data.m as 'fetchModule' + const parameters = data.a as [string, string] + + try { + const result = await environment[method](...parameters) + this.options.send({ + __v: true, + r: result, + i: data.i, + }) + } catch (error) { + this.options.send({ + __v: true, + e: { + name: error.name, + message: error.message, + stack: error.stack, + }, + i: data.i, + }) + } + }) + } +} diff --git a/packages/vite/src/node/server/environments/nodeEnvironment.ts b/packages/vite/src/node/server/environments/nodeEnvironment.ts new file mode 100644 index 00000000000000..0e55fb8fe848ac --- /dev/null +++ b/packages/vite/src/node/server/environments/nodeEnvironment.ts @@ -0,0 +1,24 @@ +import type { ResolvedConfig } from '../../config' +import type { DevEnvironmentSetup } from '../environment' +import { DevEnvironment } from '../environment' +import { asyncFunctionDeclarationPaddingLineCount } from '../../../shared/utils' + +export function createNodeDevEnvironment( + name: string, + config: ResolvedConfig, + options?: DevEnvironmentSetup, +): DevEnvironment { + return new DevEnvironment(name, config, { + ...options, + runner: { + processSourceMap(map) { + // this assumes that "new AsyncFunction" is used to create the module + return Object.assign({}, map, { + mappings: + ';'.repeat(asyncFunctionDeclarationPaddingLineCount) + map.mappings, + }) + }, + ...options?.runner, + }, + }) +} diff --git a/packages/vite/src/node/server/hmr.ts b/packages/vite/src/node/server/hmr.ts index f6777838788d7f..7a66166d6f712e 100644 --- a/packages/vite/src/node/server/hmr.ts +++ b/packages/vite/src/node/server/hmr.ts @@ -6,14 +6,19 @@ import colors from 'picocolors' import type { CustomPayload, HMRPayload, Update } from 'types/hmrPayload' import type { RollupError } from 'rollup' import { CLIENT_DIR } from '../constants' +import type { ResolvedConfig } from '../config' import { createDebugger, normalizePath } from '../utils' import type { InferCustomEventPayload, ViteDevServer } from '..' +import { getHookHandler } from '../plugins' import { isCSSRequest } from '../plugins/css' -import { getAffectedGlobModules } from '../plugins/importMetaGlob' import { isExplicitImportRequired } from '../plugins/importAnalysis' import { getEnvFilesForMode } from '../env' import { withTrailingSlash, wrapId } from '../../shared/utils' -import type { ModuleNode } from './moduleGraph' +import type { Plugin } from '../plugin' +import type { EnvironmentModuleNode } from './moduleGraph' +import type { ModuleNode } from './mixedModuleGraph' +import type { DevEnvironment } from './environment' +import { prepareError } from './middlewares/error' import { restartServerWithUrls } from '.' export const debugHmr = createDebugger('vite:hmr') @@ -35,6 +40,20 @@ export interface HmrOptions { channels?: HMRChannel[] } +export interface HotUpdateContext { + type: 'create' | 'update' | 'delete' + file: string + timestamp: number + modules: Array + read: () => string | Promise + server: ViteDevServer + environment: DevEnvironment +} + +/** + * @deprecated + * Used by handleHotUpdate for backward compatibility with mixed client and ssr moduleGraph + **/ export interface HmrContext { file: string timestamp: number @@ -44,8 +63,8 @@ export interface HmrContext { } interface PropagationBoundary { - boundary: ModuleNode - acceptedVia: ModuleNode + boundary: EnvironmentModuleNode + acceptedVia: EnvironmentModuleNode isWithinCircularImport: boolean } @@ -117,12 +136,52 @@ export function getShortName(file: string, root: string): string { : file } +export function getSortedPluginsByHotUpdateHook( + plugins: readonly Plugin[], +): Plugin[] { + const sortedPlugins: Plugin[] = [] + // Use indexes to track and insert the ordered plugins directly in the + // resulting array to avoid creating 3 extra temporary arrays per hook + let pre = 0, + normal = 0, + post = 0 + for (const plugin of plugins) { + const hook = plugin['hotUpdate'] ?? plugin['handleHotUpdate'] + if (hook) { + if (typeof hook === 'object') { + if (hook.order === 'pre') { + sortedPlugins.splice(pre++, 0, plugin) + continue + } + if (hook.order === 'post') { + sortedPlugins.splice(pre + normal + post++, 0, plugin) + continue + } + } + sortedPlugins.splice(pre + normal++, 0, plugin) + } + } + + return sortedPlugins +} + +const sortedHotUpdatePluginsCache = new WeakMap() +function getSortedHotUpdatePlugins(config: ResolvedConfig): Plugin[] { + let sortedPlugins = sortedHotUpdatePluginsCache.get(config) as Plugin[] + if (!sortedPlugins) { + sortedPlugins = getSortedPluginsByHotUpdateHook(config.plugins) + sortedHotUpdatePluginsCache.set(config, sortedPlugins) + } + return sortedPlugins +} + export async function handleHMRUpdate( type: 'create' | 'delete' | 'update', file: string, server: ViteDevServer, ): Promise { - const { hot, config, moduleGraph } = server + const { config } = server + const environments = Object.values(server.environments) const shortFile = getShortName(file, config.root) const isConfig = file === config.configFile @@ -154,87 +213,160 @@ export async function handleHMRUpdate( // (dev only) the client itself cannot be hot updated. if (file.startsWith(withTrailingSlash(normalizedClientDir))) { - hot.send({ - type: 'full-reload', - path: '*', - triggeredBy: path.resolve(config.root, file), - }) + environments.forEach(({ hot }) => + hot.send({ + type: 'full-reload', + path: '*', + triggeredBy: path.resolve(config.root, file), + }), + ) return } - const mods = new Set(moduleGraph.getModulesByFile(file)) - if (type === 'create') { - for (const mod of moduleGraph._hasResolveFailedErrorModules) { - mods.add(mod) - } - } - if (type === 'create' || type === 'delete') { - for (const mod of getAffectedGlobModules(file, server)) { - mods.add(mod) - } - } + // TODO: We should do everything that is here until the end of the function + // for each moduleGraph once SSR is updated to support separate moduleGraphs + // getSSRInvalidatedImporters should be removed. + // The compat hook handleHotUpdate should only be called for the browser + // For now, we only call updateModules for the browser. Later on it should + // also be called for each runtime. - // check if any plugin wants to perform custom HMR handling - const timestamp = Date.now() - const hmrContext: HmrContext = { - file, - timestamp, - modules: [...mods], - read: () => readModifiedFile(file), - server, - } + async function hmr(environment: DevEnvironment) { + try { + const mods = new Set(environment.moduleGraph.getModulesByFile(file)) + if (type === 'create') { + for (const mod of environment.moduleGraph + ._hasResolveFailedErrorModules) { + mods.add(mod) + } + } - if (type === 'update') { - for (const hook of config.getSortedPluginHooks('handleHotUpdate')) { - const filteredModules = await hook(hmrContext) - if (filteredModules) { - hmrContext.modules = filteredModules + // check if any plugin wants to perform custom HMR handling + const timestamp = Date.now() + const hotContext: HotUpdateContext = { + type, + file, + timestamp, + modules: [...mods], + read: () => readModifiedFile(file), + server, + // later on hotUpdate will be called for each runtime with a new hotContext + environment, } - } - } - if (!hmrContext.modules.length) { - // html file cannot be hot updated - if (file.endsWith('.html')) { - config.logger.info(colors.green(`page reload `) + colors.dim(shortFile), { - clear: true, - timestamp: true, - }) - hot.send({ - type: 'full-reload', - path: config.server.middlewareMode - ? '*' - : '/' + normalizePath(path.relative(config.root, file)), + let hmrContext + + for (const plugin of getSortedHotUpdatePlugins(config)) { + if (plugin.hotUpdate) { + const filteredModules = await getHookHandler(plugin.hotUpdate)( + hotContext, + ) + if (filteredModules) { + hotContext.modules = filteredModules + // Invalidate the hmrContext to force compat modules to be updated + hmrContext = undefined + } + } else if (environment.name === 'client' && type === 'update') { + // later on, we'll need: if (runtime === 'client') + // Backward compatibility with mixed client and ssr moduleGraph + hmrContext ??= { + ...hotContext, + modules: hotContext.modules.map((mod) => + server.moduleGraph.getBackwardCompatibleModuleNode(mod), + ), + type: undefined, + } as HmrContext + const filteredModules = await getHookHandler(plugin.handleHotUpdate!)( + hmrContext, + ) + if (filteredModules) { + hmrContext.modules = filteredModules + hotContext.modules = filteredModules + .map((mod) => + mod.id + ? server.environments.client.moduleGraph.getModuleById( + mod.id, + ) ?? + server.environments.ssr.moduleGraph.getModuleById(mod.id) + : undefined, + ) + .filter(Boolean) as EnvironmentModuleNode[] + } + } + } + + if (!hotContext.modules.length) { + // html file cannot be hot updated + if (file.endsWith('.html')) { + environment.logger.info( + colors.green(`page reload `) + colors.dim(shortFile), + { + clear: true, + timestamp: true, + }, + ) + environments.forEach(({ hot }) => + hot.send({ + type: 'full-reload', + path: config.server.middlewareMode + ? '*' + : '/' + normalizePath(path.relative(config.root, file)), + }), + ) + } else { + // loaded but not in the module graph, probably not js + debugHmr?.(`[no modules matched] ${colors.dim(shortFile)}`) + } + return + } + + updateModules(environment, shortFile, hotContext.modules, timestamp) + } catch (err) { + environment.hot.send({ + type: 'error', + err: prepareError(err), }) - } else { - // loaded but not in the module graph, probably not js - debugHmr?.(`[no modules matched] ${colors.dim(shortFile)}`) } - return } - updateModules(shortFile, hmrContext.modules, timestamp, server) + const hotUpdateEnvironments = + server.config.server.hotUpdateEnvironments ?? + ((server, hmr) => { + // Run HMR in parallel for all environments by default + return Promise.all( + Object.values(server.environments).map((environment) => + hmr(environment), + ), + ) + }) + + await hotUpdateEnvironments(server, hmr) } type HasDeadEnd = boolean export function updateModules( + environment: DevEnvironment, file: string, - modules: ModuleNode[], + modules: EnvironmentModuleNode[], timestamp: number, - { config, hot, moduleGraph }: ViteDevServer, afterInvalidation?: boolean, ): void { + const { hot, config } = environment const updates: Update[] = [] - const invalidatedModules = new Set() - const traversedModules = new Set() + const invalidatedModules = new Set() + const traversedModules = new Set() let needFullReload: HasDeadEnd = false for (const mod of modules) { const boundaries: PropagationBoundary[] = [] const hasDeadEnd = propagateUpdate(mod, traversedModules, boundaries) - moduleGraph.invalidateModule(mod, invalidatedModules, timestamp, true) + environment.moduleGraph.invalidateModule( + mod, + invalidatedModules, + timestamp, + true, + ) if (needFullReload) { continue @@ -257,9 +389,6 @@ export function updateModules( ? isExplicitImportRequired(acceptedVia.url) : false, isWithinCircularImport, - // browser modules are invalidated by changing ?t= query, - // but in ssr we control the module system, so we can directly remove them form cache - ssrInvalidates: getSSRInvalidatedImporters(acceptedVia), }), ), ) @@ -270,7 +399,7 @@ export function updateModules( typeof needFullReload === 'string' ? colors.dim(` (${needFullReload})`) : '' - config.logger.info( + environment.logger.info( colors.green(`page reload `) + colors.dim(file) + reason, { clear: !afterInvalidation, timestamp: true }, ) @@ -286,7 +415,7 @@ export function updateModules( return } - config.logger.info( + environment.logger.info( colors.green(`hmr update `) + colors.dim([...new Set(updates.map((u) => u.path))].join(', ')), { clear: !afterInvalidation, timestamp: true }, @@ -297,32 +426,6 @@ export function updateModules( }) } -function populateSSRImporters( - module: ModuleNode, - timestamp: number, - seen: Set = new Set(), -) { - module.ssrImportedModules.forEach((importer) => { - if (seen.has(importer)) { - return - } - if ( - importer.lastHMRTimestamp === timestamp || - importer.lastInvalidationTimestamp === timestamp - ) { - seen.add(importer) - populateSSRImporters(importer, timestamp, seen) - } - }) - return seen -} - -function getSSRInvalidatedImporters(module: ModuleNode) { - return [...populateSSRImporters(module, module.lastHMRTimestamp)].map( - (m) => m.file!, - ) -} - function areAllImportsAccepted( importedBindings: Set, acceptedExports: Set, @@ -336,10 +439,10 @@ function areAllImportsAccepted( } function propagateUpdate( - node: ModuleNode, - traversedModules: Set, + node: EnvironmentModuleNode, + traversedModules: Set, boundaries: PropagationBoundary[], - currentChain: ModuleNode[] = [node], + currentChain: EnvironmentModuleNode[] = [node], ): HasDeadEnd { if (traversedModules.has(node)) { return false @@ -451,10 +554,10 @@ function propagateUpdate( * @param traversedModules The set of modules that have traversed */ function isNodeWithinCircularImports( - node: ModuleNode, - nodeChain: ModuleNode[], - currentChain: ModuleNode[] = [node], - traversedModules = new Set(), + node: EnvironmentModuleNode, + nodeChain: EnvironmentModuleNode[], + currentChain: EnvironmentModuleNode[] = [node], + traversedModules = new Set(), ): boolean { // To help visualize how each parameters work, imagine this import graph: // @@ -524,8 +627,8 @@ function isNodeWithinCircularImports( } export function handlePrunedModules( - mods: Set, - { hot }: ViteDevServer, + mods: Set, + { hot }: DevEnvironment, ): void { // update the disposed modules' hmr timestamp // since if it's re-imported, it should re-apply side effects @@ -806,3 +909,18 @@ export function createServerHMRChannel(): ServerHMRChannel { }, } } + +export function createNoopHMRChannel(): HMRChannel { + function noop() { + // noop + } + + return { + name: 'noop', + send: noop, + on: noop, + off: noop, + listen: noop, + close: noop, + } +} diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index c7a73456187938..2634ae94b9ca90 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -14,8 +14,7 @@ import type { FSWatcher, WatchOptions } from 'dep-types/chokidar' import type { Connect } from 'dep-types/connect' import launchEditorMiddleware from 'launch-editor-middleware' import type { SourceMap } from 'rollup' -import picomatch from 'picomatch' -import type { Matcher } from 'picomatch' +import type { ModuleRunner } from 'vite/module-runner' import type { CommonServerOptions } from '../http' import { httpServerStart, @@ -24,7 +23,7 @@ import { setClientErrorHandler, } from '../http' import type { InlineConfig, ResolvedConfig } from '../config' -import { isDepsOptimizerEnabled, resolveConfig } from '../config' +import { resolveConfig } from '../config' import { diffDnsOrderChange, isInNodeModules, @@ -32,7 +31,6 @@ import { isParentDirectory, mergeConfig, normalizePath, - promiseWithResolvers, resolveHostname, resolveServerUrls, } from '../utils' @@ -41,7 +39,6 @@ import { ssrLoadModule } from '../ssr/ssrModuleLoader' import { ssrFixStacktrace, ssrRewriteStacktrace } from '../ssr/ssrStacktrace' import { ssrTransform } from '../ssr/ssrTransform' import { ERR_OUTDATED_OPTIMIZED_DEP } from '../plugins/optimizedDeps' -import { getDepsOptimizer, initDepsOptimizer } from '../optimizer' import { bindCLIShortcuts } from '../shortcuts' import type { BindCLIShortcutsOptions } from '../shortcuts' import { CLIENT_DIR, DEFAULT_DEV_PORT } from '../constants' @@ -55,8 +52,6 @@ import { } from '../watch' import { initPublicFiles } from '../publicDir' import { getEnvFilesForMode } from '../env' -import type { FetchResult } from '../../runtime/types' -import { ssrFetchModule } from '../ssr/ssrFetchModule' import type { PluginContainer } from './pluginContainer' import { ERR_CLOSED_SERVER, createPluginContainer } from './pluginContainer' import type { WebSocketServer } from './ws' @@ -78,15 +73,15 @@ import { serveStaticMiddleware, } from './middlewares/static' import { timeMiddleware } from './middlewares/time' -import type { ModuleNode } from './moduleGraph' -import { ModuleGraph } from './moduleGraph' +import type { EnvironmentModuleNode } from './moduleGraph' +import { ModuleGraph } from './mixedModuleGraph' +import type { ModuleNode } from './mixedModuleGraph' import { notFoundMiddleware } from './middlewares/notFound' -import { errorMiddleware, prepareError } from './middlewares/error' +import { errorMiddleware } from './middlewares/error' import type { HMRBroadcaster, HmrOptions } from './hmr' import { createHMRBroadcaster, createServerHMRChannel, - getShortName, handleHMRUpdate, updateModules, } from './hmr' @@ -95,6 +90,8 @@ import type { TransformOptions, TransformResult } from './transformRequest' import { transformRequest } from './transformRequest' import { searchForWorkspaceRoot } from './searchRoot' import { warmupFiles } from './warmup' +import { DevEnvironment } from './environment' +import { createNodeDevEnvironment } from './environments/nodeEnvironment' export interface ServerOptions extends CommonServerOptions { /** @@ -104,6 +101,7 @@ export interface ServerOptions extends CommonServerOptions { /** * Warm-up files to transform and cache the results in advance. This improves the * initial page load during server starts and prevents transform waterfalls. + * @deprecated use dev.warmup / environment.ssr.dev.warmup */ warmup?: { /** @@ -147,6 +145,7 @@ export interface ServerOptions extends CommonServerOptions { /** * Pre-transform known direct imports * @default true + * @deprecated use dev.preTransformRequests */ preTransformRequests?: boolean /** @@ -156,10 +155,19 @@ export interface ServerOptions extends CommonServerOptions { * By default, it excludes all paths containing `node_modules`. You can pass `false` to * disable this behavior, or, for full control, a function that takes the source path and * sourcemap path and returns whether to ignore the source path. + * @deprecated use dev.sourcemapIgnoreList */ sourcemapIgnoreList?: | false | ((sourcePath: string, sourcemapPath: string) => boolean) + /** + * Run HMR tasks, by default the HMR propagation is done in parallel for all environments + * @experimental + */ + hotUpdateEnvironments?: ( + server: ViteDevServer, + hmr: (environment: DevEnvironment) => Promise, + ) => Promise } export interface ResolvedServerOptions @@ -250,15 +258,22 @@ export interface ViteDevServer { * * Always sends a message to at least a WebSocket client. Any third party can * add a channel to the broadcaster to process messages + * @deprecated use `environment.hot` instead */ hot: HMRBroadcaster /** * Rollup plugin container that can run plugin hooks on a given file + * @deprecated use `environment.pluginContainer` instead */ pluginContainer: PluginContainer + /** + * Module execution environments attached to the Vite server. + */ + environments: Record<'client' | 'ssr' | (string & {}), DevEnvironment> /** * Module graph that tracks the import relationships, url to file mapping * and hmr state. + * @deprecated use `environment.moduleGraph` instead */ moduleGraph: ModuleGraph /** @@ -269,6 +284,7 @@ export interface ViteDevServer { /** * Programmatically resolve, load and transform a URL and get the result * without going through the http request pipeline. + * @deprecated use environment.transformRequest */ transformRequest( url: string, @@ -278,6 +294,7 @@ export interface ViteDevServer { * Same as `transformRequest` but only warm up the URLs so the next request * will already be cached. The function will never throw as it handles and * reports errors internally. + * @deprecated use environment.warmupRequest */ warmupRequest(url: string, options?: TransformOptions): Promise /** @@ -290,6 +307,7 @@ export interface ViteDevServer { ): Promise /** * Transform module code into SSR format. + * TODO: expose this to any environment? */ ssrTransform( code: string, @@ -304,11 +322,6 @@ export interface ViteDevServer { url: string, opts?: { fixStacktrace?: boolean }, ): Promise> - /** - * Fetch information about the module for Vite SSR runtime. - * @experimental - */ - ssrFetchModule(id: string, importer?: string): Promise /** * Returns a fixed version of the given stack */ @@ -322,6 +335,11 @@ export interface ViteDevServer { * API to retrieve the module to be reloaded. If `hmr` is false, this is a no-op. */ reloadModule(module: ModuleNode): Promise + /** + * Triggers HMR for an environment module in the module graph. + * If `hmr` is false, this is a no-op. + */ + reloadEnvironmentModule(module: EnvironmentModuleNode): Promise /** * Start the server. */ @@ -344,7 +362,6 @@ export interface ViteDevServer { * @param forceOptimize - force the optimizer to re-bundle, same as --force cli flag */ restart(forceOptimize?: boolean): Promise - /** * Open browser */ @@ -355,24 +372,13 @@ export interface ViteDevServer { * passed as a parameter to avoid deadlocks. Calling this function after the first * static imports section of the module graph has been processed will resolve immediately. * @experimental + * @deprecated use environment.waitForRequestsIdle() */ waitForRequestsIdle: (ignoredId?: string) => Promise - /** - * @internal - */ - _registerRequestProcessing: (id: string, done: () => Promise) => void - /** - * @internal - */ - _onCrawlEnd(cb: () => void): void /** * @internal */ _setInternalServer(server: ViteDevServer): void - /** - * @internal - */ - _importGlobMap: Map /** * @internal */ @@ -381,21 +387,6 @@ export interface ViteDevServer { * @internal */ _forceOptimizeOnRestart: boolean - /** - * @internal - */ - _pendingRequests: Map< - string, - { - request: Promise - timestamp: number - abort: () => void - } - > - /** - * @internal - */ - _fsDenyGlob: Matcher /** * @internal */ @@ -408,6 +399,10 @@ export interface ViteDevServer { * @internal */ _configServerPort?: number | undefined + /** + * @internal + */ + _ssrCompatModuleRunner?: ModuleRunner } export interface ResolvedServerUrls { @@ -459,9 +454,8 @@ export async function _createServer( : await resolveHttpServer(serverConfig, middlewares, httpsOptions) const ws = createWebSocketServer(httpServer, config, httpsOptions) - const hot = createHMRBroadcaster() - .addChannel(ws) - .addChannel(createServerHMRChannel()) + const ssrHotChannel = createServerHMRChannel() + const hot = createHMRBroadcaster().addChannel(ws).addChannel(ssrHotChannel) if (typeof config.server.hmr === 'object' && config.server.hmr.channels) { config.server.hmr.channels.forEach((channel) => hot.addChannel(channel)) } @@ -484,40 +478,67 @@ export async function _createServer( ) as FSWatcher) : createNoopWatcher(resolvedWatchOptions) - const moduleGraph: ModuleGraph = new ModuleGraph((url, ssr) => - container.resolveId(url, undefined, { ssr }), - ) + const environments: Record = {} + + const client_createEnvironment = + config.environments.client?.dev?.createEnvironment ?? + ((name: string, config: ResolvedConfig) => + new DevEnvironment(name, config, { hot: ws, watcher })) + + environments.client = await client_createEnvironment('client', config) + + const ssr_createEnvironment = + config.environments.ssr?.dev?.createEnvironment ?? + ((name: string, config: ResolvedConfig) => + createNodeDevEnvironment(name, config, { hot: ssrHotChannel, watcher })) + + environments.ssr = await ssr_createEnvironment('ssr', config) + + for (const [name, EnvironmentOptions] of Object.entries( + config.environments, + )) { + // TODO: move client and ssr inside the loop? + if (name !== 'client' && name !== 'ssr') { + const createEnvironment = + EnvironmentOptions.dev?.createEnvironment ?? + ((name: string, config: ResolvedConfig) => + new DevEnvironment(name, config, { + hot: ws, // TODO: what should we use here? + })) + environments[name] = await createEnvironment(name, config) + } + } + + for (const environment of Object.values(environments)) { + await environment.init() + } + + // Backward compatibility + + const moduleGraph = new ModuleGraph({ + client: () => environments.client.moduleGraph, + ssr: () => environments.ssr.moduleGraph, + }) + const pluginContainer = createPluginContainer(environments) - const container = await createPluginContainer(config, moduleGraph, watcher) const closeHttpServer = createServerCloseFn(httpServer) let exitProcess: () => void const devHtmlTransformFn = createDevHtmlTransformFn(config) - const onCrawlEndCallbacks: (() => void)[] = [] - const crawlEndFinder = setupOnCrawlEnd(() => { - onCrawlEndCallbacks.forEach((cb) => cb()) - }) - function waitForRequestsIdle(ignoredId?: string): Promise { - return crawlEndFinder.waitForRequestsIdle(ignoredId) - } - function _registerRequestProcessing(id: string, done: () => Promise) { - crawlEndFinder.registerRequestProcessing(id, done) - } - function _onCrawlEnd(cb: () => void) { - onCrawlEndCallbacks.push(cb) - } - let server: ViteDevServer = { config, middlewares, httpServer, watcher, - pluginContainer: container, ws, hot, + + environments, + pluginContainer, moduleGraph, + resolvedUrls: null, // will be set on listen ssrTransform( code: string, @@ -527,12 +548,19 @@ export async function _createServer( ) { return ssrTransform(code, inMap, url, originalCode, server.config) }, + // environment.transformRequest and .warmupRequest don't take an options param for now, + // so the logic and error handling needs to be duplicated here. + // The only param in options that could be important is `html`, but we may remove it as + // that is part of the internal control flow for the vite dev server to be able to bail + // out and do the html fallback transformRequest(url, options) { - return transformRequest(url, server, options) + const environment = server.environments[options?.ssr ? 'ssr' : 'client'] + return transformRequest(environment, url, options) }, async warmupRequest(url, options) { try { - await transformRequest(url, server, options) + const environment = server.environments[options?.ssr ? 'ssr' : 'client'] + await transformRequest(environment, url, options) } catch (e) { if ( e?.code === ERR_OUTDATED_OPTIMIZED_DEP || @@ -560,18 +588,33 @@ export async function _createServer( opts?.fixStacktrace, ) }, - async ssrFetchModule(url: string, importer?: string) { - return ssrFetchModule(server, url, importer) - }, ssrFixStacktrace(e) { - ssrFixStacktrace(e, moduleGraph) + ssrFixStacktrace(e, server.environments.ssr.moduleGraph) }, ssrRewriteStacktrace(stack: string) { - return ssrRewriteStacktrace(stack, moduleGraph) + return ssrRewriteStacktrace(stack, server.environments.ssr.moduleGraph) }, async reloadModule(module) { if (serverConfig.hmr !== false && module.file) { - updateModules(module.file, [module], Date.now(), server) + // TODO: Should we also update the node moduleGraph for backward compatibility? + const environmentModule = (module._clientModule ?? module._ssrModule)! + updateModules( + environments[environmentModule.environment]!, + module.file, + [environmentModule], + Date.now(), + ) + } + }, + async reloadEnvironmentModule(module) { + // TODO: Should this be reloadEnvironmentModule(environment, module) ? + if (serverConfig.hmr !== false && module.file) { + updateModules( + environments[module.environment]!, + module.file, + [module], + Date.now(), + ) } }, async listen(port?: number, isRestart?: boolean) { @@ -638,28 +681,17 @@ export async function _createServer( process.stdin.off('end', exitProcess) } } + await Promise.allSettled([ watcher.close(), hot.close(), - container.close(), - crawlEndFinder?.cancel(), - getDepsOptimizer(server.config)?.close(), - getDepsOptimizer(server.config, true)?.close(), + Promise.allSettled( + Object.values(server.environments).map((environment) => + environment.close(), + ), + ), closeHttpServer(), ]) - // Await pending requests. We throw early in transformRequest - // and in hooks if the server is closing for non-ssr requests, - // so the import analysis plugin stops pre-transforming static - // imports and this block is resolved sooner. - // During SSR, we let pending requests finish to avoid exposing - // the server closed error to the users. - while (server._pendingRequests.size > 0) { - await Promise.allSettled( - [...server._pendingRequests.values()].map( - (pending) => pending.request, - ), - ) - } server.resolvedUrls = null }, printUrls() { @@ -691,9 +723,9 @@ export async function _createServer( return server._restartPromise }, - waitForRequestsIdle, - _registerRequestProcessing, - _onCrawlEnd, + waitForRequestsIdle(ignoredId?: string): Promise { + return environments.client.waitForRequestsIdle(ignoredId) + }, _setInternalServer(_server: ViteDevServer) { // Rebind internal the server variable so functions reference the user @@ -701,22 +733,7 @@ export async function _createServer( server = _server }, _restartPromise: null, - _importGlobMap: new Map(), _forceOptimizeOnRestart: false, - _pendingRequests: new Map(), - _fsDenyGlob: picomatch( - // matchBase: true does not work as it's documented - // https://github.com/micromatch/picomatch/issues/89 - // convert patterns without `/` on our side for now - config.server.fs.deny.map((pattern) => - pattern.includes('/') ? pattern : `**/${pattern}`, - ), - { - matchBase: false, - nocase: true, - dot: true, - }, - ), _shortcutsOptions: undefined, } @@ -752,14 +769,7 @@ export async function _createServer( file: string, ) => { if (serverConfig.hmr !== false) { - try { - await handleHMRUpdate(type, file, server) - } catch (err) { - hot.send({ - type: 'error', - err: prepareError(err), - }) - } + await handleHMRUpdate(type, file, server) } } @@ -767,32 +777,43 @@ export async function _createServer( const onFileAddUnlink = async (file: string, isUnlink: boolean) => { file = normalizePath(file) - await container.watchChange(file, { event: isUnlink ? 'delete' : 'create' }) + await pluginContainer.watchChange(file, { + event: isUnlink ? 'delete' : 'create', + }) if (publicDir && publicFiles) { if (file.startsWith(publicDir)) { const path = file.slice(publicDir.length) publicFiles[isUnlink ? 'delete' : 'add'](path) if (!isUnlink) { - const moduleWithSamePath = await moduleGraph.getModuleByUrl(path) + const clientModuleGraph = server.environments.client.moduleGraph + const moduleWithSamePath = + await clientModuleGraph.getModuleByUrl(path) const etag = moduleWithSamePath?.transformResult?.etag if (etag) { // The public file should win on the next request over a module with the // same path. Prevent the transform etag fast path from serving the module - moduleGraph.etagToModuleMap.delete(etag) + clientModuleGraph.etagToModuleMap.delete(etag) } } } } - if (isUnlink) moduleGraph.onFileDelete(file) + if (isUnlink) { + // invalidate module graph cache on file change + for (const environment of Object.values(server.environments)) { + environment.moduleGraph.onFileDelete(file) + } + } await onHMRUpdate(isUnlink ? 'delete' : 'create', file) } watcher.on('change', async (file) => { file = normalizePath(file) - await container.watchChange(file, { event: 'update' }) + await pluginContainer.watchChange(file, { event: 'update' }) // invalidate module graph cache on file change - moduleGraph.onFileChange(file) + for (const environment of Object.values(server.environments)) { + environment.moduleGraph.onFileChange(file) + } await onHMRUpdate('update', file) }) @@ -805,32 +826,6 @@ export async function _createServer( onFileAddUnlink(file, true) }) - hot.on('vite:invalidate', async ({ path, message }) => { - const mod = moduleGraph.urlToModuleMap.get(path) - if ( - mod && - mod.isSelfAccepting && - mod.lastHMRTimestamp > 0 && - !mod.lastHMRInvalidationReceived - ) { - mod.lastHMRInvalidationReceived = true - config.logger.info( - colors.yellow(`hmr invalidate `) + - colors.dim(path) + - (message ? ` ${message}` : ''), - { timestamp: true }, - ) - const file = getShortName(mod.file!, config.root) - updateModules( - file, - [...mod.importers], - mod.lastHMRTimestamp, - server, - true, - ) - } - }) - if (!middlewareMode && httpServer) { httpServer.once('listening', () => { // update actual port since this may be different from initial value @@ -936,11 +931,18 @@ export async function _createServer( if (initingServer) return initingServer initingServer = (async function () { - await container.buildStart({}) - // start deps optimizer after all container plugins are ready - if (isDepsOptimizerEnabled(config, false)) { - await initDepsOptimizer(config, server) - } + // TODO: Build start should be called for all environments + // The ecosystem and our tests expects a single call. We need to + // check how to do this change to be backward compatible + await server.environments.client.pluginContainer.buildStart({}) + + await Promise.all( + Object.values(server.environments).map((environment) => + environment.depsOptimizer?.init(), + ), + ) + + // TODO: move warmup call inside environment init() warmupFiles(server) initingServer = undefined serverInited = true @@ -1049,6 +1051,7 @@ export function resolveServerOptions( raw: ServerOptions | undefined, logger: Logger, ): ResolvedServerOptions { + // TODO: deprecated server options moved to the dev config const server: ResolvedServerOptions = { preTransformRequests: true, ...(raw as Omit), @@ -1133,7 +1136,7 @@ async function restartServer(server: ViteDevServer) { // server instance and set the user instance to be used in the new server. // This allows us to keep the same server instance for the user. { - let newServer = null + let newServer: ViteDevServer | null = null try { // delay ws server listen newServer = await _createServer(inlineConfig, { hotListen: false }) @@ -1208,81 +1211,3 @@ export async function restartServerWithUrls( server.printUrls() } } - -const callCrawlEndIfIdleAfterMs = 50 - -interface CrawlEndFinder { - registerRequestProcessing: (id: string, done: () => Promise) => void - waitForRequestsIdle: (ignoredId?: string) => Promise - cancel: () => void -} - -function setupOnCrawlEnd(onCrawlEnd: () => void): CrawlEndFinder { - const registeredIds = new Set() - const seenIds = new Set() - const onCrawlEndPromiseWithResolvers = promiseWithResolvers() - - let timeoutHandle: NodeJS.Timeout | undefined - - let cancelled = false - function cancel() { - cancelled = true - } - - let crawlEndCalled = false - function callOnCrawlEnd() { - if (!cancelled && !crawlEndCalled) { - crawlEndCalled = true - onCrawlEnd() - } - onCrawlEndPromiseWithResolvers.resolve() - } - - function registerRequestProcessing( - id: string, - done: () => Promise, - ): void { - if (!seenIds.has(id)) { - seenIds.add(id) - registeredIds.add(id) - done() - .catch(() => {}) - .finally(() => markIdAsDone(id)) - } - } - - function waitForRequestsIdle(ignoredId?: string): Promise { - if (ignoredId) { - seenIds.add(ignoredId) - markIdAsDone(ignoredId) - } - return onCrawlEndPromiseWithResolvers.promise - } - - function markIdAsDone(id: string): void { - if (registeredIds.has(id)) { - registeredIds.delete(id) - checkIfCrawlEndAfterTimeout() - } - } - - function checkIfCrawlEndAfterTimeout() { - if (cancelled || registeredIds.size > 0) return - - if (timeoutHandle) clearTimeout(timeoutHandle) - timeoutHandle = setTimeout( - callOnCrawlEndWhenIdle, - callCrawlEndIfIdleAfterMs, - ) - } - async function callOnCrawlEndWhenIdle() { - if (cancelled || registeredIds.size > 0) return - callOnCrawlEnd() - } - - return { - registerRequestProcessing, - waitForRequestsIdle, - cancel, - } -} diff --git a/packages/vite/src/node/server/middlewares/error.ts b/packages/vite/src/node/server/middlewares/error.ts index 1d67f1aa55e4ed..a4de10dc8c8d17 100644 --- a/packages/vite/src/node/server/middlewares/error.ts +++ b/packages/vite/src/node/server/middlewares/error.ts @@ -51,7 +51,7 @@ export function logError(server: ViteDevServer, err: RollupError): void { error: err, }) - server.hot.send({ + server.environments.client.hot.send({ type: 'error', err: prepareError(err), }) diff --git a/packages/vite/src/node/server/middlewares/indexHtml.ts b/packages/vite/src/node/server/middlewares/indexHtml.ts index b5893dd072b972..b88a9c4185ff61 100644 --- a/packages/vite/src/node/server/middlewares/indexHtml.ts +++ b/packages/vite/src/node/server/middlewares/indexHtml.ts @@ -130,8 +130,8 @@ const processNodeUrl = ( ): string => { // prefix with base (dev only, base is never relative) const replacer = (url: string) => { - if (server?.moduleGraph) { - const mod = server.moduleGraph.urlToModuleMap.get(url) + if (server) { + const mod = server.environments.client.moduleGraph.urlToModuleMap.get(url) if (mod && mod.lastHMRTimestamp > 0) { url = injectQuery(url, `t=${mod.lastHMRTimestamp}`) } @@ -182,7 +182,7 @@ const devHtmlHook: IndexHtmlTransformHook = async ( html, { path: htmlPath, filename, server, originalUrl }, ) => { - const { config, moduleGraph, watcher } = server! + const { config, watcher } = server! const base = config.base || '/' let proxyModulePath: string @@ -243,9 +243,10 @@ const devHtmlHook: IndexHtmlTransformHook = async ( const modulePath = `${proxyModuleUrl}?html-proxy&index=${inlineModuleIndex}.${ext}` // invalidate the module so the newly cached contents will be served - const module = server?.moduleGraph.getModuleById(modulePath) + const clientModuleGraph = server?.environments.client.moduleGraph + const module = clientModuleGraph?.getModuleById(modulePath) if (module) { - server?.moduleGraph.invalidateModule(module) + clientModuleGraph!.invalidateModule(module) } s.update( node.sourceCodeLocation!.startOffset, @@ -351,10 +352,16 @@ const devHtmlHook: IndexHtmlTransformHook = async ( const url = `${proxyModulePath}?html-proxy&direct&index=${index}.css` // ensure module in graph after successful load - const mod = await moduleGraph.ensureEntryFromUrl(url, false) + const mod = + await server!.environments.client.moduleGraph.ensureEntryFromUrl( + url, + false, + ) ensureWatchedFile(watcher, mod.file, config.root) - const result = await server!.pluginContainer.transform(code, mod.id!) + const result = await server!.pluginContainer.transform(code, mod.id!, { + environment: server!.environments.client, + }) let content = '' if (result) { if (result.map && 'version' in result.map) { @@ -376,10 +383,16 @@ const devHtmlHook: IndexHtmlTransformHook = async ( // will transform with css plugin and cache result with css-post plugin const url = `${proxyModulePath}?html-proxy&inline-css&style-attr&index=${index}.css` - const mod = await moduleGraph.ensureEntryFromUrl(url, false) + const mod = + await server!.environments.client.moduleGraph.ensureEntryFromUrl( + url, + false, + ) ensureWatchedFile(watcher, mod.file, config.root) - await server?.pluginContainer.transform(code, mod.id!) + await server?.pluginContainer.transform(code, mod.id!, { + environment: server!.environments.client, + }) const hash = getHash(cleanUrl(mod.id!)) const result = htmlProxyResult.get(`${hash}_${index}`) diff --git a/packages/vite/src/node/server/middlewares/static.ts b/packages/vite/src/node/server/middlewares/static.ts index d706b3fa926fee..3795e10a440849 100644 --- a/packages/vite/src/node/server/middlewares/static.ts +++ b/packages/vite/src/node/server/middlewares/static.ts @@ -2,9 +2,12 @@ import path from 'node:path' import type { OutgoingHttpHeaders, ServerResponse } from 'node:http' import type { Options } from 'sirv' import sirv from 'sirv' +import picomatch from 'picomatch' +import type { Matcher } from 'picomatch' import type { Connect } from 'dep-types/connect' import escapeHtml from 'escape-html' -import type { ViteDevServer } from '../..' +import type { ViteDevServer } from '../../server' +import type { ResolvedConfig } from '../../config' import { FS_PREFIX } from '../../constants' import { fsPathFromId, @@ -204,27 +207,80 @@ export function serveRawFsMiddleware( } } +const safeModulePathsCache = new WeakMap>() +function isSafeModulePath(config: ResolvedConfig, filePath: string) { + let safeModulePaths = safeModulePathsCache.get(config) + if (!safeModulePaths) { + safeModulePaths = new Set() + safeModulePathsCache.set(config, safeModulePaths) + } + return safeModulePaths.has(filePath) +} +export function addSafeModulePath( + config: ResolvedConfig, + filePath: string, +): void { + let safeModulePaths = safeModulePathsCache.get(config) + if (!safeModulePaths) { + safeModulePaths = new Set() + safeModulePathsCache.set(config, safeModulePaths) + } + safeModulePaths.add(filePath) +} + +const fsDenyGlobCache = new WeakMap() +function fsDenyGlob(config: ResolvedConfig, filePath: string): boolean { + let matcher = fsDenyGlobCache.get(config) + if (!matcher) { + ;(matcher = picomatch( + // matchBase: true does not work as it's documented + // https://github.com/micromatch/picomatch/issues/89 + // convert patterns without `/` on our side for now + config.server.fs.deny.map((pattern) => + pattern.includes('/') ? pattern : `**/${pattern}`, + ), + { + matchBase: false, + nocase: true, + dot: true, + }, + )), + fsDenyGlobCache.set(config, matcher) + } + return matcher(filePath) +} + /** * Check if the url is allowed to be served, via the `server.fs` config. + * @deprecate use isFileLoadingAllowed */ export function isFileServingAllowed( url: string, server: ViteDevServer, ): boolean { - if (!server.config.server.fs.strict) return true + const { config } = server + if (!config.server.fs.strict) return true + const filePath = fsPathFromUrl(url) + return isFileLoadingAllowed(config, filePath) +} - const file = fsPathFromUrl(url) +function isUriInFilePath(uri: string, filePath: string) { + return isSameFileUri(uri, filePath) || isParentDirectory(uri, filePath) +} - if (server._fsDenyGlob(file)) return false +export function isFileLoadingAllowed( + config: ResolvedConfig, + filePath: string, +): boolean { + const { fs } = config.server - if (server.moduleGraph.safeModulesPath.has(file)) return true + if (!fs.strict) return true - if ( - server.config.server.fs.allow.some( - (uri) => isSameFileUri(uri, file) || isParentDirectory(uri, file), - ) - ) - return true + if (fsDenyGlob(config, filePath)) return false + + if (isSafeModulePath(config, filePath)) return true + + if (fs.allow.some((uri) => isUriInFilePath(uri, filePath))) return true return false } diff --git a/packages/vite/src/node/server/middlewares/transform.ts b/packages/vite/src/node/server/middlewares/transform.ts index 12a440d4c10774..df20cd4a3c429d 100644 --- a/packages/vite/src/node/server/middlewares/transform.ts +++ b/packages/vite/src/node/server/middlewares/transform.ts @@ -32,7 +32,6 @@ import { ERR_OUTDATED_OPTIMIZED_DEP, } from '../../plugins/optimizedDeps' import { ERR_CLOSED_SERVER } from '../pluginContainer' -import { getDepsOptimizer } from '../../optimizer' import { cleanUrl, unwrapId, withTrailingSlash } from '../../../shared/utils' import { NULL_BYTE_PLACEHOLDER } from '../../../shared/constants' @@ -48,10 +47,12 @@ export function cachedTransformMiddleware( ): Connect.NextHandleFunction { // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...` return function viteCachedTransformMiddleware(req, res, next) { + const environment = server.environments.client + // check if we can return 304 early const ifNoneMatch = req.headers['if-none-match'] if (ifNoneMatch) { - const moduleByEtag = server.moduleGraph.getModuleByEtag(ifNoneMatch) + const moduleByEtag = environment.moduleGraph.getModuleByEtag(ifNoneMatch) if (moduleByEtag?.transformResult?.etag === ifNoneMatch) { // For CSS requests, if the same CSS file is imported in a module, // the browser sends the request for the direct CSS request with the etag @@ -80,6 +81,9 @@ export function transformMiddleware( const publicPath = `${publicDir.slice(root.length)}/` return async function viteTransformMiddleware(req, res, next) { + // TODO: We could do this for all browser like environments, and avoid the harcoded environments.client here + const environment = server.environments.client + if (req.method !== 'GET' || knownIgnoreList.has(req.url!)) { return next() } @@ -100,7 +104,7 @@ export function transformMiddleware( const isSourceMap = withoutQuery.endsWith('.map') // since we generate source map references, handle those requests here if (isSourceMap) { - const depsOptimizer = getDepsOptimizer(server.config, false) // non-ssr + const depsOptimizer = environment.depsOptimizer if (depsOptimizer?.isOptimizedDepUrl(url)) { // If the browser is requesting a source map for an optimized dep, it // means that the dependency has already been pre-bundled and loaded @@ -142,7 +146,7 @@ export function transformMiddleware( } else { const originalUrl = url.replace(/\.map($|\?)/, '$1') const map = ( - await server.moduleGraph.getModuleByUrl(originalUrl, false) + await environment.moduleGraph.getModuleByUrl(originalUrl) )?.transformResult?.map if (map) { return send(req, res, JSON.stringify(map), 'json', { @@ -185,8 +189,8 @@ export function transformMiddleware( const ifNoneMatch = req.headers['if-none-match'] if ( ifNoneMatch && - (await server.moduleGraph.getModuleByUrl(url, false)) - ?.transformResult?.etag === ifNoneMatch + (await environment.moduleGraph.getModuleByUrl(url))?.transformResult + ?.etag === ifNoneMatch ) { debugCache?.(`[304] ${prettifyUrl(url, server.config.root)}`) res.statusCode = 304 @@ -195,11 +199,11 @@ export function transformMiddleware( } // resolve, load and transform using the plugin container - const result = await transformRequest(url, server, { + const result = await transformRequest(environment, url, { html: req.headers.accept?.includes('text/html'), }) if (result) { - const depsOptimizer = getDepsOptimizer(server.config, false) // non-ssr + const depsOptimizer = environment.depsOptimizer const type = isDirectCSSRequest(url) ? 'css' : 'js' const isDep = DEP_VERSION_RE.test(url) || depsOptimizer?.isOptimizedDepUrl(url) @@ -264,6 +268,7 @@ export function transformMiddleware( return } if (e?.code === ERR_LOAD_URL) { + // TODO: Why not also do this on ERR_LOAD_PUBLIC_URL? // Let other middleware handle if we can't load the url via transformRequest return next() } diff --git a/packages/vite/src/node/server/mixedModuleGraph.ts b/packages/vite/src/node/server/mixedModuleGraph.ts new file mode 100644 index 00000000000000..7bc2b8b7112147 --- /dev/null +++ b/packages/vite/src/node/server/mixedModuleGraph.ts @@ -0,0 +1,645 @@ +import type { ModuleInfo } from 'rollup' +import type { TransformResult } from './transformRequest' +import type { + EnvironmentModuleGraph, + EnvironmentModuleNode, + ResolvedUrl, +} from './moduleGraph' + +/** + * Backward compatible ModuleNode and ModuleGraph with mixed nodes from both the client and ssr enviornments + * It would be good to take the types names for the new EnvironmentModuleNode and EnvironmentModuleGraph but we can't + * do that at this point without breaking to much code in the ecosystem. + * We are going to deprecate these types and we can try to use them back in the future. + */ + +export class ModuleNode { + _moduleGraph: ModuleGraph + _clientModule: EnvironmentModuleNode | undefined + _ssrModule: EnvironmentModuleNode | undefined + constructor( + moduleGraph: ModuleGraph, + clientModule?: EnvironmentModuleNode, + ssrModule?: EnvironmentModuleNode, + ) { + this._moduleGraph = moduleGraph + this._clientModule = clientModule + this._ssrModule = ssrModule + } + _get( + prop: T, + ): EnvironmentModuleNode[T] { + return (this._clientModule?.[prop] ?? this._ssrModule?.[prop])! + } + _wrapModuleSet( + prop: ModuleSetNames, + module: EnvironmentModuleNode | undefined, + ): Set { + if (!module) { + return new Set() + } + return createBackwardCompatibleModuleSet(this._moduleGraph, prop, module) + } + _getModuleSetUnion(prop: 'importedModules' | 'importers'): Set { + // A good approximation to the previous logic that returned the union of + // the importedModules and importers from both the browser and server + const importedModules = new Set() + const ids = new Set() + if (this._clientModule) { + for (const mod of this._clientModule[prop]) { + if (mod.id) ids.add(mod.id) + importedModules.add( + this._moduleGraph.getBackwardCompatibleModuleNode(mod), + ) + } + } + if (this._ssrModule) { + for (const mod of this._ssrModule[prop]) { + if (mod.id && !ids.has(mod.id)) { + importedModules.add( + this._moduleGraph.getBackwardCompatibleModuleNode(mod), + ) + } + } + } + return importedModules + } + get url(): string { + return this._get('url') + } + get id(): string | null { + return this._get('id') + } + get file(): string | null { + return this._get('file') + } + get type(): 'js' | 'css' { + return this._get('type') + } + get info(): ModuleInfo | undefined { + return this._get('info') + } + get meta(): Record | undefined { + return this._get('meta') + } + get importers(): Set { + return this._getModuleSetUnion('importers') + } + get clientImportedModules(): Set { + return this._wrapModuleSet('importedModules', this._clientModule) + } + get ssrImportedModules(): Set { + return this._wrapModuleSet('importedModules', this._ssrModule) + } + get importedModules(): Set { + return this._getModuleSetUnion('importedModules') + } + get acceptedHmrDeps(): Set { + return this._wrapModuleSet('acceptedHmrDeps', this._clientModule) + } + get acceptedHmrExports(): Set | null { + return this._clientModule?.acceptedHmrExports ?? null + } + get importedBindings(): Map> | null { + return this._clientModule?.importedBindings ?? null + } + get isSelfAccepting(): boolean | undefined { + return this._clientModule?.isSelfAccepting + } + get transformResult(): TransformResult | null { + return this._clientModule?.transformResult ?? null + } + set transformResult(value: TransformResult | null) { + if (this._clientModule) { + this._clientModule.transformResult = value + } + } + get ssrTransformResult(): TransformResult | null { + return this._ssrModule?.transformResult ?? null + } + set ssrTransformResult(value: TransformResult | null) { + if (this._ssrModule) { + this._ssrModule.transformResult = value + } + } + get ssrModule(): Record | null { + return this._ssrModule?.ssrModule ?? null + } + get ssrError(): Error | null { + return this._ssrModule?.ssrError ?? null + } + get lastHMRTimestamp(): number { + return this._clientModule?.lastHMRTimestamp ?? 0 + } + set lastHMRTimestamp(value: number) { + if (this._clientModule) { + this._clientModule.lastHMRTimestamp = value + } + } + get lastInvalidationTimestamp(): number { + return this._clientModule?.lastInvalidationTimestamp ?? 0 + } + get invalidationState(): TransformResult | 'HARD_INVALIDATED' | undefined { + return this._clientModule?.invalidationState + } + get ssrInvalidationState(): TransformResult | 'HARD_INVALIDATED' | undefined { + return this._ssrModule?.invalidationState + } +} + +function mapIterator( + iterable: IterableIterator, + transform: (value: T) => K, +): IterableIterator { + return { + [Symbol.iterator](): IterableIterator { + return this + }, + next(): IteratorResult { + const r = iterable.next() + return r.done + ? r + : { + value: transform(r.value), + done: false, + } + }, + } +} + +export class ModuleGraph { + /** @internal */ + _moduleGraphs: { + client: () => EnvironmentModuleGraph + ssr: () => EnvironmentModuleGraph + } + + /** @internal */ + get _client(): EnvironmentModuleGraph { + return this._moduleGraphs.client() + } + + /** @internal */ + get _ssr(): EnvironmentModuleGraph { + return this._moduleGraphs.ssr() + } + + urlToModuleMap: Map + idToModuleMap: Map + etagToModuleMap: Map + + fileToModulesMap: Map> + + constructor(moduleGraphs: { + client: () => EnvironmentModuleGraph + ssr: () => EnvironmentModuleGraph + }) { + this._moduleGraphs = moduleGraphs + + const getModuleMapUnion = + (prop: 'urlToModuleMap' | 'idToModuleMap') => () => { + // A good approximation to the previous logic that returned the union of + // the importedModules and importers from both the browser and server + if (this._ssr[prop].size === 0) { + return this._client[prop] + } + const map = new Map(this._client[prop]) + for (const [key, module] of this._ssr[prop]) { + if (!map.has(key)) { + map.set(key, module) + } + } + return map + } + + this.urlToModuleMap = createBackwardCompatibleModuleMap( + this, + 'urlToModuleMap', + getModuleMapUnion('urlToModuleMap'), + ) + this.idToModuleMap = createBackwardCompatibleModuleMap( + this, + 'idToModuleMap', + getModuleMapUnion('idToModuleMap'), + ) + this.etagToModuleMap = createBackwardCompatibleModuleMap( + this, + 'etagToModuleMap', + () => this._client.etagToModuleMap, + ) + this.fileToModulesMap = createBackwardCompatibleFileToModulesMap(this) + } + + /** @deprecated */ + getModuleById(id: string): ModuleNode | undefined { + const clientModule = this._client.getModuleById(id) + const ssrModule = this._ssr.getModuleById(id) + if (!clientModule && !ssrModule) { + return + } + return this.getBackwardCompatibleModuleNodeDual(clientModule, ssrModule) + } + + /** @deprecated */ + async getModuleByUrl( + url: string, + ssr?: boolean, + ): Promise { + // In the mixed graph, the ssr flag was used to resolve the id. + const [clientModule, ssrModule] = await Promise.all([ + this._client.getModuleByUrl(url), + this._ssr.getModuleByUrl(url), + ]) + if (!clientModule && !ssrModule) { + return + } + return this.getBackwardCompatibleModuleNodeDual(clientModule, ssrModule) + } + + /** @deprecated */ + getModulesByFile(file: string): Set | undefined { + // Until Vite 5.1.x, the moduleGraph contained modules from both the browser and server + // We maintain backwards compatibility by returning a Set of module proxies assuming + // that the modules for a certain file are the same in both the browser and server + const clientModules = this._client.getModulesByFile(file) + if (clientModules) { + return new Set( + [...clientModules].map( + (mod) => this.getBackwardCompatibleBrowserModuleNode(mod)!, + ), + ) + } + const ssrModules = this._ssr.getModulesByFile(file) + if (ssrModules) { + return new Set( + [...ssrModules].map( + (mod) => this.getBackwardCompatibleServerModuleNode(mod)!, + ), + ) + } + return undefined + } + + /** @deprecated */ + onFileChange(file: string): void { + this._client.onFileChange(file) + this._ssr.onFileChange(file) + } + + /** @deprecated */ + onFileDelete(file: string): void { + this._client.onFileDelete(file) + this._ssr.onFileDelete(file) + } + + /** @internal */ + _getModuleGraph(environment: string): EnvironmentModuleGraph { + switch (environment) { + case 'client': + return this._client + case 'ssr': + return this._ssr + default: + throw new Error(`Invalid module node environment ${environment}`) + } + } + + /** @deprecated */ + invalidateModule( + mod: ModuleNode, + seen: Set = new Set(), + timestamp: number = Date.now(), + isHmr: boolean = false, + /** @internal */ + softInvalidate = false, + ): void { + if (mod._clientModule) { + this._client.invalidateModule( + mod._clientModule, + new Set( + [...seen].map((mod) => mod._clientModule).filter(Boolean), + ) as Set, + timestamp, + isHmr, + softInvalidate, + ) + } + if (mod._ssrModule) { + // TODO: Maybe this isn't needed? + this._ssr.invalidateModule( + mod._ssrModule, + new Set( + [...seen].map((mod) => mod._ssrModule).filter(Boolean), + ) as Set, + timestamp, + isHmr, + softInvalidate, + ) + } + } + + /** @deprecated */ + invalidateAll(): void { + this._client.invalidateAll() + this._ssr.invalidateAll() + } + + /* TODO: I don't know if we need to implement this method (or how to do it yet) + async updateModuleInfo( + module: ModuleNode, + importedModules: Set, + importedBindings: Map> | null, + acceptedModules: Set, + acceptedExports: Set | null, + isSelfAccepting: boolean, + ssr?: boolean, + staticImportedUrls?: Set, // internal + ): Promise | undefined> { + const modules = await this._getModuleGraph( + module.environment, + ).updateModuleInfo( + module, + importedModules, // ? + importedBindings, + acceptedModules, // ? + acceptedExports, + isSelfAccepting, + staticImportedUrls, + ) + return modules + ? new Set( + [...modules].map((mod) => this.getBackwardCompatibleModuleNode(mod)!), + ) + : undefined + } + */ + + /** @deprecated */ + async ensureEntryFromUrl( + rawUrl: string, + ssr?: boolean, + setIsSelfAccepting = true, + ): Promise { + const module = await (ssr ? this._ssr : this._client).ensureEntryFromUrl( + rawUrl, + setIsSelfAccepting, + ) + return this.getBackwardCompatibleModuleNode(module)! + } + + /** @deprecated */ + createFileOnlyEntry(file: string): ModuleNode { + const clientModule = this._client.createFileOnlyEntry(file) + const ssrModule = this._ssr.createFileOnlyEntry(file) + return this.getBackwardCompatibleModuleNodeDual(clientModule, ssrModule)! + } + + /** @deprecated */ + async resolveUrl(url: string, ssr?: boolean): Promise { + return ssr ? this._ssr.resolveUrl(url) : this._client.resolveUrl(url) + } + + /** @deprecated */ + updateModuleTransformResult( + mod: ModuleNode, + result: TransformResult | null, + ssr?: boolean, + ): void { + const environment = ssr ? 'ssr' : 'client' + this._getModuleGraph(environment).updateModuleTransformResult( + (environment === 'client' ? mod._clientModule : mod._ssrModule)!, + result, + ) + } + + /** @deprecated */ + getModuleByEtag(etag: string): ModuleNode | undefined { + const mod = this._client.etagToModuleMap.get(etag) + return mod && this.getBackwardCompatibleBrowserModuleNode(mod) + } + + getBackwardCompatibleBrowserModuleNode( + clientModule: EnvironmentModuleNode, + ): ModuleNode { + return this.getBackwardCompatibleModuleNodeDual( + clientModule, + clientModule.id ? this._ssr.getModuleById(clientModule.id) : undefined, + ) + } + + getBackwardCompatibleServerModuleNode( + ssrModule: EnvironmentModuleNode, + ): ModuleNode { + return this.getBackwardCompatibleModuleNodeDual( + ssrModule.id ? this._client.getModuleById(ssrModule.id) : undefined, + ssrModule, + ) + } + + getBackwardCompatibleModuleNode(mod: EnvironmentModuleNode): ModuleNode { + return mod.environment === 'client' + ? this.getBackwardCompatibleBrowserModuleNode(mod) + : this.getBackwardCompatibleServerModuleNode(mod) + } + + getBackwardCompatibleModuleNodeDual( + clientModule?: EnvironmentModuleNode, + ssrModule?: EnvironmentModuleNode, + ): ModuleNode { + // ... + return new ModuleNode(this, clientModule, ssrModule) + } +} + +type ModuleSetNames = 'acceptedHmrDeps' | 'importedModules' + +function createBackwardCompatibleModuleSet( + moduleGraph: ModuleGraph, + prop: ModuleSetNames, + module: EnvironmentModuleNode, +): Set { + return { + [Symbol.iterator]() { + return this.keys() + }, + has(key) { + if (!key.id) { + return false + } + const keyModule = moduleGraph + ._getModuleGraph(module.environment) + .getModuleById(key.id) + return keyModule !== undefined && module[prop].has(keyModule) + }, + values() { + return this.keys() + }, + keys() { + return mapIterator(module[prop].keys(), (mod) => + moduleGraph.getBackwardCompatibleModuleNode(mod), + ) + }, + get size() { + return module[prop].size + }, + forEach(callback, thisArg) { + return module[prop].forEach((mod) => { + const backwardCompatibleMod = + moduleGraph.getBackwardCompatibleModuleNode(mod) + callback.call( + thisArg, + backwardCompatibleMod, + backwardCompatibleMod, + this, + ) + }) + }, + // TODO: should we implement all the set methods? + // missing: add, clear, delete, difference, intersection, isDisjointFrom, + // isSubsetOf, isSupersetOf, symmetricDifference, union + } as Set +} + +function createBackwardCompatibleModuleMap( + moduleGraph: ModuleGraph, + prop: 'urlToModuleMap' | 'idToModuleMap' | 'etagToModuleMap', + getModuleMap: () => Map, +): Map { + return { + [Symbol.iterator]() { + return this.entries() + }, + get(key) { + const clientModule = moduleGraph._client[prop].get(key) + const ssrModule = moduleGraph._ssr[prop].get(key) + if (!clientModule && !ssrModule) { + return + } + return moduleGraph.getBackwardCompatibleModuleNodeDual( + clientModule, + ssrModule, + ) + }, + keys() { + return getModuleMap().keys() + }, + values() { + return mapIterator(getModuleMap().values(), (mod) => + moduleGraph.getBackwardCompatibleModuleNode(mod), + ) + }, + entries() { + return mapIterator(getModuleMap().entries(), ([key, mod]) => [ + key, + moduleGraph.getBackwardCompatibleModuleNode(mod), + ]) + }, + get size() { + // TODO: Should we use Math.max(moduleGraph._client[prop].size, moduleGraph._ssr[prop].size) + // for performance? I don't think there are many use cases of this method + return getModuleMap().size + }, + forEach(callback, thisArg) { + return getModuleMap().forEach((mod, key) => { + const backwardCompatibleMod = + moduleGraph.getBackwardCompatibleModuleNode(mod) + callback.call(thisArg, backwardCompatibleMod, key, this) + }) + }, + } as Map +} + +function createBackwardCompatibleFileToModulesMap( + moduleGraph: ModuleGraph, +): Map> { + const getFileToModulesMap = (): Map> => { + // A good approximation to the previous logic that returned the union of + // the importedModules and importers from both the browser and server + if (!moduleGraph._ssr.fileToModulesMap.size) { + return moduleGraph._client.fileToModulesMap + } + const map = new Map(moduleGraph._client.fileToModulesMap) + for (const [key, modules] of moduleGraph._ssr.fileToModulesMap) { + const modulesSet = map.get(key) + if (!modulesSet) { + map.set(key, modules) + } else { + for (const ssrModule of modules) { + let hasModule = false + for (const clientModule of modulesSet) { + hasModule ||= clientModule.id === ssrModule.id + if (hasModule) { + break + } + } + if (!hasModule) { + modulesSet.add(ssrModule) + } + } + } + } + return map + } + const getBackwardCompatibleModules = ( + modules: Set, + ): Set => + new Set( + [...modules].map((mod) => + moduleGraph.getBackwardCompatibleModuleNode(mod), + ), + ) + + return { + [Symbol.iterator]() { + return this.entries() + }, + get(key) { + const clientModules = moduleGraph._client.fileToModulesMap.get(key) + const ssrModules = moduleGraph._ssr.fileToModulesMap.get(key) + if (!clientModules && !ssrModules) { + return + } + const modules = clientModules ?? new Set() + if (ssrModules) { + for (const ssrModule of ssrModules) { + if (ssrModule.id) { + let found = false + for (const mod of modules) { + found ||= mod.id === ssrModule.id + if (found) { + break + } + } + if (!found) { + modules?.add(ssrModule) + } + } + } + } + return getBackwardCompatibleModules(modules) + }, + keys() { + return getFileToModulesMap().keys() + }, + values() { + return mapIterator( + getFileToModulesMap().values(), + getBackwardCompatibleModules, + ) + }, + entries() { + return mapIterator(getFileToModulesMap().entries(), ([key, modules]) => [ + key, + getBackwardCompatibleModules(modules), + ]) + }, + get size() { + return getFileToModulesMap().size + }, + forEach(callback, thisArg) { + return getFileToModulesMap().forEach((modules, key) => { + callback.call(thisArg, getBackwardCompatibleModules(modules), key, this) + }) + }, + } as Map> +} diff --git a/packages/vite/src/node/server/moduleGraph.ts b/packages/vite/src/node/server/moduleGraph.ts index 442ece308dbaff..eeec3cd6188f07 100644 --- a/packages/vite/src/node/server/moduleGraph.ts +++ b/packages/vite/src/node/server/moduleGraph.ts @@ -10,7 +10,8 @@ import { FS_PREFIX } from '../constants' import { cleanUrl } from '../../shared/utils' import type { TransformResult } from './transformRequest' -export class ModuleNode { +export class EnvironmentModuleNode { + environment: string /** * Public served url path, starts with / */ @@ -18,22 +19,26 @@ export class ModuleNode { /** * Resolved file system path + query */ - id: string | null = null + id: string | null = null // TODO: remove null file: string | null = null type: 'js' | 'css' info?: ModuleInfo meta?: Record - importers = new Set() - clientImportedModules = new Set() - ssrImportedModules = new Set() - acceptedHmrDeps = new Set() + importers = new Set() + + importedModules = new Set() + + acceptedHmrDeps = new Set() acceptedHmrExports: Set | null = null importedBindings: Map> | null = null isSelfAccepting?: boolean transformResult: TransformResult | null = null - ssrTransformResult: TransformResult | null = null + + // ssrModule and ssrError are no longer needed. They are on the module runner module cache. + // Once `ssrLoadModule` is re-implemented on top of the new APIs, we can delete these. ssrModule: Record | null = null ssrError: Error | null = null + lastHMRTimestamp = 0 /** * `import.meta.hot.invalidate` is called by the client. @@ -54,10 +59,6 @@ export class ModuleNode { * @internal */ invalidationState: TransformResult | 'HARD_INVALIDATED' | undefined - /** - * @internal - */ - ssrInvalidationState: TransformResult | 'HARD_INVALIDATED' | undefined /** * The module urls that are statically imported in the code. This information is separated * out from `importedModules` as only importers that statically import the module can be @@ -69,21 +70,14 @@ export class ModuleNode { /** * @param setIsSelfAccepting - set `false` to set `isSelfAccepting` later. e.g. #7870 */ - constructor(url: string, setIsSelfAccepting = true) { + constructor(url: string, environment: string, setIsSelfAccepting = true) { + this.environment = environment this.url = url this.type = isDirectCSSRequest(url) ? 'css' : 'js' if (setIsSelfAccepting) { this.isSelfAccepting = false } } - - get importedModules(): Set { - const importedModules = new Set(this.clientImportedModules) - for (const module of this.ssrImportedModules) { - importedModules.add(module) - } - return importedModules - } } export type ResolvedUrl = [ @@ -92,66 +86,65 @@ export type ResolvedUrl = [ meta: object | null | undefined, ] -export class ModuleGraph { - urlToModuleMap = new Map() - idToModuleMap = new Map() - etagToModuleMap = new Map() +export class EnvironmentModuleGraph { + environment: string + + urlToModuleMap = new Map() + idToModuleMap = new Map() + etagToModuleMap = new Map() // a single file may corresponds to multiple modules with different queries - fileToModulesMap = new Map>() - safeModulesPath = new Set() + fileToModulesMap = new Map>() /** * @internal */ _unresolvedUrlToModuleMap = new Map< string, - Promise | ModuleNode + Promise | EnvironmentModuleNode >() + /** * @internal */ - _ssrUnresolvedUrlToModuleMap = new Map< - string, - Promise | ModuleNode - >() + _resolveId: (url: string) => Promise /** @internal */ - _hasResolveFailedErrorModules = new Set() + _hasResolveFailedErrorModules = new Set() constructor( - private resolveId: ( - url: string, - ssr: boolean, - ) => Promise, - ) {} + environment: string, + resolveId: (url: string) => Promise, + ) { + this.environment = environment + this._resolveId = resolveId + } async getModuleByUrl( rawUrl: string, - ssr?: boolean, - ): Promise { + ): Promise { // Quick path, if we already have a module for this rawUrl (even without extension) rawUrl = removeImportQuery(removeTimestampQuery(rawUrl)) - const mod = this._getUnresolvedUrlToModule(rawUrl, ssr) + const mod = this._getUnresolvedUrlToModule(rawUrl) if (mod) { return mod } - const [url] = await this._resolveUrl(rawUrl, ssr) + const [url] = await this._resolveUrl(rawUrl) return this.urlToModuleMap.get(url) } - getModuleById(id: string): ModuleNode | undefined { + getModuleById(id: string): EnvironmentModuleNode | undefined { return this.idToModuleMap.get(removeTimestampQuery(id)) } - getModulesByFile(file: string): Set | undefined { + getModulesByFile(file: string): Set | undefined { return this.fileToModulesMap.get(file) } onFileChange(file: string): void { const mods = this.getModulesByFile(file) if (mods) { - const seen = new Set() + const seen = new Set() mods.forEach((mod) => { this.invalidateModule(mod, seen) }) @@ -170,15 +163,15 @@ export class ModuleGraph { } invalidateModule( - mod: ModuleNode, - seen: Set = new Set(), + mod: EnvironmentModuleNode, + seen: Set = new Set(), timestamp: number = Date.now(), isHmr: boolean = false, /** @internal */ softInvalidate = false, ): void { const prevInvalidationState = mod.invalidationState - const prevSsrInvalidationState = mod.ssrInvalidationState + // const prevSsrInvalidationState = mod.ssrInvalidationState // Handle soft invalidation before the `seen` check, as consecutive soft/hard invalidations can // cause the final soft invalidation state to be different. @@ -186,19 +179,17 @@ export class ModuleGraph { // import timestamps only in `transformRequest`. If there's no previous `transformResult`, hard invalidate it. if (softInvalidate) { mod.invalidationState ??= mod.transformResult ?? 'HARD_INVALIDATED' - mod.ssrInvalidationState ??= mod.ssrTransformResult ?? 'HARD_INVALIDATED' } // If hard invalidated, further soft invalidations have no effect until it's reset to `undefined` else { mod.invalidationState = 'HARD_INVALIDATED' - mod.ssrInvalidationState = 'HARD_INVALIDATED' } // Skip updating the module if it was already invalidated before and the invalidation state has not changed if ( seen.has(mod) && - prevInvalidationState === mod.invalidationState && - prevSsrInvalidationState === mod.ssrInvalidationState + prevInvalidationState === mod.invalidationState + // && prevSsrInvalidationState === mod.ssrInvalidationState ) { return } @@ -219,7 +210,7 @@ export class ModuleGraph { if (etag) this.etagToModuleMap.delete(etag) mod.transformResult = null - mod.ssrTransformResult = null + mod.ssrModule = null mod.ssrError = null @@ -246,7 +237,7 @@ export class ModuleGraph { invalidateAll(): void { const timestamp = Date.now() - const seen = new Set() + const seen = new Set() this.idToModuleMap.forEach((mod) => { this.invalidateModule(mod, seen, timestamp) }) @@ -261,19 +252,18 @@ export class ModuleGraph { * This is only used for soft invalidations so `undefined` is fine but may cause more runtime processing. */ async updateModuleInfo( - mod: ModuleNode, - importedModules: Set, + mod: EnvironmentModuleNode, + importedModules: Set, importedBindings: Map> | null, - acceptedModules: Set, + acceptedModules: Set, acceptedExports: Set | null, isSelfAccepting: boolean, - ssr?: boolean, /** @internal */ staticImportedUrls?: Set, - ): Promise | undefined> { + ): Promise | undefined> { mod.isSelfAccepting = isSelfAccepting - const prevImports = ssr ? mod.ssrImportedModules : mod.clientImportedModules - let noLongerImported: Set | undefined + const prevImports = mod.importedModules + let noLongerImported: Set | undefined let resolvePromises = [] let resolveResults = new Array(importedModules.size) @@ -283,7 +273,7 @@ export class ModuleGraph { const nextIndex = index++ if (typeof imported === 'string') { resolvePromises.push( - this.ensureEntryFromUrl(imported, ssr).then((dep) => { + this.ensureEntryFromUrl(imported).then((dep) => { dep.importers.add(mod) resolveResults[nextIndex] = dep }), @@ -299,18 +289,11 @@ export class ModuleGraph { } const nextImports = new Set(resolveResults) - if (ssr) { - mod.ssrImportedModules = nextImports - } else { - mod.clientImportedModules = nextImports - } + mod.importedModules = nextImports // remove the importer from deps that were imported but no longer are. prevImports.forEach((dep) => { - if ( - !mod.clientImportedModules.has(dep) && - !mod.ssrImportedModules.has(dep) - ) { + if (!mod.importedModules.has(dep)) { dep.importers.delete(mod) if (!dep.importers.size) { // dependency no longer imported @@ -327,7 +310,7 @@ export class ModuleGraph { const nextIndex = index++ if (typeof accepted === 'string') { resolvePromises.push( - this.ensureEntryFromUrl(accepted, ssr).then((dep) => { + this.ensureEntryFromUrl(accepted).then((dep) => { resolveResults[nextIndex] = dep }), ) @@ -351,10 +334,9 @@ export class ModuleGraph { async ensureEntryFromUrl( rawUrl: string, - ssr?: boolean, setIsSelfAccepting = true, - ): Promise { - return this._ensureEntryFromUrl(rawUrl, ssr, setIsSelfAccepting) + ): Promise { + return this._ensureEntryFromUrl(rawUrl, setIsSelfAccepting) } /** @@ -362,26 +344,25 @@ export class ModuleGraph { */ async _ensureEntryFromUrl( rawUrl: string, - ssr?: boolean, setIsSelfAccepting = true, // Optimization, avoid resolving the same url twice if the caller already did it resolved?: PartialResolvedId, - ): Promise { + ): Promise { // Quick path, if we already have a module for this rawUrl (even without extension) rawUrl = removeImportQuery(removeTimestampQuery(rawUrl)) - let mod = this._getUnresolvedUrlToModule(rawUrl, ssr) + let mod = this._getUnresolvedUrlToModule(rawUrl) if (mod) { return mod } const modPromise = (async () => { - const [url, resolvedId, meta] = await this._resolveUrl( - rawUrl, - ssr, - resolved, - ) + const [url, resolvedId, meta] = await this._resolveUrl(rawUrl, resolved) mod = this.idToModuleMap.get(resolvedId) if (!mod) { - mod = new ModuleNode(url, setIsSelfAccepting) + mod = new EnvironmentModuleNode( + url, + this.environment, + setIsSelfAccepting, + ) if (meta) mod.meta = meta this.urlToModuleMap.set(url, mod) mod.id = resolvedId @@ -399,13 +380,13 @@ export class ModuleGraph { else if (!this.urlToModuleMap.has(url)) { this.urlToModuleMap.set(url, mod) } - this._setUnresolvedUrlToModule(rawUrl, mod, ssr) + this._setUnresolvedUrlToModule(rawUrl, mod) return mod })() // Also register the clean url to the module, so that we can short-circuit // resolving the same url twice - this._setUnresolvedUrlToModule(rawUrl, modPromise, ssr) + this._setUnresolvedUrlToModule(rawUrl, modPromise) return modPromise } @@ -413,7 +394,7 @@ export class ModuleGraph { // url because they are inlined into the main css import. But they still // need to be represented in the module graph so that they can trigger // hmr in the importing css file. - createFileOnlyEntry(file: string): ModuleNode { + createFileOnlyEntry(file: string): EnvironmentModuleNode { file = normalizePath(file) let fileMappedModules = this.fileToModulesMap.get(file) if (!fileMappedModules) { @@ -428,7 +409,7 @@ export class ModuleGraph { } } - const mod = new ModuleNode(url) + const mod = new EnvironmentModuleNode(url, this.environment) mod.file = file fileMappedModules.add(mod) return mod @@ -438,33 +419,29 @@ export class ModuleGraph { // 1. remove the HMR timestamp query (?t=xxxx) and the ?import query // 2. resolve its extension so that urls with or without extension all map to // the same module - async resolveUrl(url: string, ssr?: boolean): Promise { + async resolveUrl(url: string): Promise { url = removeImportQuery(removeTimestampQuery(url)) - const mod = await this._getUnresolvedUrlToModule(url, ssr) + const mod = await this._getUnresolvedUrlToModule(url) if (mod?.id) { return [mod.url, mod.id, mod.meta] } - return this._resolveUrl(url, ssr) + return this._resolveUrl(url) } updateModuleTransformResult( - mod: ModuleNode, + mod: EnvironmentModuleNode, result: TransformResult | null, - ssr: boolean, ): void { - if (ssr) { - mod.ssrTransformResult = result - } else { + if (this.environment === 'client') { const prevEtag = mod.transformResult?.etag if (prevEtag) this.etagToModuleMap.delete(prevEtag) - - mod.transformResult = result - if (result?.etag) this.etagToModuleMap.set(result.etag, mod) } + + mod.transformResult = result } - getModuleByEtag(etag: string): ModuleNode | undefined { + getModuleByEtag(etag: string): EnvironmentModuleNode | undefined { return this.etagToModuleMap.get(etag) } @@ -473,24 +450,17 @@ export class ModuleGraph { */ _getUnresolvedUrlToModule( url: string, - ssr?: boolean, - ): Promise | ModuleNode | undefined { - return ( - ssr ? this._ssrUnresolvedUrlToModuleMap : this._unresolvedUrlToModuleMap - ).get(url) + ): Promise | EnvironmentModuleNode | undefined { + return this._unresolvedUrlToModuleMap.get(url) } /** * @internal */ _setUnresolvedUrlToModule( url: string, - mod: Promise | ModuleNode, - ssr?: boolean, + mod: Promise | EnvironmentModuleNode, ): void { - ;(ssr - ? this._ssrUnresolvedUrlToModuleMap - : this._unresolvedUrlToModuleMap - ).set(url, mod) + this._unresolvedUrlToModuleMap.set(url, mod) } /** @@ -498,10 +468,9 @@ export class ModuleGraph { */ async _resolveUrl( url: string, - ssr?: boolean, alreadyResolved?: PartialResolvedId, ): Promise { - const resolved = alreadyResolved ?? (await this.resolveId(url, !!ssr)) + const resolved = alreadyResolved ?? (await this._resolveId(url)) const resolvedId = resolved?.id || url if ( url !== resolvedId && diff --git a/packages/vite/src/node/server/pluginContainer.ts b/packages/vite/src/node/server/pluginContainer.ts index 4ea3d0e6b51ea0..72e89aa6e7bd49 100644 --- a/packages/vite/src/node/server/pluginContainer.ts +++ b/packages/vite/src/node/server/pluginContainer.ts @@ -61,7 +61,7 @@ import { TraceMap, originalPositionFor } from '@jridgewell/trace-mapping' import MagicString from 'magic-string' import type { FSWatcher } from 'chokidar' import colors from 'picocolors' -import type { Plugin } from '../plugin' +import type { BoundedPlugin, Plugin, PluginEnvironment } from '../plugin' import { combineSourcemaps, createDebugger, @@ -76,11 +76,11 @@ import { timeFrom, } from '../utils' import { FS_PREFIX } from '../constants' -import type { ResolvedConfig } from '../config' import { createPluginHookUtils, getHookHandler } from '../plugins' import { cleanUrl, unwrapId } from '../../shared/utils' +import type { DevEnvironment } from './environment' import { buildErrorMessage } from './middlewares/error' -import type { ModuleGraph, ModuleNode } from './moduleGraph' +import type { EnvironmentModuleNode } from './moduleGraph' const noop = () => {} @@ -103,18 +103,16 @@ export interface PluginContainerOptions { writeFile?: (name: string, source: string | Uint8Array) => void } -export interface PluginContainer { +export interface BoundedPluginContainer { options: InputOptions - getModuleInfo(id: string): ModuleInfo | null buildStart(options: InputOptions): Promise resolveId( id: string, - importer?: string, + importer: string | undefined, options?: { attributes?: Record custom?: CustomPluginOptions skip?: Set - ssr?: boolean /** * @internal */ @@ -127,15 +125,9 @@ export interface PluginContainer { id: string, options?: { inMap?: SourceDescription['map'] - ssr?: boolean }, ): Promise<{ code: string; map: SourceMap | { mappings: '' } | null }> - load( - id: string, - options?: { - ssr?: boolean - }, - ): Promise + load(id: string, options?: {}): Promise watchChange( id: string, change: { event: 'create' | 'update' | 'delete' }, @@ -149,17 +141,31 @@ type PluginContext = Omit< 'cache' > -export async function createPluginContainer( - config: ResolvedConfig, - moduleGraph?: ModuleGraph, +/** + * Create a plugin container with a set of plugins. We pass them as a parameter + * instead of using environment.plugins to allow the creation of different + * pipelines working with the same environment (used for createIdResolver). + */ +export async function createBoundedPluginContainer( + environment: PluginEnvironment, + plugins: BoundedPlugin[], watcher?: FSWatcher, -): Promise { +): Promise { const { - plugins, + config, logger, - root, - build: { rollupOptions }, - } = config + options: { + build: { rollupOptions }, + }, + } = environment + const { root } = config + + // Backward compatibility + const ssr = environment.name !== 'client' + + const moduleGraph = + environment.mode === 'dev' ? environment.moduleGraph : undefined + const { getSortedPluginHooks, getSortedPlugins } = createPluginHookUtils(plugins) @@ -182,7 +188,7 @@ export async function createPluginContainer( const watchFiles = new Set() // _addedFiles from the `load()` hook gets saved here so it can be reused in the `transform()` hook const moduleNodeToLoadAddedImports = new WeakMap< - ModuleNode, + EnvironmentModuleNode, Set | null >() @@ -253,40 +259,11 @@ export async function createPluginContainer( // same default value of "moduleInfo.meta" as in Rollup const EMPTY_OBJECT = Object.freeze({}) - function getModuleInfo(id: string) { - const module = moduleGraph?.getModuleById(id) - if (!module) { - return null - } - if (!module.info) { - module.info = new Proxy( - { id, meta: module.meta || EMPTY_OBJECT } as ModuleInfo, - ModuleInfoProxy, - ) - } - return module.info - } - - function updateModuleInfo(id: string, { meta }: { meta?: object | null }) { - if (meta) { - const moduleInfo = getModuleInfo(id) - if (moduleInfo) { - moduleInfo.meta = { ...moduleInfo.meta, ...meta } - } - } - } - - function updateModuleLoadAddedImports(id: string, ctx: Context) { - const module = moduleGraph?.getModuleById(id) - if (module) { - moduleNodeToLoadAddedImports.set(module, ctx._addedImports) - } - } - // we should create a new context for each async hook pipeline so that the // active plugin in that pipeline can be tracked in a concurrency-safe manner. // using a class to make creating new contexts more efficient class Context implements PluginContext { + environment: PluginEnvironment // TODO: | ScanEnvironment meta = minimalContext.meta ssr = false _scan = false @@ -297,6 +274,7 @@ export async function createPluginContainer( _addedImports: Set | null = null constructor(initialPlugin?: Plugin) { + this.environment = environment this._activePlugin = initialPlugin || null } @@ -324,7 +302,6 @@ export async function createPluginContainer( custom: options?.custom, isEntry: !!options?.isEntry, skip, - ssr: this.ssr, scan: this._scan, }) if (typeof out === 'string') out = { id: out } @@ -338,16 +315,16 @@ export async function createPluginContainer( } & Partial>, ): Promise { // We may not have added this to our module graph yet, so ensure it exists - await moduleGraph?.ensureEntryFromUrl(unwrapId(options.id), this.ssr) + await moduleGraph?.ensureEntryFromUrl(unwrapId(options.id)) // Not all options passed to this function make sense in the context of loading individual files, // but we can at least update the module info properties we support - updateModuleInfo(options.id, options) + this._updateModuleInfo(options.id, options) - const loadResult = await container.load(options.id, { ssr: this.ssr }) + const loadResult = await container.load(options.id) const code = typeof loadResult === 'object' ? loadResult?.code : loadResult if (code != null) { - await container.transform(code, options.id, { ssr: this.ssr }) + await container.transform(code, options.id) } const moduleInfo = this.getModuleInfo(options.id) @@ -360,13 +337,39 @@ export async function createPluginContainer( } getModuleInfo(id: string) { - return getModuleInfo(id) + const module = moduleGraph?.getModuleById(id) + if (!module) { + return null + } + if (!module.info) { + module.info = new Proxy( + { id, meta: module.meta || EMPTY_OBJECT } as ModuleInfo, + ModuleInfoProxy, + ) + } + return module.info + } + + _updateModuleInfo(id: string, { meta }: { meta?: object | null }) { + if (meta) { + const moduleInfo = this.getModuleInfo(id) + if (moduleInfo) { + moduleInfo.meta = { ...moduleInfo.meta, ...meta } + } + } + } + + _updateModuleLoadAddedImports(id: string) { + const module = moduleGraph?.getModuleById(id) + if (module) { + moduleNodeToLoadAddedImports.set(module, this._addedImports) + } } getModuleIds() { - return moduleGraph - ? moduleGraph.idToModuleMap.keys() - : Array.prototype[Symbol.iterator]() + return ( + moduleGraph?.idToModuleMap.keys() ?? Array.prototype[Symbol.iterator]() + ) } addWatchFile(id: string) { @@ -651,8 +654,6 @@ export async function createPluginContainer( return options })(), - getModuleInfo, - async buildStart() { await handleHookPromise( hookParallel( @@ -665,17 +666,18 @@ export async function createPluginContainer( async resolveId(rawId, importer = join(root, 'index.html'), options) { const skip = options?.skip - const ssr = options?.ssr const scan = !!options?.scan + const ctx = new Context() - ctx.ssr = !!ssr - ctx._scan = scan ctx._resolveSkips = skip + ctx._scan = scan + const resolveStart = debugResolve ? performance.now() : 0 let id: string | null = null const partial: Partial = {} for (const plugin of getSortedPlugins('resolveId')) { - if (closed && !ssr) throwClosedServerError() + if (closed && environment?.options.dev.recoverable) + throwClosedServerError() if (!plugin.resolveId) continue if (skip?.has(plugin)) continue @@ -733,36 +735,36 @@ export async function createPluginContainer( }, async load(id, options) { - const ssr = options?.ssr + options = options ? { ...options, ssr } : { ssr } const ctx = new Context() - ctx.ssr = !!ssr for (const plugin of getSortedPlugins('load')) { - if (closed && !ssr) throwClosedServerError() + if (closed && environment?.options.dev.recoverable) + throwClosedServerError() if (!plugin.load) continue ctx._activePlugin = plugin const handler = getHookHandler(plugin.load) const result = await handleHookPromise( - handler.call(ctx as any, id, { ssr }), + handler.call(ctx as any, id, options), ) if (result != null) { if (isObject(result)) { - updateModuleInfo(id, result) + ctx._updateModuleInfo(id, result) } - updateModuleLoadAddedImports(id, ctx) + ctx._updateModuleLoadAddedImports(id) return result } } - updateModuleLoadAddedImports(id, ctx) + ctx._updateModuleLoadAddedImports(id) return null }, async transform(code, id, options) { + options = options ? { ...options, ssr } : { ssr } const inMap = options?.inMap - const ssr = options?.ssr const ctx = new TransformContext(id, code, inMap as SourceMap) - ctx.ssr = !!ssr for (const plugin of getSortedPlugins('transform')) { - if (closed && !ssr) throwClosedServerError() + if (closed && environment?.options.dev.recoverable) + throwClosedServerError() if (!plugin.transform) continue ctx._activePlugin = plugin ctx._activeId = id @@ -772,7 +774,7 @@ export async function createPluginContainer( const handler = getHookHandler(plugin.transform) try { result = await handleHookPromise( - handler.call(ctx as any, code, id, { ssr }), + handler.call(ctx as any, code, id, options), ) } catch (e) { ctx.error(e) @@ -794,7 +796,7 @@ export async function createPluginContainer( ctx.sourcemapChain.push(result.map) } } - updateModuleInfo(id, result) + ctx._updateModuleInfo(id, result) } else { code = result } @@ -834,3 +836,103 @@ export async function createPluginContainer( return container } + +// Backward compatiblity + +export interface PluginContainer { + options: InputOptions + buildStart(options: InputOptions): Promise + resolveId( + id: string, + importer: string | undefined, + options?: { + attributes?: Record + custom?: CustomPluginOptions + skip?: Set + ssr?: boolean + environment?: PluginEnvironment + /** + * @internal + */ + scan?: boolean + isEntry?: boolean + }, + ): Promise + transform( + code: string, + id: string, + options?: { + inMap?: SourceDescription['map'] + ssr?: boolean + environment?: PluginEnvironment + }, + ): Promise<{ code: string; map: SourceMap | { mappings: '' } | null }> + load( + id: string, + options?: { + ssr?: boolean + environment?: PluginEnvironment + }, + ): Promise + watchChange( + id: string, + change: { event: 'create' | 'update' | 'delete' }, + ): Promise + close(): Promise +} + +/** + * server.pluginContainer compatibility + * + * The default environment is in buildStart, buildEnd, watchChange, and closeBundle hooks, + * wich are called once for all environments, or when no environment is passed in other hooks. + * The ssrEnvironment is needed for backward compatibility when the ssr flag is passed without + * an environment. The defaultEnvironment in the main pluginContainer in the server should be + * the client environment for backward compatibility. + **/ + +export function createPluginContainer( + environments: Record, +): PluginContainer { + // Backward compatibility + // Users should call pluginContainer.resolveId (and load/transform) passing the environment they want to work with + // But there is code that is going to call it without passing an environment, or with the ssr flag to get the ssr environment + function getEnvironment(options?: { ssr?: boolean }) { + return environments?.[options?.ssr ? 'ssr' : 'client'] + } + function getPluginContainer(options?: { ssr?: boolean }) { + return (getEnvironment(options) as DevEnvironment).pluginContainer! + } + + const container: PluginContainer = { + get options() { + return (environments.client as DevEnvironment).pluginContainer!.options + }, + + async buildStart() { + // noop, buildStart will be called for each environment + }, + + async resolveId(rawId, importer, options) { + return getPluginContainer(options).resolveId(rawId, importer, options) + }, + + async load(id, options) { + return getPluginContainer(options).load(id, options) + }, + + async transform(code, id, options) { + return getPluginContainer(options).transform(code, id, options) + }, + + async watchChange(id, change) { + // noop, watchChange is already called for each environment + }, + + async close() { + // noop, close will be called for each environment + }, + } + + return container +} diff --git a/packages/vite/src/node/server/transformRequest.ts b/packages/vite/src/node/server/transformRequest.ts index 94e8c124041077..7a6223e1fc4141 100644 --- a/packages/vite/src/node/server/transformRequest.ts +++ b/packages/vite/src/node/server/transformRequest.ts @@ -6,7 +6,7 @@ import MagicString from 'magic-string' import { init, parse as parseImports } from 'es-module-lexer' import type { PartialResolvedId, SourceDescription, SourceMap } from 'rollup' import colors from 'picocolors' -import type { ModuleNode, ViteDevServer } from '..' +import type { EnvironmentModuleNode } from '../server/moduleGraph' import { createDebugger, ensureWatchedFile, @@ -18,17 +18,17 @@ import { stripBase, timeFrom, } from '../utils' +import { ssrParseImports, ssrTransform } from '../ssr/ssrTransform' import { checkPublicFile } from '../publicDir' -import { isDepsOptimizerEnabled } from '../config' -import { getDepsOptimizer, initDevSsrDepsOptimizer } from '../optimizer' import { cleanUrl, unwrapId } from '../../shared/utils' import { applySourcemapIgnoreList, extractSourcemapFromFile, injectSourcesContent, } from './sourcemap' -import { isFileServingAllowed } from './middlewares/static' +import { isFileLoadingAllowed } from './middlewares/static' import { throwClosedServerError } from './pluginContainer' +import type { DevEnvironment } from './environment' export const ERR_LOAD_URL = 'ERR_LOAD_URL' export const ERR_LOAD_PUBLIC_URL = 'ERR_LOAD_PUBLIC_URL' @@ -40,24 +40,46 @@ const debugCache = createDebugger('vite:cache') export interface TransformResult { code: string map: SourceMap | { mappings: '' } | null + ssr?: boolean etag?: string deps?: string[] dynamicDeps?: string[] } +// TODO: Rename to LoadOptions and move to /plugin.ts ? export interface TransformOptions { + /** + * @deprecated infered from environment + */ ssr?: boolean + /** + * TODO: should this be internal? + */ html?: boolean } +// TODO: This function could be moved to the DevEnvironment class. +// It was already using private fields from the server before, and it now does +// the same with environment._closing, environment._pendingRequests and +// environment._registerRequestProcessing. Maybe it makes sense to keep it in +// separate file to preserve the history or keep the DevEnvironment class cleaner, +// but conceptually this is: `environment.transformRequest(url, options)` + export function transformRequest( + environment: DevEnvironment, url: string, - server: ViteDevServer, options: TransformOptions = {}, ): Promise { - if (server._restartPromise && !options.ssr) throwClosedServerError() + // Backward compatibility when only `ssr` is passed + if (!options?.ssr) { + // Backward compatibility + options = { ...options, ssr: environment.name !== 'client' } + } - const cacheKey = (options.ssr ? 'ssr:' : options.html ? 'html:' : '') + url + if (environment._closing && environment?.options.dev.recoverable) + throwClosedServerError() + + const cacheKey = `${options.html ? 'html:' : ''}${url}` // This module may get invalidated while we are processing it. For example // when a full page reload is needed after the re-processing of pre-bundled @@ -81,10 +103,10 @@ export function transformRequest( // last time this module is invalidated const timestamp = Date.now() - const pending = server._pendingRequests.get(cacheKey) + const pending = environment._pendingRequests.get(cacheKey) if (pending) { - return server.moduleGraph - .getModuleByUrl(removeTimestampQuery(url), options.ssr) + return environment.moduleGraph + .getModuleByUrl(removeTimestampQuery(url)) .then((module) => { if (!module || pending.timestamp > module.lastInvalidationTimestamp) { // The pending request is still valid, we can safely reuse its result @@ -97,24 +119,24 @@ export function transformRequest( // First request has been invalidated, abort it to clear the cache, // then perform a new doTransform. pending.abort() - return transformRequest(url, server, options) + return transformRequest(environment, url, options) } }) } - const request = doTransform(url, server, options, timestamp) + const request = doTransform(environment, url, options, timestamp) // Avoid clearing the cache of future requests if aborted let cleared = false const clearCache = () => { if (!cleared) { - server._pendingRequests.delete(cacheKey) + environment._pendingRequests.delete(cacheKey) cleared = true } } // Cache the request and clear it once processing is done - server._pendingRequests.set(cacheKey, { + environment._pendingRequests.set(cacheKey, { request, timestamp, abort: clearCache, @@ -124,100 +146,89 @@ export function transformRequest( } async function doTransform( + environment: DevEnvironment, url: string, - server: ViteDevServer, options: TransformOptions, timestamp: number, ) { url = removeTimestampQuery(url) - const { config, pluginContainer } = server - const ssr = !!options.ssr + await environment.init() - if (ssr && isDepsOptimizerEnabled(config, true)) { - await initDevSsrDepsOptimizer(config, server) - } + const { pluginContainer } = environment - let module = await server.moduleGraph.getModuleByUrl(url, ssr) + let module = await environment.moduleGraph.getModuleByUrl(url) if (module) { // try use cache from url const cached = await getCachedTransformResult( + environment, url, module, - server, - ssr, timestamp, ) if (cached) return cached } + // TODO: Simplify const resolved = module ? undefined - : (await pluginContainer.resolveId(url, undefined, { ssr })) ?? undefined + : (await pluginContainer.resolveId(url, undefined)) ?? undefined // resolve const id = module?.id ?? resolved?.id ?? url - module ??= server.moduleGraph.getModuleById(id) + module ??= environment.moduleGraph.getModuleById(id) if (module) { // if a different url maps to an existing loaded id, make sure we relate this url to the id - await server.moduleGraph._ensureEntryFromUrl(url, ssr, undefined, resolved) + await environment.moduleGraph._ensureEntryFromUrl(url, undefined, resolved) // try use cache from id const cached = await getCachedTransformResult( + environment, url, module, - server, - ssr, timestamp, ) if (cached) return cached } const result = loadAndTransform( + environment, id, url, - server, options, timestamp, module, resolved, ) - if (!ssr) { - // Only register client requests, server.waitForRequestsIdle should - // have been called server.waitForClientRequestsIdle. We can rename - // it as part of the environment API work - const depsOptimizer = getDepsOptimizer(config, ssr) - if (!depsOptimizer?.isOptimizedDepFile(id)) { - server._registerRequestProcessing(id, () => result) - } + const { depsOptimizer } = environment + if (!depsOptimizer?.isOptimizedDepFile(id)) { + environment._registerRequestProcessing(id, () => result) } return result } async function getCachedTransformResult( + environment: DevEnvironment, url: string, - module: ModuleNode, - server: ViteDevServer, - ssr: boolean, + module: EnvironmentModuleNode, timestamp: number, ) { - const prettyUrl = debugCache ? prettifyUrl(url, server.config.root) : '' + const prettyUrl = debugCache ? prettifyUrl(url, environment.config.root) : '' // tries to handle soft invalidation of the module if available, // returns a boolean true is successful, or false if no handling is needed const softInvalidatedTransformResult = module && - (await handleModuleSoftInvalidation(module, ssr, timestamp, server)) + (await handleModuleSoftInvalidation(environment, module, timestamp)) if (softInvalidatedTransformResult) { debugCache?.(`[memory-hmr] ${prettyUrl}`) return softInvalidatedTransformResult } // check if we have a fresh cache - const cached = - module && (ssr ? module.ssrTransformResult : module.transformResult) + const cached = module?.transformResult if (cached) { debugCache?.(`[memory] ${prettyUrl}`) return cached @@ -225,29 +236,32 @@ async function getCachedTransformResult( } async function loadAndTransform( + environment: DevEnvironment, id: string, url: string, - server: ViteDevServer, options: TransformOptions, timestamp: number, - mod?: ModuleNode, + mod?: EnvironmentModuleNode, resolved?: PartialResolvedId, ) { - const { config, pluginContainer, moduleGraph } = server + const { config, pluginContainer } = environment const { logger } = config const prettyUrl = debugLoad || debugTransform ? prettifyUrl(url, config.root) : '' - const ssr = !!options.ssr - const file = cleanUrl(id) + const moduleGraph = environment.moduleGraph let code: string | null = null let map: SourceDescription['map'] = null // load const loadStart = debugLoad ? performance.now() : 0 - const loadResult = await pluginContainer.load(id, { ssr }) + const loadResult = await pluginContainer.load(id, options) + + // TODO: Replace this with pluginLoadFallback if (loadResult == null) { + const file = cleanUrl(id) + // if this is an html request and there is no load result, skip ahead to // SPA fallback. if (options.html && !id.endsWith('.html')) { @@ -258,7 +272,10 @@ async function loadAndTransform( // as string // only try the fallback if access is allowed, skip for out of root url // like /service-worker.js or /api/users - if (options.ssr || isFileServingAllowed(file, server)) { + if ( + environment.options.nodeCompatible || + isFileLoadingAllowed(config, file) + ) { try { code = await fsp.readFile(file, 'utf-8') debugLoad?.(`${timeFrom(loadStart)} [fs] ${prettyUrl}`) @@ -270,8 +287,8 @@ async function loadAndTransform( throw e } } - if (code != null) { - ensureWatchedFile(server.watcher, file, config.root) + if (code != null && environment.watcher) { + ensureWatchedFile(environment.watcher, file, config.root) } } if (code) { @@ -306,10 +323,8 @@ async function loadAndTransform( `should not be imported from source code. It can only be referenced ` + `via HTML tags.` : `Does the file exist?` - const importerMod: ModuleNode | undefined = server.moduleGraph.idToModuleMap - .get(id) - ?.importers.values() - .next().value + const importerMod: EnvironmentModuleNode | undefined = + moduleGraph.idToModuleMap.get(id)?.importers.values().next().value const importer = importerMod?.file || importerMod?.url const err: any = new Error( `Failed to load url ${url} (resolved id: ${id})${ @@ -320,16 +335,16 @@ async function loadAndTransform( throw err } - if (server._restartPromise && !ssr) throwClosedServerError() + if (environment._closing && environment.options.dev.recoverable) + throwClosedServerError() // ensure module in graph after successful load - mod ??= await moduleGraph._ensureEntryFromUrl(url, ssr, undefined, resolved) + mod ??= await moduleGraph._ensureEntryFromUrl(url, undefined, resolved) // transform const transformStart = debugTransform ? performance.now() : 0 const transformResult = await pluginContainer.transform(code, id, { inMap: map, - ssr, }) const originalCode = code if ( @@ -392,21 +407,27 @@ async function loadAndTransform( } } - if (server._restartPromise && !ssr) throwClosedServerError() + if (environment._closing && environment.options.dev.recoverable) + throwClosedServerError() - const result = - ssr && !server.config.experimental.skipSsrTransform - ? await server.ssrTransform(code, normalizedMap, url, originalCode) - : ({ - code, - map: normalizedMap, - etag: getEtag(code, { weak: true }), - } satisfies TransformResult) + const result = environment.options.dev.moduleRunnerTransform + ? await ssrTransform( + code, + normalizedMap, + url, + originalCode, + environment.config, + ) + : ({ + code, + map: normalizedMap, + etag: getEtag(code, { weak: true }), + } satisfies TransformResult) // Only cache the result if the module wasn't invalidated while it was // being processed, so it is re-processed next time if it is stale if (timestamp > mod.lastInvalidationTimestamp) - moduleGraph.updateModuleTransformResult(mod, result, ssr) + moduleGraph.updateModuleTransformResult(mod, result) return result } @@ -419,37 +440,40 @@ async function loadAndTransform( * - SSR: We don't need to change anything as `ssrLoadModule` controls it */ async function handleModuleSoftInvalidation( - mod: ModuleNode, - ssr: boolean, + environment: DevEnvironment, + mod: EnvironmentModuleNode, timestamp: number, - server: ViteDevServer, ) { - const transformResult = ssr ? mod.ssrInvalidationState : mod.invalidationState + const transformResult = mod.invalidationState // Reset invalidation state - if (ssr) mod.ssrInvalidationState = undefined - else mod.invalidationState = undefined + mod.invalidationState = undefined // Skip if not soft-invalidated if (!transformResult || transformResult === 'HARD_INVALIDATED') return - if (ssr ? mod.ssrTransformResult : mod.transformResult) { + if (mod.transformResult) { throw new Error( `Internal server error: Soft-invalidated module "${mod.url}" should not have existing transform result`, ) } let result: TransformResult - // For SSR soft-invalidation, no transformation is needed - if (ssr) { + // No transformation is needed if it's disabled manually + // This is primarily for backwards compatible SSR + if (!environment.options.injectInvalidationTimestamp) { result = transformResult } - // For client soft-invalidation, we need to transform each imports with new timestamps if available + // We need to transform each imports with new timestamps if available else { - await init const source = transformResult.code const s = new MagicString(source) - const [imports] = parseImports(source, mod.id || undefined) + const imports = transformResult.ssr + ? await ssrParseImports(mod.url, source) + : await (async () => { + await init + return parseImports(source, mod.id || undefined)[0] + })() for (const imp of imports) { let rawUrl = source.slice(imp.s, imp.e) @@ -463,9 +487,12 @@ async function handleModuleSoftInvalidation( const urlWithoutTimestamp = removeTimestampQuery(rawUrl) // hmrUrl must be derived the same way as importAnalysis const hmrUrl = unwrapId( - stripBase(removeImportQuery(urlWithoutTimestamp), server.config.base), + stripBase( + removeImportQuery(urlWithoutTimestamp), + environment.config.base, + ), ) - for (const importedMod of mod.clientImportedModules) { + for (const importedMod of mod.importedModules) { if (importedMod.url !== hmrUrl) continue if (importedMod.lastHMRTimestamp > 0) { const replacedUrl = injectQuery( @@ -477,9 +504,9 @@ async function handleModuleSoftInvalidation( s.overwrite(start, end, replacedUrl) } - if (imp.d === -1 && server.config.server.preTransformRequests) { + if (imp.d === -1 && environment.options.dev.preTransformRequests) { // pre-transform known direct imports - server.warmupRequest(hmrUrl, { ssr }) + environment.warmupRequest(hmrUrl) } break @@ -499,7 +526,7 @@ async function handleModuleSoftInvalidation( // Only cache the result if the module wasn't invalidated while it was // being processed, so it is re-processed next time if it is stale if (timestamp > mod.lastInvalidationTimestamp) - server.moduleGraph.updateModuleTransformResult(mod, result, ssr) + environment.moduleGraph.updateModuleTransformResult(mod, result) return result } diff --git a/packages/vite/src/node/server/warmup.ts b/packages/vite/src/node/server/warmup.ts index 33af7bb2a599a3..e39797b4fceac3 100644 --- a/packages/vite/src/node/server/warmup.ts +++ b/packages/vite/src/node/server/warmup.ts @@ -5,28 +5,24 @@ import colors from 'picocolors' import { FS_PREFIX } from '../constants' import { normalizePath } from '../utils' import type { ViteDevServer } from '../index' +import type { DevEnvironment } from './environment' export function warmupFiles(server: ViteDevServer): void { - const options = server.config.server.warmup - const root = server.config.root - - if (options?.clientFiles?.length) { - mapFiles(options.clientFiles, root).then((files) => { - for (const file of files) { - warmupFile(server, file, false) - } - }) - } - if (options?.ssrFiles?.length) { - mapFiles(options.ssrFiles, root).then((files) => { + const { root } = server.config + for (const environment of Object.values(server.environments)) { + mapFiles(environment.options.dev.warmup, root).then((files) => { for (const file of files) { - warmupFile(server, file, true) + warmupFile(server, server.environments.client, file) } }) } } -async function warmupFile(server: ViteDevServer, file: string, ssr: boolean) { +async function warmupFile( + server: ViteDevServer, + environment: DevEnvironment, + file: string, +) { // transform html with the `transformIndexHtml` hook as Vite internals would // pre-transform the imported JS modules linked. this may cause `transformIndexHtml` // plugins to be executed twice, but that's probably fine. @@ -38,7 +34,7 @@ async function warmupFile(server: ViteDevServer, file: string, ssr: boolean) { await server.transformIndexHtml(url, html) } catch (e) { // Unexpected error, log the issue but avoid an unhandled exception - server.config.logger.error( + environment.logger.error( `Pre-transform error (${colors.cyan(file)}): ${e.message}`, { error: e, @@ -51,7 +47,7 @@ async function warmupFile(server: ViteDevServer, file: string, ssr: boolean) { // for other files, pass it through `transformRequest` with warmup else { const url = fileToUrl(file, server.config.root) - await server.warmupRequest(url, { ssr }) + await environment.warmupRequest(url) } } diff --git a/packages/vite/src/node/ssr/fetchModule.ts b/packages/vite/src/node/ssr/fetchModule.ts index 60c0cb0a416b57..bb27fd61aaf40a 100644 --- a/packages/vite/src/node/ssr/fetchModule.ts +++ b/packages/vite/src/node/ssr/fetchModule.ts @@ -1,15 +1,15 @@ import { pathToFileURL } from 'node:url' -import type { ModuleNode, TransformResult, ViteDevServer } from '..' -import type { InternalResolveOptionsWithOverrideConditions } from '../plugins/resolve' +import type { FetchResult } from 'vite/module-runner' +import type { EnvironmentModuleNode, TransformResult } from '..' import { tryNodeResolve } from '../plugins/resolve' import { isBuiltin, isExternalUrl, isFilePathESM } from '../utils' -import type { FetchResult } from '../../runtime/types' import { unwrapId } from '../../shared/utils' import { + MODULE_RUNNER_SOURCEMAPPING_SOURCE, SOURCEMAPPING_URL, - VITE_RUNTIME_SOURCEMAPPING_SOURCE, } from '../../shared/constants' import { genSourceMapUrl } from '../server/sourcemap' +import type { DevEnvironment } from '../server/environment' export interface FetchModuleOptions { inlineSourceMap?: boolean @@ -17,11 +17,11 @@ export interface FetchModuleOptions { } /** - * Fetch module information for Vite runtime. + * Fetch module information for Vite runner. * @experimental */ export async function fetchModule( - server: ViteDevServer, + environment: DevEnvironment, url: string, importer?: string, options: FetchModuleOptions = {}, @@ -36,33 +36,35 @@ export async function fetchModule( } if (url[0] !== '.' && url[0] !== '/') { - const { - isProduction, - resolve: { dedupe, preserveSymlinks }, - root, - ssr, - } = server.config - const overrideConditions = ssr.resolve?.externalConditions || [] - - const resolveOptions: InternalResolveOptionsWithOverrideConditions = { - mainFields: ['main'], - conditions: [], - overrideConditions: [...overrideConditions, 'production', 'development'], - extensions: ['.js', '.cjs', '.json'], - dedupe, - preserveSymlinks, - isBuild: false, - isProduction, - root, - ssrConfig: ssr, - packageCache: server.config.packageCache, - } + const { isProduction, root } = environment.config + const { externalConditions, dedupe, preserveSymlinks } = + environment.options.resolve const resolved = tryNodeResolve( url, importer, - { ...resolveOptions, tryEsmOnly: true }, - false, + { + mainFields: ['main'], + conditions: [], + externalConditions, + external: [], + noExternal: [], + overrideConditions: [ + ...externalConditions, + 'production', + 'development', + ], + extensions: ['.js', '.cjs', '.json'], + dedupe, + preserveSymlinks, + isBuild: false, + isProduction, + root, + packageCache: environment.config.packageCache, + tryEsmOnly: true, + webCompatible: environment.options.webCompatible, + nodeCompatible: environment.options.nodeCompatible, + }, undefined, true, ) @@ -74,7 +76,7 @@ export async function fetchModule( throw err } const file = pathToFileURL(resolved.id).toString() - const type = isFilePathESM(resolved.id, server.config.packageCache) + const type = isFilePathESM(resolved.id, environment.config.packageCache) ? 'module' : 'commonjs' return { externalize: file, type } @@ -82,7 +84,7 @@ export async function fetchModule( url = unwrapId(url) - let result = await server.transformRequest(url, { ssr: true }) + let result = await environment.transformRequest(url) if (!result) { throw new Error( @@ -93,7 +95,7 @@ export async function fetchModule( } // module entry should be created by transformRequest - const mod = await server.moduleGraph.getModuleByUrl(url, true) + const mod = await environment.moduleGraph.getModuleByUrl(url) if (!mod) { throw new Error( @@ -120,7 +122,7 @@ const OTHER_SOURCE_MAP_REGEXP = new RegExp( ) function inlineSourceMap( - mod: ModuleNode, + mod: EnvironmentModuleNode, result: TransformResult, processSourceMap?: FetchModuleOptions['processSourceMap'], ) { @@ -130,7 +132,7 @@ function inlineSourceMap( if ( !map || !('version' in map) || - code.includes(VITE_RUNTIME_SOURCEMAPPING_SOURCE) + code.includes(MODULE_RUNNER_SOURCEMAPPING_SOURCE) ) return result @@ -142,7 +144,7 @@ function inlineSourceMap( const sourceMap = processSourceMap?.(map) || map result.code = `${code.trimEnd()}\n//# sourceURL=${ mod.id - }\n${VITE_RUNTIME_SOURCEMAPPING_SOURCE}\n//# ${SOURCEMAPPING_URL}=${genSourceMapUrl(sourceMap)}\n` + }\n${MODULE_RUNNER_SOURCEMAPPING_SOURCE}\n//# ${SOURCEMAPPING_URL}=${genSourceMapUrl(sourceMap)}\n` return result } diff --git a/packages/vite/src/node/ssr/index.ts b/packages/vite/src/node/ssr/index.ts index 3847e69544b2c0..4b0626c58d76b9 100644 --- a/packages/vite/src/node/ssr/index.ts +++ b/packages/vite/src/node/ssr/index.ts @@ -4,14 +4,31 @@ export type SSRTarget = 'node' | 'webworker' export type SsrDepOptimizationOptions = DepOptimizationConfig +/** + * @deprecated use environments.ssr + */ export interface SSROptions { + /** + * @deprecated use environment.resolve.noExternal + */ noExternal?: string | RegExp | (string | RegExp)[] | true + /** + * @deprecated use environment.resolve.external + */ external?: string[] | true /** * Define the target for the ssr build. The browser field in package.json * is ignored for node but used if webworker is the target + * + * if (ssr.target === 'webworker') { + * build.rollupOptions.entryFileNames = '[name].js' + * build.rollupOptions.inlineDynamicImports = (typeof input === 'string' || Object.keys(input).length === 1)) + * webCompatible = true + * } + * * @default 'node' + * @deprecated use environment.webCompatible */ target?: SSRTarget @@ -22,9 +39,13 @@ export interface SSROptions { * During dev: * explicit no external CJS dependencies are optimized by default * @experimental + * @deprecated */ optimizeDeps?: SsrDepOptimizationOptions + /** + * @deprecated + */ resolve?: { /** * Conditions that are used in the plugin pipeline. The default value is the root config's `resolve.conditions`. @@ -32,6 +53,7 @@ export interface SSROptions { * Use this to override the default ssr conditions for the ssr build. * * @default rootConfig.resolve.conditions + * @deprecated */ conditions?: string[] @@ -39,6 +61,7 @@ export interface SSROptions { * Conditions that are used during ssr import (including `ssrLoadModule`) of externalized dependencies. * * @default [] + * @deprecated */ externalConditions?: string[] } diff --git a/packages/vite/src/node/ssr/runtime/__tests__/fixtures/default-string.ts b/packages/vite/src/node/ssr/runtime/__tests__/fixtures/default-string.ts new file mode 100644 index 00000000000000..6b473bf83e5380 --- /dev/null +++ b/packages/vite/src/node/ssr/runtime/__tests__/fixtures/default-string.ts @@ -0,0 +1,3 @@ +const str: string = 'hello world' + +export default str diff --git a/packages/vite/src/node/ssr/runtime/__tests__/fixtures/worker.mjs b/packages/vite/src/node/ssr/runtime/__tests__/fixtures/worker.mjs new file mode 100644 index 00000000000000..bc617d0300e69d --- /dev/null +++ b/packages/vite/src/node/ssr/runtime/__tests__/fixtures/worker.mjs @@ -0,0 +1,35 @@ +// @ts-check + +import { BroadcastChannel, parentPort } from 'node:worker_threads' +import { fileURLToPath } from 'node:url' +import { ESModulesEvaluator, ModuleRunner, RemoteRunnerTransport } from 'vite/module-runner' + +if (!parentPort) { + throw new Error('File "worker.js" must be run in a worker thread') +} + +const runner = new ModuleRunner( + { + root: fileURLToPath(new URL('./', import.meta.url)), + transport: new RemoteRunnerTransport({ + onMessage: listener => { + parentPort?.on('message', listener) + }, + send: message => { + parentPort?.postMessage(message) + } + }) + }, + new ESModulesEvaluator(), +) + +const channel = new BroadcastChannel('vite-worker') +channel.onmessage = async (message) => { + try { + const mod = await runner.import(message.data.id) + channel.postMessage({ result: mod.default }) + } catch (e) { + channel.postMessage({ error: e.stack }) + } +} +parentPort.postMessage('ready') \ No newline at end of file diff --git a/packages/vite/src/node/ssr/runtime/__tests__/server-hmr.spec.ts b/packages/vite/src/node/ssr/runtime/__tests__/server-hmr.spec.ts index ccc822f543cefc..997df1f12095b7 100644 --- a/packages/vite/src/node/ssr/runtime/__tests__/server-hmr.spec.ts +++ b/packages/vite/src/node/ssr/runtime/__tests__/server-hmr.spec.ts @@ -1,39 +1,39 @@ import { describe, expect } from 'vitest' -import { createViteRuntimeTester } from './utils' +import { createModuleRunnerTester } from './utils' describe( - 'vite-runtime hmr works as expected', + 'module runner hmr works as expected', async () => { - const it = await createViteRuntimeTester({ + const it = await createModuleRunnerTester({ server: { // override watch options because it's disabled by default watch: {}, }, }) - it('hmr options are defined', async ({ runtime }) => { - expect(runtime.hmrClient).toBeDefined() + it('hmr options are defined', async ({ runner }) => { + expect(runner.hmrClient).toBeDefined() - const mod = await runtime.executeUrl('/fixtures/hmr.js') + const mod = await runner.import('/fixtures/hmr.js') expect(mod).toHaveProperty('hmr') expect(mod.hmr).toHaveProperty('accept') }) - it('correctly populates hmr client', async ({ runtime }) => { - const mod = await runtime.executeUrl('/fixtures/d') + it('correctly populates hmr client', async ({ runner }) => { + const mod = await runner.import('/fixtures/d') expect(mod.d).toBe('a') const fixtureC = '/fixtures/c.ts' const fixtureD = '/fixtures/d.ts' - expect(runtime.hmrClient!.hotModulesMap.size).toBe(2) - expect(runtime.hmrClient!.dataMap.size).toBe(2) - expect(runtime.hmrClient!.ctxToListenersMap.size).toBe(2) + expect(runner.hmrClient!.hotModulesMap.size).toBe(2) + expect(runner.hmrClient!.dataMap.size).toBe(2) + expect(runner.hmrClient!.ctxToListenersMap.size).toBe(2) for (const fixture of [fixtureC, fixtureD]) { - expect(runtime.hmrClient!.hotModulesMap.has(fixture)).toBe(true) - expect(runtime.hmrClient!.dataMap.has(fixture)).toBe(true) - expect(runtime.hmrClient!.ctxToListenersMap.has(fixture)).toBe(true) + expect(runner.hmrClient!.hotModulesMap.has(fixture)).toBe(true) + expect(runner.hmrClient!.dataMap.has(fixture)).toBe(true) + expect(runner.hmrClient!.ctxToListenersMap.has(fixture)).toBe(true) } }) }, diff --git a/packages/vite/src/node/ssr/runtime/__tests__/server-no-hmr.spec.ts b/packages/vite/src/node/ssr/runtime/__tests__/server-no-hmr.spec.ts index ea2816756c927f..d4cf03c756c565 100644 --- a/packages/vite/src/node/ssr/runtime/__tests__/server-no-hmr.spec.ts +++ b/packages/vite/src/node/ssr/runtime/__tests__/server-no-hmr.spec.ts @@ -1,8 +1,8 @@ import { describe, expect } from 'vitest' -import { createViteRuntimeTester } from './utils' +import { createModuleRunnerTester } from './utils' -describe('vite-runtime hmr works as expected', async () => { - const it = await createViteRuntimeTester({ +describe('module runner hmr works as expected', async () => { + const it = await createModuleRunnerTester({ server: { // override watch options because it's disabled by default watch: {}, @@ -10,10 +10,10 @@ describe('vite-runtime hmr works as expected', async () => { }, }) - it("hmr client is not defined if it's disabled", async ({ runtime }) => { - expect(runtime.hmrClient).toBeUndefined() + it("hmr client is not defined if it's disabled", async ({ runner }) => { + expect(runner.hmrClient).toBeUndefined() - const mod = await runtime.executeUrl('/fixtures/hmr.js') + const mod = await runner.import('/fixtures/hmr.js') expect(mod).toHaveProperty('hmr') expect(mod.hmr).toBeUndefined() }) diff --git a/packages/vite/src/node/ssr/runtime/__tests__/server-runtime.spec.ts b/packages/vite/src/node/ssr/runtime/__tests__/server-runtime.spec.ts index bcf06bb91d4005..d6323eaf9daf5f 100644 --- a/packages/vite/src/node/ssr/runtime/__tests__/server-runtime.spec.ts +++ b/packages/vite/src/node/ssr/runtime/__tests__/server-runtime.spec.ts @@ -3,42 +3,42 @@ import { posix, win32 } from 'node:path' import { fileURLToPath } from 'node:url' import { describe, expect } from 'vitest' import { isWindows } from '../../../../shared/utils' -import { createViteRuntimeTester } from './utils' +import { createModuleRunnerTester } from './utils' const _URL = URL -describe('vite-runtime initialization', async () => { - const it = await createViteRuntimeTester() +describe('module runner initialization', async () => { + const it = await createModuleRunnerTester() - it('correctly runs ssr code', async ({ runtime }) => { - const mod = await runtime.executeUrl('/fixtures/simple.js') + it('correctly runs ssr code', async ({ runner }) => { + const mod = await runner.import('/fixtures/simple.js') expect(mod.test).toEqual('I am initialized') // loads the same module if id is a file url const fileUrl = new _URL('./fixtures/simple.js', import.meta.url) - const mod2 = await runtime.executeUrl(fileUrl.toString()) + const mod2 = await runner.import(fileUrl.toString()) expect(mod).toBe(mod2) // loads the same module if id is a file path const filePath = fileURLToPath(fileUrl) - const mod3 = await runtime.executeUrl(filePath) + const mod3 = await runner.import(filePath) expect(mod).toBe(mod3) }) - it('can load virtual modules as an entry point', async ({ runtime }) => { - const mod = await runtime.executeEntrypoint('virtual:test') + it('can load virtual modules as an entry point', async ({ runner }) => { + const mod = await runner.import('virtual:test') expect(mod.msg).toBe('virtual') }) - it('css is loaded correctly', async ({ runtime }) => { - const css = await runtime.executeUrl('/fixtures/test.css') + it('css is loaded correctly', async ({ runner }) => { + const css = await runner.import('/fixtures/test.css') expect(css.default).toMatchInlineSnapshot(` ".test { color: red; } " `) - const module = await runtime.executeUrl('/fixtures/test.module.css') + const module = await runner.import('/fixtures/test.module.css') expect(module).toMatchObject({ default: { test: expect.stringMatching(/^_test_/), @@ -47,8 +47,8 @@ describe('vite-runtime initialization', async () => { }) }) - it('assets are loaded correctly', async ({ runtime }) => { - const assets = await runtime.executeUrl('/fixtures/assets.js') + it('assets are loaded correctly', async ({ runner }) => { + const assets = await runner.import('/fixtures/assets.js') expect(assets).toMatchObject({ mov: '/fixtures/assets/placeholder.mov', txt: '/fixtures/assets/placeholder.txt', @@ -57,17 +57,17 @@ describe('vite-runtime initialization', async () => { }) }) - it('ids with Vite queries are loaded correctly', async ({ runtime }) => { - const raw = await runtime.executeUrl('/fixtures/simple.js?raw') + it('ids with Vite queries are loaded correctly', async ({ runner }) => { + const raw = await runner.import('/fixtures/simple.js?raw') expect(raw.default).toMatchInlineSnapshot(` "export const test = 'I am initialized' import.meta.hot?.accept() " `) - const url = await runtime.executeUrl('/fixtures/simple.js?url') + const url = await runner.import('/fixtures/simple.js?url') expect(url.default).toMatchInlineSnapshot(`"/fixtures/simple.js"`) - const inline = await runtime.executeUrl('/fixtures/test.css?inline') + const inline = await runner.import('/fixtures/test.css?inline') expect(inline.default).toMatchInlineSnapshot(` ".test { color: red; @@ -77,16 +77,16 @@ describe('vite-runtime initialization', async () => { }) it('modules with query strings are treated as different modules', async ({ - runtime, + runner, }) => { - const modSimple = await runtime.executeUrl('/fixtures/simple.js') - const modUrl = await runtime.executeUrl('/fixtures/simple.js?url') + const modSimple = await runner.import('/fixtures/simple.js') + const modUrl = await runner.import('/fixtures/simple.js?url') expect(modSimple).not.toBe(modUrl) expect(modUrl.default).toBe('/fixtures/simple.js') }) - it('exports is not modifiable', async ({ runtime }) => { - const mod = await runtime.executeUrl('/fixtures/simple.js') + it('exports is not modifiable', async ({ runner }) => { + const mod = await runner.import('/fixtures/simple.js') expect(Object.isSealed(mod)).toBe(true) expect(() => { mod.test = 'I am modified' @@ -110,11 +110,11 @@ describe('vite-runtime initialization', async () => { ) }) - it('throws the same error', async ({ runtime }) => { + it('throws the same error', async ({ runner }) => { expect.assertions(3) const s = Symbol() try { - await runtime.executeUrl('/fixtures/has-error.js') + await runner.import('/fixtures/has-error.js') } catch (e) { expect(e[s]).toBeUndefined() e[s] = true @@ -122,16 +122,15 @@ describe('vite-runtime initialization', async () => { } try { - await runtime.executeUrl('/fixtures/has-error.js') + await runner.import('/fixtures/has-error.js') } catch (e) { expect(e[s]).toBe(true) } }) - it('importing external cjs library checks exports', async ({ runtime }) => { - await expect(() => - runtime.executeUrl('/fixtures/cjs-external-non-existing.js'), - ).rejects.toThrowErrorMatchingInlineSnapshot(` + it('importing external cjs library checks exports', async ({ runner }) => { + await expect(() => runner.import('/fixtures/cjs-external-non-existing.js')) + .rejects.toThrowErrorMatchingInlineSnapshot(` [SyntaxError: [vite] Named export 'nonExisting' not found. The requested module '@vitejs/cjs-external' is a CommonJS module, which may not support all module.exports as named exports. CommonJS modules can always be imported via the default export, for example using: @@ -141,28 +140,28 @@ describe('vite-runtime initialization', async () => { `) // subsequent imports of the same external package should not throw if imports are correct await expect( - runtime.executeUrl('/fixtures/cjs-external-existing.js'), + runner.import('/fixtures/cjs-external-existing.js'), ).resolves.toMatchObject({ result: 'world', }) }) - it('importing external esm library checks exports', async ({ runtime }) => { + it('importing external esm library checks exports', async ({ runner }) => { await expect(() => - runtime.executeUrl('/fixtures/esm-external-non-existing.js'), + runner.import('/fixtures/esm-external-non-existing.js'), ).rejects.toThrowErrorMatchingInlineSnapshot( `[SyntaxError: [vite] The requested module '@vitejs/esm-external' does not provide an export named 'nonExisting']`, ) // subsequent imports of the same external package should not throw if imports are correct await expect( - runtime.executeUrl('/fixtures/esm-external-existing.js'), + runner.import('/fixtures/esm-external-existing.js'), ).resolves.toMatchObject({ result: 'world', }) }) - it("dynamic import doesn't produce duplicates", async ({ runtime }) => { - const mod = await runtime.executeUrl('/fixtures/dynamic-import.js') + it("dynamic import doesn't produce duplicates", async ({ runner }) => { + const mod = await runner.import('/fixtures/dynamic-import.js') const modules = await mod.initialize() // toBe checks that objects are actually the same, not just structually // using toEqual here would be a mistake because it chesk the structural difference @@ -172,14 +171,14 @@ describe('vite-runtime initialization', async () => { expect(modules.static).toBe(modules.dynamicAbsoluteExtension) }) - it('correctly imports a virtual module', async ({ runtime }) => { - const mod = await runtime.executeUrl('/fixtures/virtual.js') + it('correctly imports a virtual module', async ({ runner }) => { + const mod = await runner.import('/fixtures/virtual.js') expect(mod.msg0).toBe('virtual0') expect(mod.msg).toBe('virtual') }) - it('importing package from node_modules', async ({ runtime }) => { - const mod = (await runtime.executeUrl( + it('importing package from node_modules', async ({ runner }) => { + const mod = (await runner.import( '/fixtures/installed.js', )) as typeof import('tinyspy') const fn = mod.spy() @@ -187,17 +186,14 @@ describe('vite-runtime initialization', async () => { expect(fn.called).toBe(true) }) - it('importing native node package', async ({ runtime }) => { - const mod = await runtime.executeUrl('/fixtures/native.js') + it('importing native node package', async ({ runner }) => { + const mod = await runner.import('/fixtures/native.js') expect(mod.readdirSync).toBe(readdirSync) expect(mod.existsSync).toBe(existsSync) }) - it('correctly resolves module url', async ({ runtime, server }) => { - const { meta } = - await runtime.executeUrl( - '/fixtures/basic', - ) + it('correctly resolves module url', async ({ runner, server }) => { + const { meta } = await runner.import('/fixtures/basic') const basicUrl = new _URL('./fixtures/basic.js', import.meta.url).toString() expect(meta.url).toBe(basicUrl) diff --git a/packages/vite/src/node/ssr/runtime/__tests__/server-source-maps.spec.ts b/packages/vite/src/node/ssr/runtime/__tests__/server-source-maps.spec.ts index fd8973235af0b6..cc97a44cc494ef 100644 --- a/packages/vite/src/node/ssr/runtime/__tests__/server-source-maps.spec.ts +++ b/packages/vite/src/node/ssr/runtime/__tests__/server-source-maps.spec.ts @@ -1,9 +1,9 @@ import { describe, expect } from 'vitest' -import type { ViteRuntime } from 'vite/runtime' -import { createViteRuntimeTester, editFile, resolvePath } from './utils' +import type { ModuleRunner } from 'vite/module-runner' +import { createModuleRunnerTester, editFile, resolvePath } from './utils' -describe('vite-runtime initialization', async () => { - const it = await createViteRuntimeTester( +describe('module runner initialization', async () => { + const it = await createModuleRunnerTester( {}, { sourcemapInterceptor: 'prepareStackTrace', @@ -18,32 +18,32 @@ describe('vite-runtime initialization', async () => { return err } } - const serializeStack = (runtime: ViteRuntime, err: Error) => { - return err.stack!.split('\n')[1].replace(runtime.options.root, '') + const serializeStack = (runner: ModuleRunner, err: Error) => { + return err.stack!.split('\n')[1].replace(runner.options.root, '') } - const serializeStackDeep = (runtime: ViteRuntime, err: Error) => { + const serializeStackDeep = (runtime: ModuleRunner, err: Error) => { return err .stack!.split('\n') .map((s) => s.replace(runtime.options.root, '')) } it('source maps are correctly applied to stack traces', async ({ - runtime, + runner, server, }) => { expect.assertions(3) const topLevelError = await getError(() => - runtime.executeUrl('/fixtures/has-error.js'), + runner.import('/fixtures/has-error.js'), ) - expect(serializeStack(runtime, topLevelError)).toBe( + expect(serializeStack(runner, topLevelError)).toBe( ' at /fixtures/has-error.js:2:7', ) const methodError = await getError(async () => { - const mod = await runtime.executeUrl('/fixtures/throws-error-method.ts') + const mod = await runner.import('/fixtures/throws-error-method.ts') mod.throwError() }) - expect(serializeStack(runtime, methodError)).toBe( + expect(serializeStack(runner, methodError)).toBe( ' at Module.throwError (/fixtures/throws-error-method.ts:6:9)', ) @@ -52,25 +52,25 @@ describe('vite-runtime initialization', async () => { resolvePath(import.meta.url, './fixtures/throws-error-method.ts'), (code) => '\n\n\n\n\n' + code + '\n', ) - runtime.moduleCache.clear() - server.moduleGraph.invalidateAll() + runner.moduleCache.clear() + server.environments.ssr.moduleGraph.invalidateAll() // TODO: environment? const methodErrorNew = await getError(async () => { - const mod = await runtime.executeUrl('/fixtures/throws-error-method.ts') + const mod = await runner.import('/fixtures/throws-error-method.ts') mod.throwError() }) - expect(serializeStack(runtime, methodErrorNew)).toBe( + expect(serializeStack(runner, methodErrorNew)).toBe( ' at Module.throwError (/fixtures/throws-error-method.ts:11:9)', ) }) - it('deep stacktrace', async ({ runtime }) => { + it('deep stacktrace', async ({ runner }) => { const methodError = await getError(async () => { - const mod = await runtime.executeUrl('/fixtures/has-error-deep.ts') + const mod = await runner.import('/fixtures/has-error-deep.ts') mod.main() }) - expect(serializeStackDeep(runtime, methodError).slice(0, 3)).toEqual([ + expect(serializeStackDeep(runner, methodError).slice(0, 3)).toEqual([ 'Error: crash', ' at crash (/fixtures/has-error-deep.ts:2:9)', ' at Module.main (/fixtures/has-error-deep.ts:6:3)', diff --git a/packages/vite/src/node/ssr/runtime/__tests__/server-worker-runner.spec.ts b/packages/vite/src/node/ssr/runtime/__tests__/server-worker-runner.spec.ts new file mode 100644 index 00000000000000..70c519b1ad5925 --- /dev/null +++ b/packages/vite/src/node/ssr/runtime/__tests__/server-worker-runner.spec.ts @@ -0,0 +1,67 @@ +import { BroadcastChannel, Worker } from 'node:worker_threads' +import { describe, expect, it, onTestFinished } from 'vitest' +import { DevEnvironment } from '../../../server/environment' +import { createServer } from '../../../server' +import { RemoteEnvironmentTransport } from '../../..' + +describe('running module runner inside a worker', () => { + it('correctly runs ssr code', async () => { + expect.assertions(1) + const worker = new Worker( + new URL('./fixtures/worker.mjs', import.meta.url), + { + stdout: true, + }, + ) + await new Promise((resolve, reject) => { + worker.on('message', () => resolve()) + worker.on('error', reject) + }) + const server = await createServer({ + root: __dirname, + logLevel: 'error', + server: { + middlewareMode: true, + watch: null, + hmr: { + port: 9609, + }, + }, + environments: { + worker: { + dev: { + createEnvironment: (name, config) => { + return new DevEnvironment(name, config, { + runner: { + transport: new RemoteEnvironmentTransport({ + send: (data) => worker.postMessage(data), + onMessage: (handler) => worker.on('message', handler), + }), + }, + }) + }, + }, + }, + }, + }) + onTestFinished(() => { + server.close() + worker.terminate() + }) + const channel = new BroadcastChannel('vite-worker') + return new Promise((resolve, reject) => { + channel.onmessage = (event) => { + try { + expect((event as MessageEvent).data).toEqual({ + result: 'hello world', + }) + } catch (e) { + reject(e) + } finally { + resolve() + } + } + channel.postMessage({ id: './fixtures/default-string.ts' }) + }) + }) +}) diff --git a/packages/vite/src/node/ssr/runtime/__tests__/utils.ts b/packages/vite/src/node/ssr/runtime/__tests__/utils.ts index 7e14bb986e828a..3a15a69fc2f13f 100644 --- a/packages/vite/src/node/ssr/runtime/__tests__/utils.ts +++ b/packages/vite/src/node/ssr/runtime/__tests__/utils.ts @@ -3,21 +3,23 @@ import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import type { TestAPI } from 'vitest' import { afterEach, beforeEach, test } from 'vitest' -import type { ViteRuntime } from 'vite/runtime' -import type { MainThreadRuntimeOptions } from '../mainThreadRuntime' +import type { ModuleRunner } from 'vite/module-runner' +import type { ServerModuleRunnerOptions } from '../serverModuleRunner' import type { ViteDevServer } from '../../../server' import type { InlineConfig } from '../../../config' import { createServer } from '../../../server' -import { createViteRuntime } from '../mainThreadRuntime' +import { createServerModuleRunner } from '../serverModuleRunner' +import type { DevEnvironment } from '../../../server/environment' interface TestClient { server: ViteDevServer - runtime: ViteRuntime + runner: ModuleRunner + environment: DevEnvironment } -export async function createViteRuntimeTester( +export async function createModuleRunnerTester( config: InlineConfig = {}, - runtimeConfig: MainThreadRuntimeOptions = {}, + runnerConfig: ServerModuleRunnerOptions = {}, ): Promise> { function waitForWatcher(server: ViteDevServer) { return new Promise((resolve) => { @@ -73,13 +75,14 @@ export async function createViteRuntimeTester( ], ...config, }) - t.runtime = await createViteRuntime(t.server, { + t.environment = t.server.environments.ssr + t.runner = await createServerModuleRunner(t.server, t.environment, { hmr: { logger: false, }, // don't override by default so Vitest source maps are correct sourcemapInterceptor: false, - ...runtimeConfig, + ...runnerConfig, }) if (config.server?.watch) { await waitForWatcher(t.server) @@ -87,7 +90,7 @@ export async function createViteRuntimeTester( }) afterEach(async (t) => { - await t.runtime.destroy() + await t.runner.destroy() await t.server.close() }) diff --git a/packages/vite/src/node/ssr/runtime/serverHmrConnector.ts b/packages/vite/src/node/ssr/runtime/serverHmrConnector.ts index b8bed32a8733c2..c67dfac32639d8 100644 --- a/packages/vite/src/node/ssr/runtime/serverHmrConnector.ts +++ b/packages/vite/src/node/ssr/runtime/serverHmrConnector.ts @@ -1,5 +1,5 @@ import type { CustomPayload, HMRPayload } from 'types/hmrPayload' -import type { HMRRuntimeConnection } from 'vite/runtime' +import type { ModuleRunnerHMRConnection } from 'vite/module-runner' import type { ViteDevServer } from '../../server' import type { HMRBroadcasterClient, ServerHMRChannel } from '../../server/hmr' @@ -30,7 +30,7 @@ class ServerHMRBroadcasterClient implements HMRBroadcasterClient { * The connector class to establish HMR communication between the server and the Vite runtime. * @experimental */ -export class ServerHMRConnector implements HMRRuntimeConnection { +export class ServerHMRConnector implements ModuleRunnerHMRConnection { private handlers: ((payload: HMRPayload) => void)[] = [] private hmrChannel: ServerHMRChannel private hmrClient: ServerHMRBroadcasterClient diff --git a/packages/vite/src/node/ssr/runtime/mainThreadRuntime.ts b/packages/vite/src/node/ssr/runtime/serverModuleRunner.ts similarity index 54% rename from packages/vite/src/node/ssr/runtime/mainThreadRuntime.ts rename to packages/vite/src/node/ssr/runtime/serverModuleRunner.ts index cbb8e3d8edfbdd..d4f6d42297b851 100644 --- a/packages/vite/src/node/ssr/runtime/mainThreadRuntime.ts +++ b/packages/vite/src/node/ssr/runtime/serverModuleRunner.ts @@ -1,36 +1,51 @@ import { existsSync, readFileSync } from 'node:fs' -import { ESModulesRunner, ViteRuntime } from 'vite/runtime' -import type { ViteModuleRunner, ViteRuntimeOptions } from 'vite/runtime' +import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner' +import type { + ModuleEvaluator, + ModuleRunnerHMRConnection, + ModuleRunnerHmr, + ModuleRunnerOptions, +} from 'vite/module-runner' import type { ViteDevServer } from '../../server' -import type { HMRLogger } from '../../../shared/hmr' +import type { DevEnvironment } from '../../server/environment' import { ServerHMRConnector } from './serverHmrConnector' /** * @experimental */ -export interface MainThreadRuntimeOptions - extends Omit { +export interface ServerModuleRunnerOptions + extends Omit< + ModuleRunnerOptions, + 'root' | 'fetchModule' | 'hmr' | 'transport' + > { /** * Disable HMR or configure HMR logger. */ hmr?: | false | { - logger?: false | HMRLogger + connection?: ModuleRunnerHMRConnection + logger?: ModuleRunnerHmr['logger'] } /** - * Provide a custom module runner. This controls how the code is executed. + * Provide a custom module evaluator. This controls how the code is executed. */ - runner?: ViteModuleRunner + evaluator?: ModuleEvaluator } function createHMROptions( server: ViteDevServer, - options: MainThreadRuntimeOptions, + options: ServerModuleRunnerOptions, ) { if (server.config.server.hmr === false || options.hmr === false) { return false } + if (options.hmr?.connection) { + return { + connection: options.hmr.connection, + logger: options.hmr.logger, + } + } const connection = new ServerHMRConnector(server) return { connection, @@ -46,7 +61,7 @@ const prepareStackTrace = { }, } -function resolveSourceMapOptions(options: MainThreadRuntimeOptions) { +function resolveSourceMapOptions(options: ServerModuleRunnerOptions) { if (options.sourcemapInterceptor != null) { if (options.sourcemapInterceptor === 'prepareStackTrace') { return prepareStackTrace @@ -66,19 +81,22 @@ function resolveSourceMapOptions(options: MainThreadRuntimeOptions) { * Create an instance of the Vite SSR runtime that support HMR. * @experimental */ -export async function createViteRuntime( +export function createServerModuleRunner( server: ViteDevServer, - options: MainThreadRuntimeOptions = {}, -): Promise { + environment: DevEnvironment, + options: ServerModuleRunnerOptions = {}, +): ModuleRunner { const hmr = createHMROptions(server, options) - return new ViteRuntime( + return new ModuleRunner( { ...options, - root: server.config.root, - fetchModule: server.ssrFetchModule, + root: environment.config.root, + transport: { + fetchModule: (id, importer) => environment.fetchModule(id, importer), + }, hmr, sourcemapInterceptor: resolveSourceMapOptions(options), }, - options.runner || new ESModulesRunner(), + options.evaluator || new ESModulesEvaluator(), ) } diff --git a/packages/vite/src/node/ssr/ssrFetchModule.ts b/packages/vite/src/node/ssr/ssrFetchModule.ts deleted file mode 100644 index d0e1c98cca2569..00000000000000 --- a/packages/vite/src/node/ssr/ssrFetchModule.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { ViteDevServer } from '../server' -import type { FetchResult } from '../../runtime/types' -import { asyncFunctionDeclarationPaddingLineCount } from '../../shared/utils' -import { fetchModule } from './fetchModule' - -export function ssrFetchModule( - server: ViteDevServer, - id: string, - importer?: string, -): Promise { - return fetchModule(server, id, importer, { - processSourceMap(map) { - // this assumes that "new AsyncFunction" is used to create the module - return Object.assign({}, map, { - mappings: - ';'.repeat(asyncFunctionDeclarationPaddingLineCount) + map.mappings, - }) - }, - }) -} diff --git a/packages/vite/src/node/ssr/ssrModuleLoader.ts b/packages/vite/src/node/ssr/ssrModuleLoader.ts index 186922ff84c40b..49b4bcf14e1f82 100644 --- a/packages/vite/src/node/ssr/ssrModuleLoader.ts +++ b/packages/vite/src/node/ssr/ssrModuleLoader.ts @@ -1,32 +1,9 @@ -import path from 'node:path' -import { pathToFileURL } from 'node:url' import colors from 'picocolors' +import type { ModuleRunner } from 'vite/module-runner' import type { ViteDevServer } from '../server' -import { isBuiltin, isExternalUrl, isFilePathESM } from '../utils' -import { transformRequest } from '../server/transformRequest' -import type { InternalResolveOptionsWithOverrideConditions } from '../plugins/resolve' -import { tryNodeResolve } from '../plugins/resolve' -import { genSourceMapUrl } from '../server/sourcemap' -import { - AsyncFunction, - asyncFunctionDeclarationPaddingLineCount, - isWindows, - unwrapId, -} from '../../shared/utils' -import { - type SSRImportBaseMetadata, - analyzeImportedModDifference, - proxyGuardOnlyEsm, -} from '../../shared/ssrTransform' -import { SOURCEMAPPING_URL } from '../../shared/constants' -import { - ssrDynamicImportKey, - ssrExportAllKey, - ssrImportKey, - ssrImportMetaKey, - ssrModuleExportsKey, -} from './ssrTransform' +import { unwrapId } from '../../shared/utils' import { ssrFixStacktrace } from './ssrStacktrace' +import { createServerModuleRunner } from './runtime/serverModuleRunner' interface SSRContext { global: typeof globalThis @@ -34,232 +11,61 @@ interface SSRContext { type SSRModule = Record -interface NodeImportResolveOptions - extends InternalResolveOptionsWithOverrideConditions { - legacyProxySsrExternalModules?: boolean -} - -const pendingModules = new Map>() -const pendingImports = new Map() -const importErrors = new WeakMap() - export async function ssrLoadModule( url: string, server: ViteDevServer, - context: SSRContext = { global }, - urlStack: string[] = [], + _context: SSRContext = { global }, + _urlStack: string[] = [], fixStacktrace?: boolean, ): Promise { - url = unwrapId(url) + server.config.logger.warnOnce( + colors.yellow( + '`ssrLoadModule` is deprecated and will be removed in the next major version. ' + + 'Use `createServerModuleRunner(environment).import(url)` from "vite/module-runner" ' + + 'to load modules instead.', + ), + ) - // when we instantiate multiple dependency modules in parallel, they may - // point to shared modules. We need to avoid duplicate instantiation attempts - // by register every module as pending synchronously so that all subsequent - // request to that module are simply waiting on the same promise. - const pending = pendingModules.get(url) - if (pending) { - return pending - } + const runner = + server._ssrCompatModuleRunner || + (server._ssrCompatModuleRunner = createServerModuleRunner( + server, + server.environments.ssr, + { + sourcemapInterceptor: false, + }, + )) - const modulePromise = instantiateModule( - url, - server, - context, - urlStack, - fixStacktrace, - ) - pendingModules.set(url, modulePromise) - modulePromise - .catch(() => { - pendingImports.delete(url) - }) - .finally(() => { - pendingModules.delete(url) - }) - return modulePromise + url = unwrapId(url) + + return instantiateModule(url, runner, server, fixStacktrace) } async function instantiateModule( url: string, + runner: ModuleRunner, server: ViteDevServer, - context: SSRContext = { global }, - urlStack: string[] = [], fixStacktrace?: boolean, ): Promise { - const { moduleGraph } = server - const mod = await moduleGraph.ensureEntryFromUrl(url, true) + const environment = server.environments.ssr + const mod = await environment.moduleGraph.ensureEntryFromUrl(url) if (mod.ssrError) { throw mod.ssrError } - if (mod.ssrModule) { - return mod.ssrModule - } - const result = - mod.ssrTransformResult || - (await transformRequest(url, server, { ssr: true })) - if (!result) { - // TODO more info? is this even necessary? - throw new Error(`failed to load module for ssr: ${url}`) - } - - const ssrModule = { - [Symbol.toStringTag]: 'Module', - } - Object.defineProperty(ssrModule, '__esModule', { value: true }) - - // Tolerate circular imports by ensuring the module can be - // referenced before it's been instantiated. - mod.ssrModule = ssrModule - - // replace '/' with '\\' on Windows to match Node.js - const osNormalizedFilename = isWindows ? path.resolve(mod.file!) : mod.file! - - const ssrImportMeta = { - dirname: path.dirname(osNormalizedFilename), - filename: osNormalizedFilename, - // The filesystem URL, matching native Node.js modules - url: pathToFileURL(mod.file!).toString(), - } - - urlStack = urlStack.concat(url) - const isCircular = (url: string) => urlStack.includes(url) - - const { - isProduction, - resolve: { dedupe, preserveSymlinks }, - root, - ssr, - } = server.config - - const overrideConditions = ssr.resolve?.externalConditions || [] - - const resolveOptions: NodeImportResolveOptions = { - mainFields: ['main'], - conditions: [], - overrideConditions: [...overrideConditions, 'production', 'development'], - extensions: ['.js', '.cjs', '.json'], - dedupe, - preserveSymlinks, - isBuild: false, - isProduction, - root, - ssrConfig: ssr, - legacyProxySsrExternalModules: - server.config.legacy?.proxySsrExternalModules, - packageCache: server.config.packageCache, - } - - // Since dynamic imports can happen in parallel, we need to - // account for multiple pending deps and duplicate imports. - const pendingDeps: string[] = [] - - const ssrImport = async (dep: string, metadata?: SSRImportBaseMetadata) => { - try { - if (dep[0] !== '.' && dep[0] !== '/') { - return await nodeImport(dep, mod.file!, resolveOptions, metadata) - } - // convert to rollup URL because `pendingImports`, `moduleGraph.urlToModuleMap` requires that - dep = unwrapId(dep) - if (!isCircular(dep) && !pendingImports.get(dep)?.some(isCircular)) { - pendingDeps.push(dep) - if (pendingDeps.length === 1) { - pendingImports.set(url, pendingDeps) - } - const mod = await ssrLoadModule( - dep, - server, - context, - urlStack, - fixStacktrace, - ) - if (pendingDeps.length === 1) { - pendingImports.delete(url) - } else { - pendingDeps.splice(pendingDeps.indexOf(dep), 1) - } - // return local module to avoid race condition #5470 - return mod - } - return moduleGraph.urlToModuleMap.get(dep)?.ssrModule - } catch (err) { - // tell external error handler which mod was imported with error - importErrors.set(err, { importee: dep }) - - throw err - } - } - - const ssrDynamicImport = (dep: string) => { - // #3087 dynamic import vars is ignored at rewrite import path, - // so here need process relative path - if (dep[0] === '.') { - dep = path.posix.resolve(path.dirname(url), dep) - } - return ssrImport(dep, { isDynamicImport: true }) - } - - function ssrExportAll(sourceModule: any) { - for (const key in sourceModule) { - if (key !== 'default' && key !== '__esModule') { - Object.defineProperty(ssrModule, key, { - enumerable: true, - configurable: true, - get() { - return sourceModule[key] - }, - }) - } - } - } - - let sourceMapSuffix = '' - if (result.map && 'version' in result.map) { - const moduleSourceMap = Object.assign({}, result.map, { - mappings: - ';'.repeat(asyncFunctionDeclarationPaddingLineCount) + - result.map.mappings, - }) - sourceMapSuffix = `\n//# ${SOURCEMAPPING_URL}=${genSourceMapUrl(moduleSourceMap)}` - } - try { - const initModule = new AsyncFunction( - `global`, - ssrModuleExportsKey, - ssrImportMetaKey, - ssrImportKey, - ssrDynamicImportKey, - ssrExportAllKey, - '"use strict";' + - result.code + - `\n//# sourceURL=${mod.id}${sourceMapSuffix}`, - ) - await initModule( - context.global, - ssrModule, - ssrImportMeta, - ssrImport, - ssrDynamicImport, - ssrExportAll, - ) - } catch (e) { + const exports = await runner.import(url) + mod.ssrModule = exports + return exports + } catch (e: any) { mod.ssrError = e - const errorData = importErrors.get(e) - if (e.stack && fixStacktrace) { - ssrFixStacktrace(e, moduleGraph) + ssrFixStacktrace(e, environment.moduleGraph) } - server.config.logger.error( - colors.red( - `Error when evaluating SSR module ${url}:` + - (errorData?.importee - ? ` failed to import "${errorData.importee}"` - : '') + - `\n|- ${e.stack}\n`, - ), + environment.logger.error( + colors.red(`Error when evaluating SSR module ${url}:\n|- ${e.stack}\n`), { timestamp: true, clear: server.config.clearScreen, @@ -269,82 +75,4 @@ async function instantiateModule( throw e } - - return Object.freeze(ssrModule) -} - -// In node@12+ we can use dynamic import to load CJS and ESM -async function nodeImport( - id: string, - importer: string, - resolveOptions: NodeImportResolveOptions, - metadata?: SSRImportBaseMetadata, -) { - let url: string - let filePath: string | undefined - if (id.startsWith('data:') || isExternalUrl(id) || isBuiltin(id)) { - url = id - } else { - const resolved = tryNodeResolve( - id, - importer, - { ...resolveOptions, tryEsmOnly: true }, - false, - undefined, - true, - ) - if (!resolved) { - const err: any = new Error( - `Cannot find module '${id}' imported from '${importer}'`, - ) - err.code = 'ERR_MODULE_NOT_FOUND' - throw err - } - filePath = resolved.id - url = pathToFileURL(resolved.id).toString() - } - - const mod = await import(url) - - if (resolveOptions.legacyProxySsrExternalModules) { - return proxyESM(mod) - } else if (filePath) { - analyzeImportedModDifference( - mod, - id, - isFilePathESM(filePath, resolveOptions.packageCache) - ? 'module' - : undefined, - metadata, - ) - return proxyGuardOnlyEsm(mod, id) - } else { - return mod - } -} - -// rollup-style default import interop for cjs -function proxyESM(mod: any) { - // This is the only sensible option when the exports object is a primitive - if (isPrimitive(mod)) return { default: mod } - - let defaultExport = 'default' in mod ? mod.default : mod - - if (!isPrimitive(defaultExport) && '__esModule' in defaultExport) { - mod = defaultExport - if ('default' in defaultExport) { - defaultExport = defaultExport.default - } - } - - return new Proxy(mod, { - get(mod, prop) { - if (prop === 'default') return defaultExport - return mod[prop] ?? defaultExport?.[prop] - }, - }) -} - -function isPrimitive(value: any) { - return !value || (typeof value !== 'object' && typeof value !== 'function') } diff --git a/packages/vite/src/node/ssr/ssrStacktrace.ts b/packages/vite/src/node/ssr/ssrStacktrace.ts index a98af4dd94bb74..18489224ea4af6 100644 --- a/packages/vite/src/node/ssr/ssrStacktrace.ts +++ b/packages/vite/src/node/ssr/ssrStacktrace.ts @@ -1,6 +1,6 @@ import path from 'node:path' import { TraceMap, originalPositionFor } from '@jridgewell/trace-mapping' -import type { ModuleGraph } from '../server/moduleGraph' +import type { EnvironmentModuleGraph } from '..' let offset: number @@ -22,7 +22,7 @@ function calculateOffsetOnce() { export function ssrRewriteStacktrace( stack: string, - moduleGraph: ModuleGraph, + moduleGraph: EnvironmentModuleGraph, ): string { calculateOffsetOnce() return stack @@ -33,8 +33,8 @@ export function ssrRewriteStacktrace( (input, varName, id, line, column) => { if (!id) return input - const mod = moduleGraph.idToModuleMap.get(id) - const rawSourceMap = mod?.ssrTransformResult?.map + const mod = moduleGraph.getModuleById(id) + const rawSourceMap = mod?.transformResult?.map if (!rawSourceMap) { return input @@ -86,7 +86,10 @@ export function rebindErrorStacktrace(e: Error, stacktrace: string): void { const rewroteStacktraces = new WeakSet() -export function ssrFixStacktrace(e: Error, moduleGraph: ModuleGraph): void { +export function ssrFixStacktrace( + e: Error, + moduleGraph: EnvironmentModuleGraph, +): void { if (!e.stack) return // stacktrace shouldn't be rewritten more than once if (rewroteStacktraces.has(e)) return diff --git a/packages/vite/src/node/ssr/ssrTransform.ts b/packages/vite/src/node/ssr/ssrTransform.ts index c3800a48a8d138..8700565bc85eb8 100644 --- a/packages/vite/src/node/ssr/ssrTransform.ts +++ b/packages/vite/src/node/ssr/ssrTransform.ts @@ -2,8 +2,10 @@ import path from 'node:path' import MagicString from 'magic-string' import type { SourceMap } from 'rollup' import type { + CallExpression, Function as FunctionNode, Identifier, + Literal, Pattern, Property, VariableDeclaration, @@ -13,6 +15,7 @@ import { extract_names as extractNames } from 'periscopic' import { walk as eswalk } from 'estree-walker' import type { RawSourceMap } from '@ampproject/remapping' import { parseAstAsync as rollupParseAstAsync } from 'rollup/parseAst' +import type { ImportSpecifier } from 'es-module-lexer' import type { TransformResult } from '../server/transformRequest' import { combineSourcemaps, isDefined } from '../utils' import { isJSONRequest } from '../plugins/json' @@ -37,6 +40,8 @@ export const ssrImportMetaKey = `__vite_ssr_import_meta__` const hashbangRE = /^#!.*\n/ +// TODO: Should we rename to moduleRunnerTransform? + export async function ssrTransform( code: string, inMap: SourceMap | { mappings: '' } | null, @@ -59,6 +64,7 @@ async function ssrTransformJSON( map: inMap, deps: [], dynamicDeps: [], + ssr: true, } } @@ -322,11 +328,58 @@ async function ssrTransformScript( return { code: s.toString(), map, + ssr: true, deps: [...deps], dynamicDeps: [...dynamicDeps], } } +export async function ssrParseImports( + url: string, + code: string, +): Promise { + let ast: any + try { + ast = await rollupParseAstAsync(code) + } catch (err) { + if (!err.loc || !err.loc.line) throw err + const line = err.loc.line + throw new Error( + `Parse failure: ${ + err.message + }\nAt file: ${url}\nContents of line ${line}: ${ + code.split('\n')[line - 1] + }`, + ) + } + const imports: ImportSpecifier[] = [] + eswalk(ast, { + enter(_n, parent) { + if (_n.type !== 'Identifier') return + const node = _n as Node & Identifier + const isStaticImport = node.name === ssrImportKey + const isDynamicImport = node.name === ssrDynamicImportKey + if (isStaticImport || isDynamicImport) { + // this is a standardised output, so we can safely assume the parent and arguments + const importExpression = parent as Node & CallExpression + const importLiteral = importExpression.arguments[0] as Node & Literal + + imports.push({ + n: importLiteral.value as string | undefined, + s: importLiteral.start, + e: importLiteral.end, + se: importExpression.start, + ss: importExpression.end, + t: isStaticImport ? 2 : 1, + d: isDynamicImport ? importLiteral.start : -1, + a: -1, // not used + }) + } + }, + }) + return imports +} + interface Visitors { onIdentifier: ( node: Identifier & { diff --git a/packages/vite/src/node/tsconfig.json b/packages/vite/src/node/tsconfig.json index db8e56fa8449c5..a6108501ed36a4 100644 --- a/packages/vite/src/node/tsconfig.json +++ b/packages/vite/src/node/tsconfig.json @@ -1,12 +1,18 @@ { "extends": "../../tsconfig.base.json", - "include": ["./", "../runtime", "../dep-types", "../types", "constants.ts"], + "include": [ + "./", + "../module-runner", + "../dep-types", + "../types", + "constants.ts" + ], "exclude": ["../**/__tests__"], "compilerOptions": { "lib": ["ESNext", "DOM"], "stripInternal": true, "paths": { - "vite/runtime": ["../runtime"] + "vite/module-runner": ["../module-runner"] } } } diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index b9ceece25f40f2..3c16b01a1fcba8 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -1095,7 +1095,7 @@ function mergeConfigRecursively( merged[key] = [].concat(existing, value) continue } else if ( - key === 'noExternal' && + key === 'noExternal' && // TODO: environments rootPath === 'ssr' && (existing === true || value === true) ) { diff --git a/packages/vite/src/shared/constants.ts b/packages/vite/src/shared/constants.ts index 7c0e685d5abf6b..a12c674cc98ed6 100644 --- a/packages/vite/src/shared/constants.ts +++ b/packages/vite/src/shared/constants.ts @@ -19,5 +19,5 @@ export const NULL_BYTE_PLACEHOLDER = `__x00__` export let SOURCEMAPPING_URL = 'sourceMa' SOURCEMAPPING_URL += 'ppingURL' -export const VITE_RUNTIME_SOURCEMAPPING_SOURCE = - '//# sourceMappingSource=vite-runtime' +export const MODULE_RUNNER_SOURCEMAPPING_SOURCE = + '//# sourceMappingSource=vite-generated' diff --git a/packages/vite/src/shared/hmr.ts b/packages/vite/src/shared/hmr.ts index 0f2cb23b4ad71f..ee4f727158f60e 100644 --- a/packages/vite/src/shared/hmr.ts +++ b/packages/vite/src/shared/hmr.ts @@ -111,9 +111,12 @@ export class HMRContext implements ViteHotContext { path: this.ownerPath, message, }) - this.send('vite:invalidate', { path: this.ownerPath, message }) + this.send('vite:invalidate', { + path: this.ownerPath, + message, + }) this.hmrClient.logger.debug( - `[vite] invalidate ${this.ownerPath}${message ? `: ${message}` : ''}`, + `invalidate ${this.ownerPath}${message ? `: ${message}` : ''}`, ) } @@ -252,7 +255,7 @@ export class HMRClient { this.logger.error(err) } this.logger.error( - `[hmr] Failed to reload ${path}. ` + + `Failed to reload ${path}. ` + `This could be due to syntax errors or importing non-existent ` + `modules. (see errors above)`, ) @@ -313,7 +316,7 @@ export class HMRClient { ) } const loggedPath = isSelfUpdate ? path : `${acceptedPath} via ${path}` - this.logger.debug(`[vite] hot updated: ${loggedPath}`) + this.logger.debug(`hot updated: ${loggedPath}`) } } } diff --git a/packages/vite/types/hmrPayload.d.ts b/packages/vite/types/hmrPayload.d.ts index 79dc349d3c880c..2c6a9d0c2d1121 100644 --- a/packages/vite/types/hmrPayload.d.ts +++ b/packages/vite/types/hmrPayload.d.ts @@ -25,7 +25,7 @@ export interface Update { /** @internal */ isWithinCircularImport?: boolean /** @internal */ - ssrInvalidates?: string[] + invalidates?: string[] } export interface PrunePayload { diff --git a/playground/environment-react-ssr/__tests__/basic.spec.ts b/playground/environment-react-ssr/__tests__/basic.spec.ts new file mode 100644 index 00000000000000..4b98b37a2394f7 --- /dev/null +++ b/playground/environment-react-ssr/__tests__/basic.spec.ts @@ -0,0 +1,9 @@ +import { test } from 'vitest' +import { page } from '~utils' + +test('basic', async () => { + await page.getByText('hydrated: true').isVisible() + await page.getByText('Count: 0').isVisible() + await page.getByRole('button', { name: '+' }).click() + await page.getByText('Count: 1').isVisible() +}) diff --git a/playground/environment-react-ssr/index.html b/playground/environment-react-ssr/index.html new file mode 100644 index 00000000000000..9f4d44a675c1b1 --- /dev/null +++ b/playground/environment-react-ssr/index.html @@ -0,0 +1,14 @@ + + + + + environment-react-ssr + + + + + + diff --git a/playground/environment-react-ssr/package.json b/playground/environment-react-ssr/package.json new file mode 100644 index 00000000000000..77228c8054c5c8 --- /dev/null +++ b/playground/environment-react-ssr/package.json @@ -0,0 +1,17 @@ +{ + "name": "@vitejs/test-environment-react-ssr", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build --all", + "preview": "vite preview" + }, + "devDependencies": { + "@types/react": "^18.2.73", + "@types/react-dom": "^18.2.23", + "react": "^18.2.0", + "react-dom": "^18.2.0" + } +} diff --git a/playground/environment-react-ssr/src/entry-client.tsx b/playground/environment-react-ssr/src/entry-client.tsx new file mode 100644 index 00000000000000..e33d677abfbab2 --- /dev/null +++ b/playground/environment-react-ssr/src/entry-client.tsx @@ -0,0 +1,12 @@ +import ReactDomClient from 'react-dom/client' +import React from 'react' +import Root from './root' + +async function main() { + const el = document.getElementById('root') + React.startTransition(() => { + ReactDomClient.hydrateRoot(el!, ) + }) +} + +main() diff --git a/playground/environment-react-ssr/src/entry-server.tsx b/playground/environment-react-ssr/src/entry-server.tsx new file mode 100644 index 00000000000000..9df5ef336b2c9f --- /dev/null +++ b/playground/environment-react-ssr/src/entry-server.tsx @@ -0,0 +1,24 @@ +import ReactDomServer from 'react-dom/server' +import type { Connect, ViteDevServer } from 'vite' +import Root from './root' + +const hanlder: Connect.NextHandleFunction = async (_req, res) => { + const ssrHtml = ReactDomServer.renderToString() + let html = await importHtml() + html = html.replace(//, `
${ssrHtml}
`) + res.setHeader('content-type', 'text/html').end(html) +} + +export default hanlder + +declare let __globalServer: ViteDevServer + +async function importHtml() { + if (import.meta.env.DEV) { + const mod = await import('/index.html?raw') + return __globalServer.transformIndexHtml('/', mod.default) + } else { + const mod = await import('/dist/client/index.html?raw') + return mod.default + } +} diff --git a/playground/environment-react-ssr/src/root.tsx b/playground/environment-react-ssr/src/root.tsx new file mode 100644 index 00000000000000..3d077cafb892ba --- /dev/null +++ b/playground/environment-react-ssr/src/root.tsx @@ -0,0 +1,19 @@ +import React from 'react' + +export default function Root() { + const [count, setCount] = React.useState(0) + + const [hydrated, setHydrated] = React.useState(false) + React.useEffect(() => { + setHydrated(true) + }, []) + + return ( +
+
hydrated: {String(hydrated)}
+
Count: {count}
+ + +
+ ) +} diff --git a/playground/environment-react-ssr/tsconfig.json b/playground/environment-react-ssr/tsconfig.json new file mode 100644 index 00000000000000..be3ffda527ca91 --- /dev/null +++ b/playground/environment-react-ssr/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "include": ["src"], + "compilerOptions": { + "jsx": "react-jsx" + } +} diff --git a/playground/environment-react-ssr/vite.config.ts b/playground/environment-react-ssr/vite.config.ts new file mode 100644 index 00000000000000..af09a78f8e7f23 --- /dev/null +++ b/playground/environment-react-ssr/vite.config.ts @@ -0,0 +1,90 @@ +import { + type Connect, + type Plugin, + type PluginOption, + createServerModuleRunner, + defineConfig, +} from 'vite' + +export default defineConfig((env) => ({ + clearScreen: false, + appType: 'custom', + plugins: [ + vitePluginSsrMiddleware({ + entry: '/src/entry-server', + preview: new URL('./dist/server/index.js', import.meta.url).toString(), + }), + { + name: 'global-server', + configureServer(server) { + Object.assign(globalThis, { __globalServer: server }) + }, + }, + ], + environments: { + client: { + build: { + minify: false, + sourcemap: true, + outDir: 'dist/client', + }, + }, + ssr: { + build: { + outDir: 'dist/server', + // [feedback] + // is this still meant to be used? + // for example, `ssr: true` seems to make `minify: false` automatically + // and also externalization. + ssr: true, + rollupOptions: { + input: { + index: '/src/entry-server', + }, + }, + }, + }, + }, + + builder: { + async buildEnvironments(builder, build) { + await build(builder.environments.client) + await build(builder.environments.ssr) + }, + }, +})) + +// vavite-style ssr middleware plugin +export function vitePluginSsrMiddleware({ + entry, + preview, +}: { + entry: string + preview?: string +}): PluginOption { + const plugin: Plugin = { + name: vitePluginSsrMiddleware.name, + + configureServer(server) { + const runner = createServerModuleRunner(server, server.environments.ssr) + const handler: Connect.NextHandleFunction = async (req, res, next) => { + try { + const mod = await runner.import(entry) + await mod['default'](req, res, next) + } catch (e) { + next(e) + } + } + return () => server.middlewares.use(handler) + }, + + async configurePreviewServer(server) { + if (preview) { + const mod = await import(preview) + return () => server.middlewares.use(mod.default) + } + return + }, + } + return [plugin] +} diff --git a/playground/hmr-ssr/__tests__/hmr-ssr.spec.ts b/playground/hmr-ssr/__tests__/hmr-ssr.spec.ts index f28b620f565131..570ab42f507073 100644 --- a/playground/hmr-ssr/__tests__/hmr-ssr.spec.ts +++ b/playground/hmr-ssr/__tests__/hmr-ssr.spec.ts @@ -4,8 +4,8 @@ import { dirname, posix, resolve } from 'node:path' import EventEmitter from 'node:events' import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest' import type { InlineConfig, Logger, ViteDevServer } from 'vite' -import { createServer, createViteRuntime } from 'vite' -import type { ViteRuntime } from 'vite/runtime' +import { createServer, createServerModuleRunner } from 'vite' +import type { ModuleRunner } from 'vite/module-runner' import type { RollupError } from 'rollup' import { addFile, @@ -19,7 +19,7 @@ import { let server: ViteDevServer const clientLogs: string[] = [] const serverLogs: string[] = [] -let runtime: ViteRuntime +let runner: ModuleRunner const logsEmitter = new EventEmitter() @@ -54,7 +54,7 @@ const updated = (file: string, via?: string) => { describe('hmr works correctly', () => { beforeAll(async () => { - await setupViteRuntime('/hmr.ts') + await setupModuleRunner('/hmr.ts') }) test('should connect', async () => { @@ -338,7 +338,7 @@ describe('acceptExports', () => { beforeAll(async () => { await untilConsoleLogAfter( - () => setupViteRuntime(`/${testDir}/index`), + () => setupModuleRunner(`/${testDir}/index`), [CONNECTED, />>>>>>/], (logs) => { expect(logs).toContain(`<<<<<< A0 B0 D0 ; ${dep}`) @@ -466,7 +466,7 @@ describe('acceptExports', () => { beforeAll(async () => { await untilConsoleLogAfter( - () => setupViteRuntime(`/${testDir}/index`), + () => setupModuleRunner(`/${testDir}/index`), [CONNECTED, />>>>>>/], (logs) => { expect(logs).toContain(`<<< named: ${a} ; ${dep}`) @@ -520,8 +520,9 @@ describe('acceptExports', () => { beforeAll(async () => { clientLogs.length = 0 // so it's in the module graph - await server.transformRequest(testFile, { ssr: true }) - await server.transformRequest('non-tested/dep.js', { ssr: true }) + const ssrEnvironment = server.environments.ssr + await ssrEnvironment.transformRequest(testFile) + await ssrEnvironment.transformRequest('non-tested/dep.js') }) test('does not full reload', async () => { @@ -569,7 +570,7 @@ describe('acceptExports', () => { const file = 'side-effects.ts' await untilConsoleLogAfter( - () => setupViteRuntime(`/${testDir}/index`), + () => setupModuleRunner(`/${testDir}/index`), [CONNECTED, />>>/], (logs) => { expect(logs).toContain('>>> side FX') @@ -598,7 +599,7 @@ describe('acceptExports', () => { const url = '/' + file await untilConsoleLogAfter( - () => setupViteRuntime(`/${testDir}/index`), + () => setupModuleRunner(`/${testDir}/index`), [CONNECTED, '-- unused --'], (logs) => { expect(logs).toContain('-- unused --') @@ -621,7 +622,7 @@ describe('acceptExports', () => { const file = `${testDir}/${fileName}` await untilConsoleLogAfter( - () => setupViteRuntime(`/${testDir}/index`), + () => setupModuleRunner(`/${testDir}/index`), [CONNECTED, '-- used --', 'used:foo0'], (logs) => { expect(logs).toContain('-- used --') @@ -654,7 +655,7 @@ describe('acceptExports', () => { const url = '/' + file await untilConsoleLogAfter( - () => setupViteRuntime(`/${testDir}/index`), + () => setupModuleRunner(`/${testDir}/index`), [CONNECTED, '>>> ready <<<'], (logs) => { expect(logs).toContain('loaded:all:a0b0c0default0') @@ -688,7 +689,7 @@ describe('acceptExports', () => { const file = `${testDir}/${fileName}` await untilConsoleLogAfter( - () => setupViteRuntime(`/${testDir}/index`), + () => setupModuleRunner(`/${testDir}/index`), [CONNECTED, '>>> ready <<<'], (logs) => { expect(logs).toContain('loaded:some:a0b0c0default0') @@ -716,7 +717,7 @@ describe('acceptExports', () => { }) test('handle virtual module updates', async () => { - await setupViteRuntime('/hmr.ts') + await setupModuleRunner('/hmr.ts') const el = () => hmr('.virtual') expect(el()).toBe('[success]0') editFile('importedVirtual.js', (code) => code.replace('[success]', '[wow]')) @@ -724,7 +725,7 @@ test('handle virtual module updates', async () => { }) test('invalidate virtual module', async () => { - await setupViteRuntime('/hmr.ts') + await setupModuleRunner('/hmr.ts') const el = () => hmr('.virtual') expect(el()).toBe('[wow]0') globalThis.__HMR__['virtual:increment']() @@ -732,7 +733,7 @@ test('invalidate virtual module', async () => { }) test.todo('should hmr when file is deleted and restored', async () => { - await setupViteRuntime('/hmr.ts') + await setupModuleRunner('/hmr.ts') const parentFile = 'file-delete-restore/parent.js' const childFile = 'file-delete-restore/child.js' @@ -820,7 +821,7 @@ test.todo('delete file should not break hmr', async () => { test.todo( 'deleted file should trigger dispose and prune callbacks', async () => { - await setupViteRuntime('/hmr.ts') + await setupModuleRunner('/hmr.ts') const parentFile = 'file-delete-restore/parent.js' const childFile = 'file-delete-restore/child.js' @@ -857,7 +858,7 @@ test.todo( ) test('import.meta.hot?.accept', async () => { - await setupViteRuntime('/hmr.ts') + await setupModuleRunner('/hmr.ts') await untilConsoleLogAfter( () => editFile('optional-chaining/child.js', (code) => @@ -869,7 +870,7 @@ test('import.meta.hot?.accept', async () => { }) test('hmr works for self-accepted module within circular imported files', async () => { - await setupViteRuntime('/self-accept-within-circular/index') + await setupModuleRunner('/self-accept-within-circular/index') const el = () => hmr('.self-accept-within-circular') expect(el()).toBe('c') editFile('self-accept-within-circular/c.js', (code) => @@ -885,7 +886,7 @@ test('hmr works for self-accepted module within circular imported files', async }) test('hmr should not reload if no accepted within circular imported files', async () => { - await setupViteRuntime('/circular/index') + await setupModuleRunner('/circular/index') const el = () => hmr('.circular') expect(el()).toBe( // tests in the browser check that there is an error, but vite runtime just returns undefined in those cases @@ -901,7 +902,7 @@ test('hmr should not reload if no accepted within circular imported files', asyn }) test('assets HMR', async () => { - await setupViteRuntime('/hmr.ts') + await setupModuleRunner('/hmr.ts') const el = () => hmr('#logo') await untilConsoleLogAfter( () => @@ -1096,7 +1097,7 @@ function createInMemoryLogger(logs: string[]) { return logger } -async function setupViteRuntime( +async function setupModuleRunner( entrypoint: string, serverOptions: InlineConfig = {}, ) { @@ -1104,7 +1105,7 @@ async function setupViteRuntime( await server.close() clientLogs.length = 0 serverLogs.length = 0 - runtime.clearCache() + runner.clearCache() } globalThis.__HMR__ = {} as any @@ -1137,9 +1138,9 @@ async function setupViteRuntime( const logger = new HMRMockLogger() // @ts-expect-error not typed for HMR - globalThis.log = (...msg) => logger.debug(...msg) + globalThis.log = (...msg) => logger.log(...msg) - runtime = await createViteRuntime(server, { + runner = createServerModuleRunner(server, server.environments.ssr, { hmr: { logger, }, @@ -1147,22 +1148,29 @@ async function setupViteRuntime( await waitForWatcher(server, entrypoint) - await runtime.executeEntrypoint(entrypoint) + await runner.import(entrypoint) return { - runtime, + runtime: runner, server, } } class HMRMockLogger { - debug(...msg: unknown[]) { + log(...msg: unknown[]) { const log = msg.join(' ') clientLogs.push(log) logsEmitter.emit('log', log) } + + debug(...msg: unknown[]) { + const log = ['[vite]', ...msg].join(' ') + clientLogs.push(log) + logsEmitter.emit('log', log) + } error(msg: string) { - clientLogs.push(msg) - logsEmitter.emit('log', msg) + const log = ['[vite]', msg].join(' ') + clientLogs.push(log) + logsEmitter.emit('log', log) } } diff --git a/playground/hmr-ssr/vite.config.ts b/playground/hmr-ssr/vite.config.ts index 5b4a7c17fe27cb..2d570d79acf8c4 100644 --- a/playground/hmr-ssr/vite.config.ts +++ b/playground/hmr-ssr/vite.config.ts @@ -46,10 +46,13 @@ export const virtual = _virtual + '${num}';` }, configureServer(server) { server.hot.on('virtual:increment', async () => { - const mod = await server.moduleGraph.getModuleByUrl('\0virtual:file') + const mod = + await server.environments.ssr.moduleGraph.getModuleByUrl( + '\0virtual:file', + ) if (mod) { num++ - server.reloadModule(mod) + server.reloadEnvironmentModule(mod) } }) }, diff --git a/playground/hmr/vite.config.ts b/playground/hmr/vite.config.ts index b290ff60a3140d..1e3507e0e0f998 100644 --- a/playground/hmr/vite.config.ts +++ b/playground/hmr/vite.config.ts @@ -48,10 +48,13 @@ export const virtual = _virtual + '${num}';` }, configureServer(server) { server.hot.on('virtual:increment', async () => { - const mod = await server.moduleGraph.getModuleByUrl('\0virtual:file') + const mod = + await server.environments.client.moduleGraph.getModuleByUrl( + '\0virtual:file', + ) if (mod) { num++ - server.reloadModule(mod) + server.reloadEnvironmentModule(mod) } }) }, diff --git a/playground/html/__tests__/html.spec.ts b/playground/html/__tests__/html.spec.ts index ed06f730308a4d..5a99c50a004ef1 100644 --- a/playground/html/__tests__/html.spec.ts +++ b/playground/html/__tests__/html.spec.ts @@ -380,7 +380,10 @@ describe.runIf(isServe)('warmup', () => { // warmup transform files async during server startup, so the module check // here might take a while to load await withRetry(async () => { - const mod = await viteServer.moduleGraph.getModuleByUrl('/warmup/warm.js') + const mod = + await viteServer.environments.client.moduleGraph.getModuleByUrl( + '/warmup/warm.js', + ) expect(mod).toBeTruthy() }) }) diff --git a/playground/module-graph/__tests__/module-graph.spec.ts b/playground/module-graph/__tests__/module-graph.spec.ts index bfabd53f289724..20492e968c674f 100644 --- a/playground/module-graph/__tests__/module-graph.spec.ts +++ b/playground/module-graph/__tests__/module-graph.spec.ts @@ -4,7 +4,7 @@ import { isServe, page, viteServer } from '~utils' test.runIf(isServe)('importedUrls order is preserved', async () => { const el = page.locator('.imported-urls-order') expect(await el.textContent()).toBe('[success]') - const mod = await viteServer.moduleGraph.getModuleByUrl( + const mod = await viteServer.environments.client.moduleGraph.getModuleByUrl( '/imported-urls-order.js', ) const importedModuleIds = [...mod.importedModules].map((m) => m.url) diff --git a/playground/ssr-deps/__tests__/ssr-deps.spec.ts b/playground/ssr-deps/__tests__/ssr-deps.spec.ts index c8794ce915dc21..64886f2f0c7e54 100644 --- a/playground/ssr-deps/__tests__/ssr-deps.spec.ts +++ b/playground/ssr-deps/__tests__/ssr-deps.spec.ts @@ -120,7 +120,11 @@ test('import css library', async () => { }) describe.runIf(isServe)('hmr', () => { - test('handle isomorphic module updates', async () => { + // TODO: the server file is not imported on the client at all + // so it's not present in the client moduleGraph anymore + // we need to decide if we want to support a usecase when ssr change + // affcts the client in any way + test.skip('handle isomorphic module updates', async () => { await page.goto(url) expect(await page.textContent('.isomorphic-module-server')).toMatch( diff --git a/playground/ssr-html/__tests__/ssr-html.spec.ts b/playground/ssr-html/__tests__/ssr-html.spec.ts index 92a2713420f2c3..249bfb5d722383 100644 --- a/playground/ssr-html/__tests__/ssr-html.spec.ts +++ b/playground/ssr-html/__tests__/ssr-html.spec.ts @@ -123,7 +123,7 @@ describe.runIf(isServe)('network-imports', () => { [ '--experimental-network-imports', 'test-network-imports.js', - '--runtime', + '--module-runner', ], { cwd: fileURLToPath(new URL('..', import.meta.url)), diff --git a/playground/ssr-html/test-network-imports.js b/playground/ssr-html/test-network-imports.js index 91f84f6a3b3ea3..0d9ed8beb17663 100644 --- a/playground/ssr-html/test-network-imports.js +++ b/playground/ssr-html/test-network-imports.js @@ -1,8 +1,8 @@ import assert from 'node:assert' import { fileURLToPath } from 'node:url' -import { createServer, createViteRuntime } from 'vite' +import { createServer, createServerModuleRunner } from 'vite' -async function runTest(useRuntime) { +async function runTest(userRunner) { const server = await createServer({ configFile: false, root: fileURLToPath(new URL('.', import.meta.url)), @@ -11,9 +11,15 @@ async function runTest(useRuntime) { }, }) let mod - if (useRuntime) { - const runtime = await createViteRuntime(server, { hmr: false }) - mod = await runtime.executeUrl('/src/network-imports.js') + if (userRunner) { + const runner = await createServerModuleRunner( + server, + server.environments.ssr, + { + hmr: false, + }, + ) + mod = await runner.import('/src/network-imports.js') } else { mod = await server.ssrLoadModule('/src/network-imports.js') } @@ -21,4 +27,4 @@ async function runTest(useRuntime) { await server.close() } -runTest(process.argv.includes('--runtime')) +runTest(process.argv.includes('--module-runner')) diff --git a/playground/ssr-html/test-stacktrace-runtime.js b/playground/ssr-html/test-stacktrace-runtime.js index c2b8f670b5a089..ebcbc0513f0fb2 100644 --- a/playground/ssr-html/test-stacktrace-runtime.js +++ b/playground/ssr-html/test-stacktrace-runtime.js @@ -1,6 +1,6 @@ import { fileURLToPath } from 'node:url' import assert from 'node:assert' -import { createServer, createViteRuntime } from 'vite' +import { createServer, createServerModuleRunner } from 'vite' // same test case as packages/vite/src/node/ssr/runtime/__tests__/server-source-maps.spec.ts // implemented for e2e to catch build specific behavior @@ -13,11 +13,11 @@ const server = await createServer({ }, }) -const runtime = await createViteRuntime(server, { +const runner = await createServerModuleRunner(server, server.environments.ssr, { sourcemapInterceptor: 'prepareStackTrace', }) -const mod = await runtime.executeEntrypoint('/src/has-error-deep.ts') +const mod = await runner.import('/src/has-error-deep.ts') let error try { mod.main() diff --git a/playground/ssr-noexternal/package.json b/playground/ssr-noexternal/package.json index 3273e76b04c599..5df63e8468ead6 100644 --- a/playground/ssr-noexternal/package.json +++ b/playground/ssr-noexternal/package.json @@ -7,7 +7,8 @@ "dev": "node server", "build": "vite build --ssr src/entry-server.js", "serve": "NODE_ENV=production node server", - "debug": "node --inspect-brk server" + "debug": "node --inspect-brk server", + "build-all": "vite build --all" }, "dependencies": { "@vitejs/test-external-cjs": "file:./external-cjs", diff --git a/playground/ssr-noexternal/vite.config.js b/playground/ssr-noexternal/vite.config.js index 1109bddd187001..8932076d09e24d 100644 --- a/playground/ssr-noexternal/vite.config.js +++ b/playground/ssr-noexternal/vite.config.js @@ -15,5 +15,6 @@ export default defineConfig({ rollupOptions: { external: ['@vitejs/test-external-cjs'], }, + ssr: 'src/entry-server.js', // for 'all' }, }) diff --git a/playground/vitestSetup.ts b/playground/vitestSetup.ts index ff2303dc498569..a1adaadd7ced21 100644 --- a/playground/vitestSetup.ts +++ b/playground/vitestSetup.ts @@ -14,6 +14,7 @@ import type { import { build, createServer, + createViteBuilder, loadConfigFromFile, mergeConfig, preview, @@ -257,15 +258,20 @@ export async function startDefaultServe(): Promise { plugins: [resolvedPlugin()], }, ) - const rollupOutput = await build(buildConfig) - const isWatch = !!resolvedConfig!.build.watch - // in build watch,call startStaticServer after the build is complete - if (isWatch) { - watcher = rollupOutput as RollupWatcher - await notifyRebuildComplete(watcher) - } - if (buildConfig.__test__) { - buildConfig.__test__() + if (buildConfig.builder) { + const builder = await createViteBuilder({}, { root: rootDir }) + await builder.buildEnvironments() + } else { + const rollupOutput = await build(buildConfig) + const isWatch = !!resolvedConfig!.build.watch + // in build watch,call startStaticServer after the build is complete + if (isWatch) { + watcher = rollupOutput as RollupWatcher + await notifyRebuildComplete(watcher) + } + if (buildConfig.__test__) { + buildConfig.__test__() + } } const previewConfig = await loadConfig({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 603da0897e4f80..5afc4f22ff8685 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -433,6 +433,14 @@ importers: specifier: ^8.16.0 version: 8.16.0 + packages/vite/src/node/__tests__: + dependencies: + '@vitejs/cjs-ssr-dep': + specifier: link:./fixtures/cjs-ssr-dep + version: link:fixtures/cjs-ssr-dep + + packages/vite/src/node/__tests__/fixtures/cjs-ssr-dep: {} + packages/vite/src/node/__tests__/packages/module: {} packages/vite/src/node/__tests__/packages/name: {} @@ -451,14 +459,6 @@ importers: packages/vite/src/node/server/__tests__/fixtures/yarn/nested: {} - packages/vite/src/node/ssr/__tests__: - dependencies: - '@vitejs/cjs-ssr-dep': - specifier: link:./fixtures/cjs-ssr-dep - version: link:fixtures/cjs-ssr-dep - - packages/vite/src/node/ssr/__tests__/fixtures/cjs-ssr-dep: {} - packages/vite/src/node/ssr/runtime/__tests__: dependencies: '@vitejs/cjs-external': @@ -680,6 +680,21 @@ importers: playground/env-nested: {} + playground/environment-react-ssr: + devDependencies: + '@types/react': + specifier: ^18.2.73 + version: 18.2.74 + '@types/react-dom': + specifier: ^18.2.23 + version: 18.2.23 + react: + specifier: ^18.2.0 + version: 18.2.0 + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) + playground/extensions: dependencies: vue: @@ -4376,6 +4391,10 @@ packages: kleur: 3.0.3 dev: true + /@types/prop-types@15.7.12: + resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} + dev: true + /@types/qs@6.9.12: resolution: {integrity: sha512-bZcOkJ6uWrL0Qb2NAWKa7TBU+mJHPzhx9jjLL1KHF+XpzEcR7EXHvjbHlGtR/IsP1vyPrehuS6XqkmaePy//mg==} dev: true @@ -4384,6 +4403,19 @@ packages: resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} dev: true + /@types/react-dom@18.2.23: + resolution: {integrity: sha512-ZQ71wgGOTmDYpnav2knkjr3qXdAFu0vsk8Ci5w3pGAIdj7/kKAyn+VsQDhXsmzzzepAiI9leWMmubXz690AI/A==} + dependencies: + '@types/react': 18.2.74 + dev: true + + /@types/react@18.2.74: + resolution: {integrity: sha512-9AEqNZZyBx8OdZpxzQlaFEVCSFUM2YXJH46yPOiOpm078k6ZLOCcuAzGum/zK8YBwY+dbahVNbHrbgrAwIRlqw==} + dependencies: + '@types/prop-types': 15.7.12 + csstype: 3.1.3 + dev: true + /@types/resolve@1.20.2: resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} dev: true @@ -7494,7 +7526,6 @@ packages: hasBin: true dependencies: js-tokens: 4.0.0 - dev: false /loupe@2.3.7: resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} @@ -8893,7 +8924,6 @@ packages: loose-envify: 1.4.0 react: 18.2.0 scheduler: 0.23.0 - dev: false /react-is@18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} @@ -8904,7 +8934,6 @@ packages: engines: {node: '>=0.10.0'} dependencies: loose-envify: 1.4.0 - dev: false /read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} @@ -9193,7 +9222,6 @@ packages: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} dependencies: loose-envify: 1.4.0 - dev: false /scslre@0.3.0: resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} diff --git a/vitest.config.e2e.ts b/vitest.config.e2e.ts index 7533ea991c5f95..db750c65ebec21 100644 --- a/vitest.config.e2e.ts +++ b/vitest.config.e2e.ts @@ -1,7 +1,7 @@ import { resolve } from 'node:path' import { defineConfig } from 'vitest/config' -const timeout = process.env.CI ? 50000 : 30000 +const timeout = process.env.PWDEBUG ? Infinity : process.env.CI ? 50000 : 30000 export default defineConfig({ resolve: { diff --git a/vitest.config.ts b/vitest.config.ts index 2802969bc155c2..edf35c9c4cf507 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -21,9 +21,9 @@ export default defineConfig({ publicDir: false, resolve: { alias: { - 'vite/runtime': path.resolve( + 'vite/module-runner': path.resolve( _dirname, - './packages/vite/src/runtime/index.ts', + './packages/vite/src/module-runner/index.ts', ), }, },