From b41895c922a26e51b2850ac38de7646d2aa3e18b Mon Sep 17 00:00:00 2001 From: patak-dev Date: Fri, 19 Apr 2024 14:33:03 +0200 Subject: [PATCH 001/123] docs: environment API migration guide stub --- docs/guide/migration.md | 247 +--------------------------------------- 1 file changed, 5 insertions(+), 242 deletions(-) diff --git a/docs/guide/migration.md b/docs/guide/migration.md index 32d8a499e3c358..04df92f5f6a2bb 100644 --- a/docs/guide/migration.md +++ b/docs/guide/migration.md @@ -1,246 +1,9 @@ -# Migration from v4 +# Migration from v5 -## Node.js Support +## Environment API -Vite no longer supports Node.js 14 / 16 / 17 / 19, which reached its EOL. Node.js 18 / 20+ is now required. +TODO -## Rollup 4 +## Migration from v4 -Vite is now using Rollup 4 which also brings along its breaking changes, in particular: - -- Import assertions (`assertions` prop) has been renamed to import attributes (`attributes` prop). -- Acorn plugins are no longer supported. -- For Vite plugins, `this.resolve` `skipSelf` option is now `true` by default. -- For Vite plugins, `this.parse` now only supports the `allowReturnOutsideFunction` option for now. - -Read the full breaking changes in [Rollup's release notes](https://github.com/rollup/rollup/releases/tag/v4.0.0) for build-related changes in [`build.rollupOptions`](/config/build-options.md#build-rollupoptions). - -If you are using TypeScript, make sure to set `moduleResolution: 'bundler'` (or `node16`/`nodenext`) as Rollup 4 requires it. Or you can set `skipLibCheck: true` instead. - -## Deprecate CJS Node API - -The CJS Node API of Vite is deprecated. When calling `require('vite')`, a deprecation warning is now logged. You should update your files or frameworks to import the ESM build of Vite instead. - -In a basic Vite project, make sure: - -1. The `vite.config.js` file content is using the ESM syntax. -2. The closest `package.json` file has `"type": "module"`, or use the `.mjs`/`.mts` extension, e.g. `vite.config.mjs` or `vite.config.mts`. - -For other projects, there are a few general approaches: - -- **Configure ESM as default, opt-in to CJS if needed:** Add `"type": "module"` in the project `package.json`. All `*.js` files are now interpreted as ESM and needs to use the ESM syntax. You can rename a file with the `.cjs` extension to keep using CJS instead. -- **Keep CJS as default, opt-in to ESM if needed:** If the project `package.json` does not have `"type": "module"`, all `*.js` files are interpreted as CJS. You can rename a file with the `.mjs` extension to use ESM instead. -- **Dynamically import Vite:** If you need to keep using CJS, you can dynamically import Vite using `import('vite')` instead. This requires your code to be written in an `async` context, but should still be manageable as Vite's API is mostly asynchronous. - -See the [troubleshooting guide](/guide/troubleshooting.html#vite-cjs-node-api-deprecated) for more information. - -## Rework `define` and `import.meta.env.*` replacement strategy - -In Vite 4, the [`define`](/config/shared-options.md#define) and [`import.meta.env.*`](/guide/env-and-mode.md#env-variables) features use different replacement strategies in dev and build: - -- In dev, both features are injected as global variables to `globalThis` and `import.meta` respectively. -- In build, both features are statically replaced with a regex. - -This results in a dev and build inconsistency when trying to access the variables, and sometimes even caused failed builds. For example: - -```js -// vite.config.js -export default defineConfig({ - define: { - __APP_VERSION__: JSON.stringify('1.0.0'), - }, -}) -``` - -```js -const data = { __APP_VERSION__ } -// dev: { __APP_VERSION__: "1.0.0" } ✅ -// build: { "1.0.0" } ❌ - -const docs = 'I like import.meta.env.MODE' -// dev: "I like import.meta.env.MODE" ✅ -// build: "I like "production"" ❌ -``` - -Vite 5 fixes this by using `esbuild` to handle the replacements in builds, aligning with the dev behaviour. - -This change should not affect most setups, as it's already documented that `define` values should follow esbuild's syntax: - -> To be consistent with esbuild behavior, expressions must either be a JSON object (null, boolean, number, string, array, or object) or a single identifier. - -However, if you prefer to keep statically replacing values directly, you can use [`@rollup/plugin-replace`](https://github.com/rollup/plugins/tree/master/packages/replace). - -## General Changes - -### SSR externalized modules value now matches production - -In Vite 4, SSR externalized modules are wrapped with `.default` and `.__esModule` handling for better interoperability, but it doesn't match the production behaviour when loaded by the runtime environment (e.g. Node.js), causing hard-to-catch inconsistencies. By default, all direct project dependencies are SSR externalized. - -Vite 5 now removes the `.default` and `.__esModule` handling to match the production behaviour. In practice, this shouldn't affect properly-packaged dependencies, but if you encounter new issues loading modules, you can try these refactors: - -```js -// Before: -import { foo } from 'bar' - -// After: -import _bar from 'bar' -const { foo } = _bar -``` - -```js -// Before: -import foo from 'bar' - -// After: -import * as _foo from 'bar' -const foo = _foo.default -``` - -Note that these changes matches the Node.js behaviour, so you can also run the imports in Node.js to test it out. If you prefer to stick with the previous behaviour, you can set `legacy.proxySsrExternalModules` to `true`. - -### `worker.plugins` is now a function - -In Vite 4, [`worker.plugins`](/config/worker-options.md#worker-plugins) accepted an array of plugins (`(Plugin | Plugin[])[]`). From Vite 5, it needs to be configured as a function that returns an array of plugins (`() => (Plugin | Plugin[])[]`). This change is required so parallel worker builds run more consistently and predictably. - -### Allow path containing `.` to fallback to index.html - -In Vite 4, accessing a path in dev containing `.` did not fallback to index.html even if [`appType`](/config/shared-options.md#apptype) is set to `'spa'` (default). From Vite 5, it will fallback to index.html. - -Note that the browser will no longer show a 404 error message in the console if you point the image path to a non-existent file (e.g. ``). - -### Align dev and preview HTML serving behaviour - -In Vite 4, the dev and preview servers serve HTML based on its directory structure and trailing slash differently. This causes inconsistencies when testing your built app. Vite 5 refactors into a single behaviour like below, given the following file structure: - -``` -├── index.html -├── file.html -└── dir - └── index.html -``` - -| Request | Before (dev) | Before (preview) | After (dev & preview) | -| ----------------- | ---------------------------- | ----------------- | ---------------------------- | -| `/dir/index.html` | `/dir/index.html` | `/dir/index.html` | `/dir/index.html` | -| `/dir` | `/index.html` (SPA fallback) | `/dir/index.html` | `/index.html` (SPA fallback) | -| `/dir/` | `/dir/index.html` | `/dir/index.html` | `/dir/index.html` | -| `/file.html` | `/file.html` | `/file.html` | `/file.html` | -| `/file` | `/index.html` (SPA fallback) | `/file.html` | `/file.html` | -| `/file/` | `/index.html` (SPA fallback) | `/file.html` | `/index.html` (SPA fallback) | - -### Manifest files are now generated in `.vite` directory by default - -In Vite 4, the manifest files ([`build.manifest`](/config/build-options.md#build-manifest) and [`build.ssrManifest`](/config/build-options.md#build-ssrmanifest)) were generated in the root of [`build.outDir`](/config/build-options.md#build-outdir) by default. - -From Vite 5, they will be generated in the `.vite` directory in the `build.outDir` by default. This change helps deconflict public files with the same manifest file names when they are copied to the `build.outDir`. - -### Corresponding CSS files are not listed as top level entry in manifest.json file - -In Vite 4, the corresponding CSS file for a JavaScript entry point was also listed as a top-level entry in the manifest file ([`build.manifest`](/config/build-options.md#build-manifest)). These entries were unintentionally added and only worked for simple cases. - -In Vite 5, corresponding CSS files can only be found within the JavaScript entry file section. -When injecting the JS file, the corresponding CSS files [should be injected](/guide/backend-integration.md#:~:text=%3C!%2D%2D%20if%20production%20%2D%2D%3E%0A%3Clink%20rel%3D%22stylesheet%22%20href%3D%22/assets/%7B%7B%20manifest%5B%27main.js%27%5D.css%20%7D%7D%22%20/%3E%0A%3Cscript%20type%3D%22module%22%20src%3D%22/assets/%7B%7B%20manifest%5B%27main.js%27%5D.file%20%7D%7D%22%3E%3C/script%3E). -When the CSS should be injected separately, it must be added as a separate entry point. - -### CLI shortcuts require an additional `Enter` press - -CLI shortcuts, like `r` to restart the dev server, now require an additional `Enter` press to trigger the shortcut. For example, `r + Enter` to restart the dev server. - -This change prevents Vite from swallowing and controlling OS-specific shortcuts, allowing better compatibility when combining the Vite dev server with other processes, and avoids the [previous caveats](https://github.com/vitejs/vite/pull/14342). - -### Update `experimentalDecorators` and `useDefineForClassFields` TypeScript behaviour - -Vite 5 uses esbuild 0.19 and removes the compatibility layer for esbuild 0.18, which changes how [`experimentalDecorators`](https://www.typescriptlang.org/tsconfig#experimentalDecorators) and [`useDefineForClassFields`](https://www.typescriptlang.org/tsconfig#useDefineForClassFields) are handled. - -- **`experimentalDecorators` is not enabled by default** - - You need to set `compilerOptions.experimentalDecorators` to `true` in `tsconfig.json` to use decorators. - -- **`useDefineForClassFields` defaults depend on the TypeScript `target` value** - - If `target` is not `ESNext` or `ES2022` or newer, or if there's no `tsconfig.json` file, `useDefineForClassFields` will default to `false` which can be problematic with the default `esbuild.target` value of `esnext`. It may transpile to [static initialization blocks](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Static_initialization_blocks#browser_compatibility) which may not be supported in your browser. - - As such, it is recommended to set `target` to `ESNext` or `ES2022` or newer, or set `useDefineForClassFields` to `true` explicitly when configuring `tsconfig.json`. - -```jsonc -{ - "compilerOptions": { - // Set true if you use decorators - "experimentalDecorators": true, - // Set true if you see parsing errors in your browser - "useDefineForClassFields": true, - }, -} -``` - -### Remove `--https` flag and `https: true` - -The `--https` flag sets `server.https: true` and `preview.https: true` internally. This config was meant to be used together with the automatic https certification generation feature which [was dropped in Vite 3](https://v3.vitejs.dev/guide/migration.html#automatic-https-certificate-generation). Hence, this config is no longer useful as it will start a Vite HTTPS server without a certificate. - -If you use [`@vitejs/plugin-basic-ssl`](https://github.com/vitejs/vite-plugin-basic-ssl) or [`vite-plugin-mkcert`](https://github.com/liuweiGL/vite-plugin-mkcert), they will already set the `https` config internally, so you can remove `--https`, `server.https: true`, and `preview.https: true` in your setup. - -### Remove `resolvePackageEntry` and `resolvePackageData` APIs - -The `resolvePackageEntry` and `resolvePackageData` APIs are removed as they exposed Vite's internals and blocked potential Vite 4.3 optimizations in the past. These APIs can be replaced with third-party packages, for example: - -- `resolvePackageEntry`: [`import.meta.resolve`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta/resolve) or the [`import-meta-resolve`](https://github.com/wooorm/import-meta-resolve) package. -- `resolvePackageData`: Same as above, and crawl up the package directory to get the root `package.json`. Or use the community [`vitefu`](https://github.com/svitejs/vitefu) package. - -```js -import { resolve } from 'import-meta-env' -import { findDepPkgJsonPath } from 'vitefu' -import fs from 'node:fs' - -const pkg = 'my-lib' -const basedir = process.cwd() - -// `resolvePackageEntry`: -const packageEntry = resolve(pkg, basedir) - -// `resolvePackageData`: -const packageJsonPath = findDepPkgJsonPath(pkg, basedir) -const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) -``` - -## Removed Deprecated APIs - -- Default exports of CSS files (e.g `import style from './foo.css'`): Use the `?inline` query instead -- `import.meta.globEager`: Use `import.meta.glob('*', { eager: true })` instead -- `ssr.format: 'cjs'` and `legacy.buildSsrCjsExternalHeuristics` ([#13816](https://github.com/vitejs/vite/discussions/13816)) -- `server.middlewareMode: 'ssr'` and `server.middlewareMode: 'html'`: Use [`appType`](/config/shared-options.md#apptype) + [`server.middlewareMode: true`](/config/server-options.md#server-middlewaremode) instead ([#8452](https://github.com/vitejs/vite/pull/8452)) - -## Advanced - -There are some changes which only affect plugin/tool creators. - -- [[#14119] refactor!: merge `PreviewServerForHook` into `PreviewServer` type](https://github.com/vitejs/vite/pull/14119) - - The `configurePreviewServer` hook now accepts the `PreviewServer` type instead of `PreviewServerForHook` type. -- [[#14818] refactor(preview)!: use base middleware](https://github.com/vitejs/vite/pull/14818) - - Middlewares added from the returned function in `configurePreviewServer` now does not have access to the `base` when comparing the `req.url` value. This aligns the behaviour with the dev server. You can check the `base` from the `configResolved` hook if needed. -- [[#14834] fix(types)!: expose httpServer with Http2SecureServer union](https://github.com/vitejs/vite/pull/14834) - - `http.Server | http2.Http2SecureServer` is now used instead of `http.Server` where appropriate. - -Also there are other breaking changes which only affect few users. - -- [[#14098] fix!: avoid rewriting this (reverts #5312)](https://github.com/vitejs/vite/pull/14098) - - Top level `this` was rewritten to `globalThis` by default when building. This behavior is now removed. -- [[#14231] feat!: add extension to internal virtual modules](https://github.com/vitejs/vite/pull/14231) - - Internal virtual modules' id now has an extension (`.js`). -- [[#14583] refactor!: remove exporting internal APIs](https://github.com/vitejs/vite/pull/14583) - - Removed accidentally exported internal APIs: `isDepsOptimizerEnabled` and `getDepOptimizationConfig` - - Removed exported internal types: `DepOptimizationResult`, `DepOptimizationProcessing`, and `DepsOptimizer` - - Renamed `ResolveWorkerOptions` type to `ResolvedWorkerOptions` -- [[#5657] fix: return 404 for resources requests outside the base path](https://github.com/vitejs/vite/pull/5657) - - In the past, Vite responded to requests outside the base path without `Accept: text/html`, as if they were requested with the base path. Vite no longer does that and responds with 404 instead. -- [[#14723] fix(resolve)!: remove special .mjs handling](https://github.com/vitejs/vite/pull/14723) - - In the past, when a library `"exports"` field maps to an `.mjs` file, Vite will still try to match the `"browser"` and `"module"` fields to fix compatibility with certain libraries. This behavior is now removed to align with the exports resolution algorithm. -- [[#14733] feat(resolve)!: remove `resolve.browserField`](https://github.com/vitejs/vite/pull/14733) - - `resolve.browserField` has been deprecated since Vite 3 in favour of an updated default of `['browser', 'module', 'jsnext:main', 'jsnext']` for [`resolve.mainFields`](/config/shared-options.md#resolve-mainfields). -- [[#14855] feat!: add isPreview to ConfigEnv and resolveConfig](https://github.com/vitejs/vite/pull/14855) - - Renamed `ssrBuild` to `isSsrBuild` in the `ConfigEnv` object. -- [[#14945] fix(css): correctly set manifest source name and emit CSS file](https://github.com/vitejs/vite/pull/14945) - - CSS file names are now generated based on the chunk name. - -## Migration from v3 - -Check the [Migration from v3 Guide](https://v4.vitejs.dev/guide/migration.html) in the Vite v4 docs first to see the needed changes to port your app to Vite v4, and then proceed with the changes on this page. +Check the [Migration from v4 Guide](https://v5.vitejs.dev/guide/migration.html) in the Vite v5 docs first to see the needed changes to port your app to Vite 5, and then proceed with the changes on this page. From f684d4cd5cc8ce10d93bb44a5bbca556997500f1 Mon Sep 17 00:00:00 2001 From: patak <583075+patak-dev@users.noreply.github.com> Date: Fri, 19 Apr 2024 14:42:21 +0200 Subject: [PATCH 002/123] feat: environment api (#16129) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Vladimir Sheremet Co-authored-by: Hiroshi Ogawa Co-authored-by: 翠 / green Co-authored-by: Jun Shindo <46585162+jay-es@users.noreply.github.com> Co-authored-by: Greg T. Wallace Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Caven Co-authored-by: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Co-authored-by: Matyáš Racek Co-authored-by: bluwy --- docs/config/build-options.md | 8 + docs/guide/api-javascript.md | 1 + docs/guide/api-vite-runtime.md | 14 +- packages/vite/CHANGELOG.md | 123 +++ packages/vite/package.json | 12 +- packages/vite/rollup.config.ts | 12 +- packages/vite/rollup.dts.config.ts | 12 +- packages/vite/src/client/client.ts | 7 +- .../{runtime => module-runner}/constants.ts | 0 .../esmEvaluator.ts} | 8 +- .../{runtime => module-runner}/hmrHandler.ts | 85 +- .../{runtime => module-runner}/hmrLogger.ts | 5 + .../src/{runtime => module-runner}/index.ts | 19 +- .../{runtime => module-runner}/moduleCache.ts | 50 +- .../runtime.ts => module-runner/runner.ts} | 195 ++--- .../vite/src/module-runner/runnerTransport.ts | 83 ++ .../sourcemap/decoder.ts | 0 .../sourcemap/index.ts | 12 +- .../sourcemap/interceptor.ts | 16 +- .../{runtime => module-runner}/tsconfig.json | 0 .../src/{runtime => module-runner}/types.ts | 65 +- .../src/{runtime => module-runner}/utils.ts | 27 + packages/vite/src/node/__tests__/dev.spec.ts | 2 +- .../external.spec.ts} | 17 +- .../__tests__/fixtures/cjs-ssr-dep/index.js | 0 .../fixtures/cjs-ssr-dep/package.json | 0 .../src/node/{ssr => }/__tests__/package.json | 0 .../src/node/__tests__/plugins/css.spec.ts | 5 + .../src/node/__tests__/plugins/define.spec.ts | 9 +- packages/vite/src/node/build.ts | 388 +++++++-- packages/vite/src/node/cli.ts | 93 ++- packages/vite/src/node/config.ts | 775 ++++++++++++++---- packages/vite/src/node/environment.ts | 93 +++ .../node/{ssr/ssrExternal.ts => external.ts} | 92 ++- packages/vite/src/node/idResolver.ts | 90 ++ packages/vite/src/node/index.ts | 40 +- packages/vite/src/node/logger.ts | 13 +- .../src/node/optimizer/esbuildDepPlugin.ts | 19 +- packages/vite/src/node/optimizer/index.ts | 211 +++-- packages/vite/src/node/optimizer/optimizer.ts | 331 ++++---- packages/vite/src/node/optimizer/resolve.ts | 16 +- packages/vite/src/node/optimizer/scan.ts | 151 +++- packages/vite/src/node/packages.ts | 2 +- packages/vite/src/node/plugin.ts | 283 +++++-- packages/vite/src/node/plugins/asset.ts | 22 +- .../src/node/plugins/assetImportMetaUrl.ts | 15 +- packages/vite/src/node/plugins/css.ts | 169 ++-- packages/vite/src/node/plugins/define.ts | 41 +- .../src/node/plugins/dynamicImportVars.ts | 7 +- packages/vite/src/node/plugins/esbuild.ts | 6 +- .../vite/src/node/plugins/importAnalysis.ts | 56 +- .../vite/src/node/plugins/importMetaGlob.ts | 70 +- packages/vite/src/node/plugins/index.ts | 34 +- .../vite/src/node/plugins/loadFallback.ts | 102 ++- .../vite/src/node/plugins/optimizedDeps.ts | 15 +- packages/vite/src/node/plugins/preAlias.ts | 16 +- packages/vite/src/node/plugins/resolve.ts | 238 +++--- packages/vite/src/node/plugins/worker.ts | 16 +- .../src/node/plugins/workerImportMetaUrl.ts | 20 +- packages/vite/src/node/preview.ts | 6 +- packages/vite/src/node/publicUtils.ts | 2 + .../node/server/__tests__/moduleGraph.spec.ts | 82 +- .../server/__tests__/pluginContainer.spec.ts | 81 +- packages/vite/src/node/server/environment.ts | 361 ++++++++ .../src/node/server/environmentTransport.ts | 38 + .../server/environments/nodeEnvironment.ts | 24 + packages/vite/src/node/server/hmr.ts | 316 ++++--- packages/vite/src/node/server/index.ts | 391 ++++----- .../vite/src/node/server/middlewares/error.ts | 2 +- .../src/node/server/middlewares/indexHtml.ts | 31 +- .../src/node/server/middlewares/static.ts | 78 +- .../src/node/server/middlewares/transform.ts | 21 +- .../vite/src/node/server/mixedModuleGraph.ts | 645 +++++++++++++++ packages/vite/src/node/server/moduleGraph.ts | 193 ++--- .../vite/src/node/server/pluginContainer.ts | 262 ++++-- .../vite/src/node/server/transformRequest.ts | 201 +++-- packages/vite/src/node/server/warmup.ts | 28 +- packages/vite/src/node/ssr/fetchModule.ts | 72 +- packages/vite/src/node/ssr/index.ts | 23 + .../__tests__/fixtures/default-string.ts | 3 + .../ssr/runtime/__tests__/fixtures/worker.mjs | 35 + .../ssr/runtime/__tests__/server-hmr.spec.ts | 28 +- .../runtime/__tests__/server-no-hmr.spec.ts | 12 +- .../runtime/__tests__/server-runtime.spec.ts | 90 +- .../__tests__/server-source-maps.spec.ts | 38 +- .../__tests__/server-worker-runner.spec.ts | 67 ++ .../src/node/ssr/runtime/__tests__/utils.ts | 21 +- .../node/ssr/runtime/serverHmrConnector.ts | 4 +- ...ThreadRuntime.ts => serverModuleRunner.ts} | 52 +- packages/vite/src/node/ssr/ssrFetchModule.ts | 20 - packages/vite/src/node/ssr/ssrModuleLoader.ts | 340 +------- packages/vite/src/node/ssr/ssrStacktrace.ts | 13 +- packages/vite/src/node/ssr/ssrTransform.ts | 53 ++ packages/vite/src/node/tsconfig.json | 10 +- packages/vite/src/node/utils.ts | 2 +- packages/vite/src/shared/constants.ts | 4 +- packages/vite/src/shared/hmr.ts | 11 +- packages/vite/types/hmrPayload.d.ts | 2 +- .../__tests__/basic.spec.ts | 9 + playground/environment-react-ssr/index.html | 14 + playground/environment-react-ssr/package.json | 17 + .../src/entry-client.tsx | 12 + .../src/entry-server.tsx | 24 + playground/environment-react-ssr/src/root.tsx | 19 + .../environment-react-ssr/tsconfig.json | 7 + .../environment-react-ssr/vite.config.ts | 90 ++ playground/hmr-ssr/__tests__/hmr-ssr.spec.ts | 68 +- playground/hmr-ssr/vite.config.ts | 7 +- playground/hmr/vite.config.ts | 7 +- playground/html/__tests__/html.spec.ts | 5 +- .../__tests__/module-graph.spec.ts | 2 +- .../ssr-deps/__tests__/ssr-deps.spec.ts | 6 +- .../ssr-html/__tests__/ssr-html.spec.ts | 2 +- playground/ssr-html/test-network-imports.js | 18 +- .../ssr-html/test-stacktrace-runtime.js | 6 +- playground/ssr-noexternal/package.json | 3 +- playground/ssr-noexternal/vite.config.js | 1 + playground/vitestSetup.ts | 24 +- pnpm-lock.yaml | 52 +- vitest.config.e2e.ts | 2 +- vitest.config.ts | 4 +- 121 files changed, 5730 insertions(+), 2542 deletions(-) rename packages/vite/src/{runtime => module-runner}/constants.ts (100%) rename packages/vite/src/{runtime/esmRunner.ts => module-runner/esmEvaluator.ts} (82%) rename packages/vite/src/{runtime => module-runner}/hmrHandler.ts (55%) rename packages/vite/src/{runtime => module-runner}/hmrLogger.ts (51%) rename packages/vite/src/{runtime => module-runner}/index.ts (52%) rename packages/vite/src/{runtime => module-runner}/moduleCache.ts (83%) rename packages/vite/src/{runtime/runtime.ts => module-runner/runner.ts} (65%) create mode 100644 packages/vite/src/module-runner/runnerTransport.ts rename packages/vite/src/{runtime => module-runner}/sourcemap/decoder.ts (100%) rename packages/vite/src/{runtime => module-runner}/sourcemap/index.ts (76%) rename packages/vite/src/{runtime => module-runner}/sourcemap/interceptor.ts (97%) rename packages/vite/src/{runtime => module-runner}/tsconfig.json (100%) rename packages/vite/src/{runtime => module-runner}/types.ts (74%) rename packages/vite/src/{runtime => module-runner}/utils.ts (75%) rename packages/vite/src/node/{ssr/__tests__/ssrExternal.spec.ts => __tests__/external.spec.ts} (53%) rename packages/vite/src/node/{ssr => }/__tests__/fixtures/cjs-ssr-dep/index.js (100%) rename packages/vite/src/node/{ssr => }/__tests__/fixtures/cjs-ssr-dep/package.json (100%) rename packages/vite/src/node/{ssr => }/__tests__/package.json (100%) create mode 100644 packages/vite/src/node/environment.ts rename packages/vite/src/node/{ssr/ssrExternal.ts => external.ts} (56%) create mode 100644 packages/vite/src/node/idResolver.ts create mode 100644 packages/vite/src/node/server/environment.ts create mode 100644 packages/vite/src/node/server/environmentTransport.ts create mode 100644 packages/vite/src/node/server/environments/nodeEnvironment.ts create mode 100644 packages/vite/src/node/server/mixedModuleGraph.ts create mode 100644 packages/vite/src/node/ssr/runtime/__tests__/fixtures/default-string.ts create mode 100644 packages/vite/src/node/ssr/runtime/__tests__/fixtures/worker.mjs create mode 100644 packages/vite/src/node/ssr/runtime/__tests__/server-worker-runner.spec.ts rename packages/vite/src/node/ssr/runtime/{mainThreadRuntime.ts => serverModuleRunner.ts} (54%) delete mode 100644 packages/vite/src/node/ssr/ssrFetchModule.ts create mode 100644 playground/environment-react-ssr/__tests__/basic.spec.ts create mode 100644 playground/environment-react-ssr/index.html create mode 100644 playground/environment-react-ssr/package.json create mode 100644 playground/environment-react-ssr/src/entry-client.tsx create mode 100644 playground/environment-react-ssr/src/entry-server.tsx create mode 100644 playground/environment-react-ssr/src/root.tsx create mode 100644 playground/environment-react-ssr/tsconfig.json create mode 100644 playground/environment-react-ssr/vite.config.ts 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', ), }, }, From 777967dc9011f816808d97fae5b7476fac4a08b7 Mon Sep 17 00:00:00 2001 From: patak <583075+patak-dev@users.noreply.github.com> Date: Fri, 19 Apr 2024 14:48:04 +0200 Subject: [PATCH 003/123] docs: Environment API (#16089) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Igor Minar Co-authored-by: Vladimir Co-authored-by: 翠 / green Co-authored-by: Clément --- docs/.vitepress/config.ts | 4 +- docs/blog/announcing-vite5-1.md | 2 - docs/guide/api-vite-environment.md | 846 +++++++++++++++++++++++++++++ docs/guide/api-vite-runtime.md | 230 -------- docs/images/vite-environments.svg | 40 ++ 5 files changed, 888 insertions(+), 234 deletions(-) create mode 100644 docs/guide/api-vite-environment.md delete mode 100644 docs/guide/api-vite-runtime.md create mode 100644 docs/images/vite-environments.svg diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 7af266a70c0477..fbb842bfa2a801 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -281,8 +281,8 @@ export default defineConfig({ link: '/guide/api-javascript', }, { - text: 'Vite Runtime API', - link: '/guide/api-vite-runtime', + text: 'Vite Environment API', + link: '/guide/api-vite-environment', }, { text: 'Config Reference', diff --git a/docs/blog/announcing-vite5-1.md b/docs/blog/announcing-vite5-1.md index b5f7c72a8dd6c7..bb0fb3d7ce80e2 100644 --- a/docs/blog/announcing-vite5-1.md +++ b/docs/blog/announcing-vite5-1.md @@ -56,8 +56,6 @@ The new API brings many benefits: The initial idea [was proposed by Pooya Parsa](https://github.com/nuxt/vite/pull/201) and implemented by [Anthony Fu](https://github.com/antfu) as the [vite-node](https://github.com/vitest-dev/vitest/tree/main/packages/vite-node#readme) package to [power Nuxt 3 Dev SSR](https://antfu.me/posts/dev-ssr-on-nuxt) and later also used as the base for [Vitest](https://vitest.dev). So the general idea of vite-node has been battle-tested for quite some time now. This is a new iteration of the API by [Vladimir Sheremet](https://github.com/sheremet-va), who had already re-implemented vite-node in Vitest and took the learnings to make the API even more powerful and flexible when adding it to Vite Core. The PR was one year in the makings, you can see the evolution and discussions with ecosystem maintainers [here](https://github.com/vitejs/vite/issues/12165). -Read more in the [Vite Runtime API guide](/guide/api-vite-runtime) and [give us feedback](https://github.com/vitejs/vite/discussions/15774). - ## Features ### Improved support for `.css?url` diff --git a/docs/guide/api-vite-environment.md b/docs/guide/api-vite-environment.md new file mode 100644 index 00000000000000..ee2905b69422b1 --- /dev/null +++ b/docs/guide/api-vite-environment.md @@ -0,0 +1,846 @@ +# Vite Environment API + +:::warning Low-level API +Initial work for this API was introduced in Vite 5.1 with the name "Vite Runtime API". This guide describes a revised API, renamed to Vite Environment API. This API will be released in Vite 6. You can already test it in the latest `vite@6.0.0-alpha.x` version. + +Resources: + +- [PR for the new API docs](https://github.com/vitejs/vite/pull/16089). +- [Feedback discussion](https://github.com/vitejs/vite/discussions/16358) where we are gathering feedback about the new APIs. +- [Environment API branch](https://github.com/vitejs/vite/pull/16129) where the new API is implemented. + +Feel free to send us PRs against this branch to fix the issues you discover. Please share with us your feedback as you test the proposal. +::: + +A single Vite dev server can be used to interact with different module execution environments concurrently. We'll use the word environment to refer to a configured Vite processing pipeline that can resolve ids, load, and process source code and is connected to a runtime where the code is executed. The transformed source code is called a module, and the relationships between the modules processed in each environment are kept in a module graph. The code for these modules is sent to the runtimes associated with each environment to be executed. When a module is evaluated, the runtime will request its imported modules triggering the processing of a section of the module graph. In a typical Vite app, an environments will be used for the ES modules served to the client and for the server program that does SSR. An app can do SSR on a Node server, but also on an edge runtime like Workerd. So we can have different types of environments on the same Vite server: browser environments, node environments, and workerd environments to name a few. + +A Vite Module Runner allows running any code by processing it with Vite plugins first. It is different from `server.ssrLoadModule` because the runner implementation is decoupled from the server. This allows library and framework authors to implement their layer of communication between the Vite server and the runner. The browser communicates with its corresponding environment using the server Web Socket and through HTTP requests. The Node Module runner can directly do function calls to process modules as it is running in the same process. Other environments could run modules connecting to an edge runtime like workerd, or a Worker Thread as Vitest does. + +All these environments share Vite's HTTP server, middlewares, and Web Socket. The resolved config and plugins pipeline are also shared, but plugins can use `apply` so its hooks are only called for certain environments. The environment can also be accessed inside hooks for fine-grained control. + +![Vite Environments](../images/vite-environments.svg) + +## Using environments in the Vite server + +A Vite dev server exposes two environments by default: a Client environment and a SSR environment. The client environment is a browser environment by default, and the module runner is implemented by importing the virtual module `/@vite/client` to client apps. The SSR environment runs in the same Node runtime as the Vite server by default and allows application servers to be used to render requests during dev with full HMR support. We'll discuss later how frameworks and users can change the environment types for the default client and SSR environments, or register new environments (for example to have a separate module graph for RSC). + +The available environments can be accessed using `server.environments`: + +```js +server.environments.client.transformRequest(url) + +console.log(server.environments.ssr.moduleGraph) +``` + +Normally, the current `environment` instance will be available as part of the context of the code being run so the need to access them through `server.environments` should be rare. For example, inside plugin hooks. + +An dev environment is an instance of the `DevEnvironment` class: + +```ts +class DevEnvironment { + /** + * Unique identifier for the environment in a Vite server. + * By default Vite exposes 'client' and 'ssr' environments. + */ + name: string + /** + * Communication channel to send and receive messages from the + * associated module runner in the target runtime. + */ + hot: HMRChannel | null + /** + * Graph of module nodes, with the imported relationship between + * processed modules and the cached result of the processed code. + */ + moduleGraph: ModuleGraph + /** + * TBD: This abstraction isn't yet clear + * Trigger the execution of a module using the associated module runner + * in the target runtime. + */ + run: ModuleRunFunction + /** + * Resolved config options for this environment. Options at the server + * global scope are taken as defaults for all environments, and can + * be overridden (resolve conditions, external, optimizedDeps) + */ + config: ResolvedDevEnvironmentConfig + + constructor(server, { name, hot, run, config }: DevEnvironmentOptions) + + /** + * Resolve the URL to an id, load it, and process the code using the + * plugins pipeline. The module graph is also updated. + */ + async transformRequest(url: string): TransformResult + + /** + * Register a request to be processed with low priority. This is useful + * to avoid waterfalls. The Vite server has information about the imported + * modules by other requests, so it can warmup the module graph so the + * modules are already processed when they are requested. + */ + async warmupRequest(url: string): void + + /** + * Fetch information about a module from the module runner without running it. + * Note: This method may not be needed + */ + async fetchModuleInfo(url: string) +} +``` + +With `TransformResult` being: + +```ts +interface TransformResult { + code: string + map: SourceMap | { mappings: '' } | null + etag?: string + deps?: string[] + dynamicDeps?: string[] +} +``` + +An environment instance in the Vite server lets you process a URL using the `environment.transformRequest(url)` method. This function will use the plugin pipeline to resolve the `url` to a module `id`, load it (reading the file from the file system or through a plugin that implements a virtual module), and then transform the code. While transforming the module, imports and other metadata will be recorded in the environment module graph by creating or updating the corresponding module node. When processing is done, the transform result is also stored in the module. + +But the environment instance can't execute the code itself, as the runtime where the module will be run could be different from the one the Vite server is running in. This is the case for the browser environment. When a html is loaded in the browser, its scripts are executed triggering the evaluation of the entire static module graph. Each imported URL generates a request to the Vite server to get the module code, which ends up handled by the Transform Middleware by calling `server.environments.client.transformRequest(url)`. The connection between the environment instance in the server and the module runner in the browser is carried out through HTTP in this case. + +:::info transformRequest naming +We are using `transformRequest(url)` and `warmupRequest(url)` in the current version of this proposal so it is easier to discuss and understand for users used to Vite's current API. Before releasing, we can take the opportunity to review these names too. For example, it could be named `environment.processModule(url)` or `environment.loadModule(url)` taking a page from Rollup's `context.load(id)` in plugin hooks. For the moment, we think keeping the current names and delaying this discussion is better. +::: + +For the default Node environment, Vite creates a module runner that implements evaluation using `new AsyncFunction` running in the same runtime as the server. This runner is an instance of `ModuleRunner` that exposes: + +```ts +class ModuleRunner { + /** + * URL to execute. Accepts file path, server path, or id relative to the root. + * Returns an instantiated module (same as in ssrLoadModule) + */ + public async import(url: string): Promise> + /** + * Other ModuleRunner methods... + */ +``` + +:::info +In the previous iteration, we had `executeUrl` and `executeEntryPoint` methods - they are now merged into a single `import` method. If you want to opt-out of the HMR support, create a runner with `hmr: false` flag. +::: + +The default SSR Node module runner is not exposed. You can use `createNodeEnvironment` API with `createServerModuleRunner` together to create a runner that runs code in the same thread, supports HMR and doesn't conflict with the SSR implementation (in case it's been overriden in the config). Given a Vite server configured in middleware mode as described by the [SSR setup guide](/guide/ssr#setting-up-the-dev-server), let's implement the SSR middleware using the environment API. Error handling is omitted. + +```js +import { + createServer, + createServerModuleRunner, + createNodeEnvironment, +} from 'vite' + +const server = await createServer({ + server: { middlewareMode: true }, + appType: 'custom', + environments: { + node: { + dev: { + // Default Vite SSR environment can be overriden in the config, so + // make sure you have a Node environment before the request is received. + createEnvironment: createNodeEnvironment, + }, + }, + }, +}) + +const runner = createServerModuleRunner(server.environments.node) + +app.use('*', async (req, res, next) => { + const url = req.originalUrl + + // 1. Read index.html + let template = fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8') + + // 2. Apply Vite HTML transforms. This injects the Vite HMR client, + // and also applies HTML transforms from Vite plugins, e.g. global + // preambles from @vitejs/plugin-react + template = await server.transformIndexHtml(url, template) + + // 3. Load the server entry. import(url) automatically transforms + // ESM source code to be usable in Node.js! There is no bundling + // required, and provides full HMR support. + const { render } = await runner.import('/src/entry-server.js') + + // 4. render the app HTML. This assumes entry-server.js's exported + // `render` function calls appropriate framework SSR APIs, + // e.g. ReactDOMServer.renderToString() + const appHtml = await render(url) + + // 5. Inject the app-rendered HTML into the template. + const html = template.replace(``, appHtml) + + // 6. Send the rendered HTML back. + res.status(200).set({ 'Content-Type': 'text/html' }).end(html) +}) +``` + +## Environment agnostic SSR + +::: info +It isn't clear yet what APIs Vite should provide to cover the most common SSR use cases. We are thinking on releasing the Environment API without an official way to do environment agnostic SSR to let the ecosystem explore common patterns first. +::: + +## Separate module graphs + +Vite currently has a mixed Client and SSR module graph. Given an unprocessed or invalidated node, it isn't possible to know if it corresponds to the Client, SSR, or both environments. Module nodes have some properties prefixed, like `clientImportedModules` and `ssrImportedModules` (and `importedModules` that returns the union of both). `importers` contains all importers from both the Client and SSR environment for each module node. A module node also has `transformResult` and `ssrTransformResult`. + +In this proposal, each environment has its module graph (and a backward compatibility layer will be implemented to give time to the ecosystem to migrate). All module graphs have the same signature, so generic algorithms can be implemented to crawl or query the graph without depending on the environment. `hotUpdate` is a good example. When a file is modified, the module graph of each environment will be used to discover the affected modules and perform HMR for each environment independently. + +Each module is represented by a `ModuleNode` instance. Modules may be registered in the graph without yet being processed (`transformResult` would be `null` in that case). `importers` and `importedModules` are also updated after the module is processed. + +```ts +class ModuleNode { + environment: string + + url: string + id: string | null = null + file: string | null = null + + type: 'js' | 'css' + + importers = new Set() + importedModules = new Set() + importedBindings: Map> | null = null + + info?: ModuleInfo + meta?: Record + transformResult: TransformResult | null = null + + acceptedHmrDeps = new Set() + acceptedHmrExports: Set | null = null + isSelfAccepting?: boolean + lastHMRTimestamp = 0 + lastInvalidationTimestamp = 0 +} +``` + +`environment.moduleGraph` is an instance of `ModuleGraph`: + +```ts +export class ModuleGraph { + environment: string + + urlToModuleMap = new Map() + idToModuleMap = new Map() + etagToModuleMap = new Map() + fileToModulesMap = new Map>() + + constructor( + environment: string, + resolveId: (url: string) => Promise, + ) + + async getModuleByUrl(rawUrl: string): Promise + + getModulesByFile(file: string): Set | undefined + + onFileChange(file: string): void + + invalidateModule( + mod: ModuleNode, + seen: Set = new Set(), + timestamp: number = Date.now(), + isHmr: boolean = false, + ): void + + invalidateAll(): void + + async updateModuleInfo( + mod: ModuleNode, + importedModules: Set, + importedBindings: Map> | null, + acceptedModules: Set, + acceptedExports: Set | null, + isSelfAccepting: boolean, + ): Promise | undefined> + + async ensureEntryFromUrl( + rawUrl: string, + setIsSelfAccepting = true, + ): Promise + + createFileOnlyEntry(file: string): ModuleNode + + async resolveUrl(url: string): Promise + + updateModuleTransformResult( + mod: ModuleNode, + result: TransformResult | null, + ): void + + getModuleByEtag(etag: string): ModuleNode | undefined +} +``` + +## Creating new environments + +One of the goals of this feature is to provide a customizable API to process and run code. Users can create new environment types using the exposed primitives. + +```ts +import { DevEnvironment, RemoteEnvironmentTransport } from 'vite' + +function createWorkerdDevEnvironment(server: ViteDevServer, name: string, config?: DevEnvironmentConfig) { + const hot = /* ... */ + const connection = /* ... */ + const transport = new RemoteEnvironmentTransport({ + send: (data) => connection.send(data), + onMessage: (listener) => connection.on('message', listener), + }) + + const workerdDevEnvironment = new DevEnvironment(server, name, { + config: { + resolve: { conditions: ['custom'] }, + ...config, + }, + hot, + runner: { + transport, + }, + }) + return workerdDevEnvironment +} +``` + +Then users can create a workerd environment to do SSR using: + +```js +const ssrEnvironment = createWorkerdEnvironment(server, 'ssr') +``` + +## Environment Configuration + +Environments are explicitely configured with the `environments` config option. + +```js +export default { + environments: { + client: { + resolve: { + conditions: [], // configure the Client environment + }, + }, + ssr: { + dev: { + optimizeDeps: {}, // configure the SSR environment + }, + }, + rsc: { + resolve: { + noExternal: true, // configure a custom environment + }, + }, + }, +} +``` + +Vite's user config also extends from a environment config, letting users add defaults for all environments at the root level. This is quite useful for the common use case of configuring a Vite client only app, that can be done without going through `environments.client`. + +```js +export default { + resolve: { + conditions: [], // configure a default for all environments + }, +} +``` + +The `EnvironmentConfig` interface exposes all the per-environment options. There are `SharedEnvironmentConfig` that apply to both `build` and `dev` environments, like `resolve`. And there are `DevOptions` and `BuildOptions` + +```ts +interface EnvironmentConfig extends SharedEnvironmentConfig { + dev: DevOptions + build: BuildOptions +} +``` + +As we explained, the `UserConfig` interface extends from `EnvironmentConfig`. Environment specific options defined at the root level of user config are used for the default client environment. And environments can be configured explicitely using the `environments` array. The Client and SSR environments, are always present, even if an empty object is set to `environments`. + +```ts +interface UserConfig extends EnvironmentConfig { + environments: Record + // other options +} +``` + +::: info + +The `ssr` top level property has many options in common with `EnvironmentConfig`. This option was created for the same use case as `environments` but only allowed configuration of a small number of options. We're going to deprecate it in favour of a unified way to define environment configuration. + +::: + +## Custom environment instances + +To register a new dev or build environment, you can use a `create` function: + +```js +export default { + environments: { + rsc: { + dev: { + create: (server) => createNodeDevEnvironment(server), + }, + build: { + create: (builder) => createNodeBuildEnvironment(builder), + outDir: '/dist/rsc', + }, + }, + }, +} +``` + +The environment will be accessible in middlewares or plugin hooks through `server.environments`. In plugin hooks, the environment instance is passed in the options so they can do conditions depending on the way they are configured. + +Environment providers like Workerd, can expose an environment configurator for the most common case of using the same runtime for both dev and build environments. The default environment options can also be set so the user doesn't need to do it. + +```js +function createWorkedEnvironment(userConfig) { + return mergeConfig( + { + resolve: { + conditions: [ + /*...*/ + ], + }, + dev: { + createEnvironment: (server, name) => + createWorkerdDevEnvironment(server, name), + }, + build: { + createEnvironment: (builder, name) => + createWorkerdBuildEnvironment(builder, name), + }, + }, + userConfig, + ) +} +``` + +Then the config file can be writen as + +```js +import { workerdEnvironment } from 'vite-environment-workerd' + +export default { + environments: { + ssr: createWorkerdEnvironment({ + build: { + outDir: '/dist/ssr', + }, + }), + rsc: createWorkerdEnvironment({ + build: { + outDir: '/dist/rsc', + }, + }), + ], +} +``` + +In this case we see how a Workerd environment can be set for both the default SSR environment and for a new custom RSC environment. + +## Plugins and environments + +### Accessing the current environment in hooks + +The Vite server has a shared plugin pipeline, but when a module is processed it is always done in the context of a given environment. The `environment` instance is available in the plugin context of `resolveId`, `load`, and `transform`. + +A plugin could use the `environment` instance to: + +- Only apply logic for certain environments. +- Change the way they work depending on the configuration for the environment, which can be accessed using `environment.config`. The vite core resolve plugin modifies the way it resolves ids based on `environment.config.resolve.conditions` for example. + +```ts + transform(code, id) { + console.log(this.enviroment.config.resolve.conditions) + } +``` + +### Registering new environments using hooks + +Plugins can add new environments in the `config` hook: + +```ts + config(config: UserConfig) { + config.environments.rsc ??= {} + } +``` + +An empty object is enough to register the environment, default values from the root level environment config. + +### Configuring environment using hooks + +While the `config` hook is running, the complete list of environments isn't yet known and the environments can be affected by both the default values from the root level environment config or explicitely through the `config.environments` record. +Plugins should set default values using the `config` hook. To configure each environment, they can use the new `configEnvironment` hook. This hook is called for each environment with its partially resolved config including resolution of final defaults. + +```ts + configEnvironment(name: string, config: EnvironmentConfig) { + if (name === 'rsc') { + config.resolve.conditions = // ... +``` + +### The `hotUpdate` hook + +- **Type:** `(ctx: HotContext) => Array | void | Promise | void>` +- **See also:** [HMR API](./api-hmr) + +The `hotUpdate` hook allows plugins to perform custom HMR update handling for a given environment. When a file changes, the HMR algorithm is run for each environment in series according to the order in `server.environments`, so the `hotUpdate` hook will be called multiple times. The hook receives a context object with the following signature: + +```ts +interface HotContext { + file: string + timestamp: number + modules: Array + read: () => string | Promise + server: ViteDevServer +} +``` + +- `this.environment` is the module execution environment where a file update is currently being processed. + +- `modules` is an array of modules in this environment that are affected by the changed file. It's an array because a single file may map to multiple served modules (e.g. Vue SFCs). + +- `read` is an async read function that returns the content of the file. This is provided because, on some systems, the file change callback may fire too fast before the editor finishes updating the file, and direct `fs.readFile` will return empty content. The read function passed in normalizes this behavior. + +The hook can choose to: + +- Filter and narrow down the affected module list so that the HMR is more accurate. + +- Return an empty array and perform a full reload: + + ```js + hotUpdate({ modules, timestamp }) { + if (this.environment.name !== 'client') + return + + this.environment.hot.send({ type: 'full-reload' }) + // Invalidate modules manually + const invalidatedModules = new Set() + for (const mod of modules) { + this.environment.moduleGraph.invalidateModule( + mod, + invalidatedModules, + timestamp, + true + ) + } + return [] + } + ``` + +- Return an empty array and perform complete custom HMR handling by sending custom events to the client: + + ```js + hotUpdate() { + if (this.environment.name !== 'client') + return + + this.environment.hot.send({ + type: 'custom', + event: 'special-update', + data: {} + }) + return [] + } + ``` + + Client code should register the corresponding handler using the [HMR API](./api-hmr) (this could be injected by the same plugin's `transform` hook): + + ```js + if (import.meta.hot) { + import.meta.hot.on('special-update', (data) => { + // perform custom update + }) + } + ``` + +## `ModuleRunner` + +A module runner is instantiated in the target runtime. All APIs in the next section are imported from `vite/module-runner` unless stated otherwise. This export entry point is kept as lightweight as possible, only exporting the minimal needed to create runners in the + +**Type Signature:** + +```ts +export class ModuleRunner { + constructor( + public options: ModuleRunnerOptions, + public evaluator: ModuleEvaluator, + private debug?: ModuleRunnerDebugger, + ) {} + /** + * URL to execute. Accepts file path, server path, or id relative to the root. + */ + public async import(url: string): Promise + /** + * Clear all caches including HMR listeners. + */ + public clearCache(): void + /** + * Clears all caches, removes all HMR listeners, and resets source map support. + * This method doesn't stop the HMR connection. + */ + public async destroy(): Promise + /** + * Returns `true` if the runner has been destroyed by calling `destroy()` method. + */ + public isDestroyed(): boolean +} +``` + +The module evaluator in `ModuleRunner` is responsible for executing the code. Vite exports `ESModulesEvaluator` out of the box, it uses `new AsyncFunction` to evaluate the code. You can provide your own implementation if your JavaScript runtime doesn't support unsafe evaluation. + +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:** + +```js +import { ModuleRunner, ESModulesEvaluator } from 'vite/module-runner' +import { root, fetchModule } from './rpc-implementation.js' + +const moduleRunner = new ModuleRunner( + { + root, + fetchModule, + // you can also provide hmr.connection to support HMR + }, + new ESModulesEvaluator(), +) + +await moduleRunner.import('/src/entry-point.js') +``` + +## `ModuleRunnerOptions` + +```ts +export interface ModuleRunnerOptions { + /** + * Root of the project + */ + root: string + /** + * A set of methods to communicate with the server. + */ + 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. + * You can provide an object to configure how file contents and source maps are resolved for files that were not processed by Vite. + */ + sourcemapInterceptor?: + | false + | 'node' + | 'prepareStackTrace' + | InterceptorOptions + /** + * Disable HMR or configure HMR options. + */ + hmr?: + | false + | { + /** + * Configure how HMR communicates between the client and the server. + */ + connection: ModuleRunnerHMRConnection + /** + * Configure HMR logger. + */ + logger?: false | HMRLogger + } + /** + * Custom module cache. If not provided, it creates a separate module cache for each module runner instance. + */ + moduleCache?: ModuleCacheMap +} +``` + +## `ModuleEvaluator` + +**Type Signature:** + +```ts +export interface ModuleEvaluator { + /** + * Evaluate code that was transformed by Vite. + * @param context Function context + * @param code Transformed code + * @param id ID that was used to fetch the module + */ + runInlinedModule( + context: ModuleRunnerContext, + code: string, + id: string, + ): Promise + /** + * evaluate externalized module. + * @param file File URL to the external module + */ + runExternalModule(file: string): Promise +} +``` + +Vite exports `ESModulesEvaluator` that implements this interface by default. It uses `new AsyncFunction` to evaluate code, so if the code has inlined source map it should contain an [offset of 2 lines](https://tc39.es/ecma262/#sec-createdynamicfunction) to accommodate for new lines added. This is done automatically in the server node environment. If your runner implementation doesn't have this constraint, you should use `fetchModule` (exported from `vite`) directly. + +## RunnerTransport + +**Type Signature:** + +```ts +interface RunnerTransport { + /** + * A method to get the information about the module. + */ + fetchModule: FetchFunction +} +``` + +Transport object that communicates with the environment via an RPC or by directly calling the function. By default, you need to pass an object with `fetchModule` method - it can use any type of RPC inside of it, but Vite also exposes `RemoteRunnerTransport` to make the configuration easier. You need to couple it with the `RemoteEnvironmentTransport` instance on the server like in this example where module runner is created in the worker thread: + +::: code-group + +```ts [worker.js] +import { parentPort } from 'node:worker_threads' +import { fileURLToPath } from 'node:url' +import { + ESModulesEvaluator, + ModuleRunner, + RemoteRunnerTransport, +} from 'vite/module-runner' + +const runner = new ModuleRunner( + { + root: fileURLToPath(new URL('./', import.meta.url)), + transport: new RemoteRunnerTransport({ + send: (data) => parentPort.postMessage(data), + onMessage: (listener) => parentPort.on('message', listener), + timeout: 5000, + }), + }, + new ESModulesEvaluator(), +) +``` + +```ts [server.js] +import { BroadcastChannel } from 'node:worker_threads' +import { createServer, RemoteEnvironmentTransport, DevEnvironment } from 'vite' + +function createWorkerEnvironment(server) { + const worker = new Worker('./worker.js') + return new DevEnvironment(server, 'worker', { + runner: { + transport: new RemoteEnvironmentTransport({ + send: (data) => worker.postMessage(data), + onMessage: (listener) => worker.on('message', listener), + }), + }, + }) +} + +await createServer({ + environments: { + worker: { + dev: { + createEnvironment: createWorkerEnvironment, + }, + }, + }, +}) +``` + +::: + +`RemoteRunnerTransport` and `RemoteEnvironmentTransport` are meant to be used together. If you don't use either of them, then you can define your own function to communicate between the runner and the server. + +## ModuleRunnerHMRConnection + +**Type Signature:** + +```ts +export interface ModuleRunnerHMRConnection { + /** + * Checked before sending messages to the client. + */ + isReady(): boolean + /** + * Send a message to the client. + */ + send(message: string): void + /** + * Configure how HMR is handled when this connection triggers an update. + * This method expects that the connection will start listening for HMR updates and call this callback when it's received. + */ + onUpdate(callback: (payload: HMRPayload) => void): void +} +``` + +This interface defines how HMR communication is established. Vite exports `ServerHMRConnector` from the main entry point to support HMR during Vite SSR. The `isReady` and `send` methods are usually called when the custom event is triggered (like, `import.meta.hot.send("my-event")`). + +`onUpdate` is called only once when the new module runner is initiated. It passed down a method that should be called when connection triggers the HMR event. The implementation depends on the type of connection (as an example, it can be `WebSocket`/`EventEmitter`/`MessageChannel`), but it usually looks something like this: + +```js +function onUpdate(callback) { + this.connection.on('hmr', (event) => callback(event.data)) +} +``` + +The callback is queued and it will wait for the current update to be resolved before processing the next update. Unlike the browser implementation, HMR updates in a module runner will wait until all listeners (like, `vite:beforeUpdate`/`vite:beforeFullReload`) are finished before updating the modules. + +## Environments during build + +Plugin hooks also receive the environment instance during build. This replaces the `ssr` boolean we have been passing them so far. + +In the CLI, calling `vite build` will build the Client. It is equivalent to calling `vite build --environment=client`. + +The build the SSR server, the `--ssr` shourcut can be used: `vite build --ssr`. This is equivalent to calling `vite build --environment=ssr`. + +Other non-default environments can be build using `vite build --environment=name`. + +## Building all environments + +Calling `vite build --all` will instantiate a `ViteBuilder` (build-time equivalent to a `ViteDevServer`) to build all configured environments for production. By default the build of environments is run in series respecting the order of the `environments` record. A framework or user can further configure how the environments are built using: + +```js +export default { + builder: { + buildEnvironments: asnyc (builder) => { + return Promise.all(Object.values(builder.environments).map( environment => builder.build(environment) )) + } + } +} +``` + +## Backward Compatibility + +The current Vite server API will be deprecated but keep working during the next major. + +| Before | After | +| :-------------------------------------------: | :------------------------------------------------: | +| `server.transformRequest(url)` | `server.environments.client.transformRequest(url)` | +| `server.transformRequest(url, { ssr: true })` | `server.environments.ssr.tranformRequest(url)` | +| `server.warmupRequest(url)` | `server.environments.client.warmupRequest(url)` | +| `server.ssrLoadModule(url)` | `TBD` | +| `server.moduleGraph` | `environment.moduleGraph` | +| `handleHotUpdate` | `hotUpdate` | + +The last one is just an idea. We may want to keep `server.open(url)` around. + +The `server.moduleGraph` will keep returning a mixed view of the client and ssr module graphs. Backward compatible mixed module nodes will be returned from all previous functions. The same scheme is used for the module nodes passed to `handleHotUpdate`. This is the most difficult change to get right regarding backward compatibility. We may need to accept small breaking changes when we release the API in Vite 6, making it opt-in until then when releasing the API as experimental in Vite 5.2. + +## Open Questions and Alternatives + +There are some open questions and alternatives as info boxes interlined in the guide. + +Names for concepts and the API are the best we could currently find, which we should keep discussing before releasing if we end up adopting this proposal. Here are some of the alternative names we discussed in the process of creating this proposal. + +### ModuleLoader vs Environment + +Instead of `DevEnvironment`, we thought of calling the environment piece inside the Vite Server a `ModuleLoader`. So `server.environments.client` would be `server.moduleLoaders.client`. It has some advantages, `transformRequest(url)` could be renamed to `moduleLoader.load(url)`. We could pass to hooks a `loader` string instead of an `environment` string. `vite build --loader=node` could also be ok, but it is already a stretch. A `ModuleLoader` having a `run()` function that connects it to the `ModuleRunner` in the associated runtime also didn't seem like a good fit though. And `loader` could be confused with a node loader, or with the module loader in the target runtime. + +### Runtime vs Environment + +We also discussed naming runtime to the concept we call environment in this proposal. We decided to go with Environment because a Runtime refers to node, bun, deno, workerd, a browser. But we need to be able to define two different module execution "environments" for the same runtime. For example SSR and RSC environments, both running in the same node runtime. diff --git a/docs/guide/api-vite-runtime.md b/docs/guide/api-vite-runtime.md deleted file mode 100644 index 55559a61b37491..00000000000000 --- a/docs/guide/api-vite-runtime.md +++ /dev/null @@ -1,230 +0,0 @@ -# Vite Runtime API - -:::warning Low-level API -This API was introduced in Vite 5.1 as an experimental feature. It was added to [gather feedback](https://github.com/vitejs/vite/discussions/15774). There will likely be breaking changes, so make sure to pin the Vite version to `~5.1.0` when using it. This is a low-level API meant for library and framework authors. If your goal is to create an application, make sure to check out the higher-level SSR plugins and tools at [Awesome Vite SSR section](https://github.com/vitejs/awesome-vite#ssr) first. - -Currently, the API is being revised as the [Environment API](https://github.com/vitejs/vite/discussions/16358) which is released at `^6.0.0-alpha.0`. -::: - -The "Vite Runtime" is a tool that allows running any code by processing it with Vite plugins first. It is different from `server.ssrLoadModule` because the runtime implementation is decoupled from the server. This allows library and framework authors to implement their own layer of communication between the server and the runtime. - -One of the goals of this feature is to provide a customizable API to process and run the code. Vite provides enough tools to use Vite Runtime out of the box, but users can build upon it if their needs do not align with Vite's built-in implementation. - -All APIs can be imported from `vite/runtime` unless stated otherwise. - -## `ViteRuntime` - -**Type Signature:** - -```ts -export class ViteRuntime { - constructor( - public options: ViteRuntimeOptions, - public runner: ViteModuleRunner, - private debug?: ViteRuntimeDebugger, - ) {} - /** - * URL to execute. Accepts file path, server path, or id relative to the root. - */ - public async import(url: string): Promise - /** - * Clear all caches including HMR listeners. - */ - public clearCache(): void - /** - * Clears all caches, removes all HMR listeners, and resets source map support. - * This method doesn't stop the HMR connection. - */ - public async destroy(): Promise - /** - * Returns `true` if the runtime has been destroyed by calling `destroy()` method. - */ - public isDestroyed(): boolean -} -``` - -::: tip Advanced Usage -If you are just migrating from `server.ssrLoadModule` and want to support HMR, consider using [`createViteRuntime`](#createviteruntime) instead. -::: - -The `ViteRuntime` class requires `root` and `fetchModule` options when initiated. Vite exposes `ssrFetchModule` on the [`server`](/guide/api-javascript) instance for easier integration with Vite SSR. Vite also exports `fetchModule` from its main entry point - it doesn't make any assumptions about how the code is running unlike `ssrFetchModule` that expects the code to run using `new Function`. This can be seen in source maps that these functions return. - -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. - -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:** - -```js -import { ViteRuntime, ESModulesRunner } from 'vite/runtime' -import { root, fetchModule } from './rpc-implementation.js' - -const runtime = new ViteRuntime( - { - root, - fetchModule, - // you can also provide hmr.connection to support HMR - }, - new ESModulesRunner(), -) - -await runtime.import('/src/entry-point.js') -``` - -## `ViteRuntimeOptions` - -```ts -export interface ViteRuntimeOptions { - /** - * Root of the project - */ - root: string - /** - * 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. - */ - fetchModule: FetchFunction - /** - * 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. - * You can provide an object to configure how file contents and source maps are resolved for files that were not processed by Vite. - */ - sourcemapInterceptor?: - | false - | 'node' - | 'prepareStackTrace' - | InterceptorOptions - /** - * 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 - } - /** - * Custom module cache. If not provided, it creates a separate module cache for each ViteRuntime instance. - */ - moduleCache?: ModuleCacheMap -} -``` - -## `ViteModuleRunner` - -**Type Signature:** - -```ts -export interface ViteModuleRunner { - /** - * 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, - code: string, - id: string, - ): Promise - /** - * Run externalized module. - * @param file File URL to the external module - */ - runExternalModule(file: string): Promise -} -``` - -Vite exports `ESModulesRunner` that implements this interface by default. It uses `new AsyncFunction` to run code, so if the code has inlined source map it should contain an [offset of 2 lines](https://tc39.es/ecma262/#sec-createdynamicfunction) to accommodate for new lines added. This is done automatically by `server.ssrFetchModule`. If your runner implementation doesn't have this constraint, you should use `fetchModule` (exported from `vite`) directly. - -## HMRRuntimeConnection - -**Type Signature:** - -```ts -export interface HMRRuntimeConnection { - /** - * Checked before sending messages to the client. - */ - isReady(): boolean - /** - * Send message to the client. - */ - send(message: string): void - /** - * 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. - */ - onUpdate(callback: (payload: HMRPayload) => void): void -} -``` - -This interface defines how HMR communication is established. Vite exports `ServerHMRConnector` from the main entry point to support HMR during Vite SSR. The `isReady` and `send` methods are usually called when the custom event is triggered (like, `import.meta.hot.send("my-event")`). - -`onUpdate` is called only once when the new runtime is initiated. It passed down a method that should be called when connection triggers the HMR event. The implementation depends on the type of connection (as an example, it can be `WebSocket`/`EventEmitter`/`MessageChannel`), but it usually looks something like this: - -```js -function onUpdate(callback) { - this.connection.on('hmr', (event) => callback(event.data)) -} -``` - -The callback is queued and it will wait for the current update to be resolved before processing the next update. Unlike the browser implementation, HMR updates in Vite Runtime wait until all listeners (like, `vite:beforeUpdate`/`vite:beforeFullReload`) are finished before updating the modules. - -## `createViteRuntime` - -**Type Signature:** - -```ts -async function createViteRuntime( - server: ViteDevServer, - options?: MainThreadRuntimeOptions, -): Promise -``` - -**Example Usage:** - -```js -import { createServer } from 'vite' - -const __dirname = fileURLToPath(new URL('.', import.meta.url)) - -;(async () => { - const server = await createServer({ - root: __dirname, - }) - await server.listen() - - const runtime = await createViteRuntime(server) - await runtime.import('/src/entry-point.js') -})() -``` - -This method serves as an easy replacement for `server.ssrLoadModule`. Unlike `ssrLoadModule`, `createViteRuntime` provides HMR support out of the box. You can pass down [`options`](#mainthreadruntimeoptions) to customize how SSR runtime behaves to suit your needs. - -## `MainThreadRuntimeOptions` - -```ts -export interface MainThreadRuntimeOptions - extends Omit { - /** - * Disable HMR or configure HMR logger. - */ - hmr?: - | false - | { - logger?: false | HMRLogger - } - /** - * Provide a custom module runner. This controls how the code is executed. - */ - runner?: ViteModuleRunner -} -``` diff --git a/docs/images/vite-environments.svg b/docs/images/vite-environments.svg new file mode 100644 index 00000000000000..8f7c133b1d8bc2 --- /dev/null +++ b/docs/images/vite-environments.svg @@ -0,0 +1,40 @@ +Vite Dev ServerVite Dev ServerBrowser  +EnvironmentBrowser  +EnvironmentNode  +EnvironmentNode  +EnvironmentBrowser  +RuntimeBrowser  +RuntimeWorkerd  + RuntimeWorkerd  + RuntimeWorkerd  +EnvironmentWorkerd  +EnvironmentNode  +RuntimeNode  +RuntimeBrowser  +Module  +RunnerBrowser  +Module  +RunnerNode  +Module  +RunnerNode  +Module  +RunnerWorkerd  +Module  +RunnerWorkerd  +Module  +RunnerVitest JSDOM  +EnvironmentVitest JSDOM  +EnvironmentWorker  +Thread  +Module  +RunnerWorker  +Thread  +Module  +RunnerHTTP ServerHTTP ServerMiddlewaresMiddlewaresPlugins PipelinePlugins PipelineWeb SocketWeb Socket \ No newline at end of file From ad2b0bf5555b545314992bda69f6d8635f051e1c Mon Sep 17 00:00:00 2001 From: patak-dev Date: Fri, 19 Apr 2024 21:55:49 +0200 Subject: [PATCH 004/123] chore: remove configureServer from more plugins --- packages/vite/src/node/plugins/importMetaGlob.ts | 4 ++-- packages/vite/src/node/plugins/worker.ts | 8 +------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/vite/src/node/plugins/importMetaGlob.ts b/packages/vite/src/node/plugins/importMetaGlob.ts index 157d7a91ec8e4f..85030d714e2e4f 100644 --- a/packages/vite/src/node/plugins/importMetaGlob.ts +++ b/packages/vite/src/node/plugins/importMetaGlob.ts @@ -52,10 +52,10 @@ export function importGlobPlugin(config: ResolvedConfig): Plugin { return { name: 'vite:import-glob', - configureServer() { + buildStart() { importGlobMaps.clear() }, - async transform(code, id, options) { + async transform(code, id) { if (!code.includes('import.meta.glob')) return const result = await transformGlobImport( code, diff --git a/packages/vite/src/node/plugins/worker.ts b/packages/vite/src/node/plugins/worker.ts index 2e13ce45c55cc4..6ac5f470b65082 100644 --- a/packages/vite/src/node/plugins/worker.ts +++ b/packages/vite/src/node/plugins/worker.ts @@ -3,7 +3,6 @@ import MagicString from 'magic-string' import type { OutputChunk } from 'rollup' import type { ResolvedConfig } from '../config' import type { Plugin } from '../plugin' -import type { ViteDevServer } from '../server' import { ENV_ENTRY, ENV_PUBLIC_PATH } from '../constants' import { encodeURIPath, @@ -209,16 +208,11 @@ export function webWorkerPostPlugin(): Plugin { export function webWorkerPlugin(config: ResolvedConfig): Plugin { const isBuild = config.command === 'build' - let server: ViteDevServer const isWorker = config.isWorker return { name: 'vite:worker', - configureServer(_server) { - server = _server - }, - buildStart() { if (isWorker) { return @@ -261,7 +255,7 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin { } else if (workerType === 'ignore') { if (isBuild) { injectEnv = '' - } else if (server) { + } else { // 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 environment = this.environment From 5f6b62f22144a4ca1fc82161308a204a3539b546 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Fri, 19 Apr 2024 21:59:32 +0200 Subject: [PATCH 005/123] chore: rename plugin.split to plugin.create --- packages/vite/src/node/plugin.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/vite/src/node/plugin.ts b/packages/vite/src/node/plugin.ts index 4c4bf72bbeb663..872a0cf99b4d3b 100644 --- a/packages/vite/src/node/plugin.ts +++ b/packages/vite/src/node/plugin.ts @@ -166,12 +166,12 @@ export type BoundedPlugin
= BasePlugin export interface Plugin extends BasePlugin { /** - * Split the plugin into multiple plugins based on the environment. + * Spawn 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 + create?: (environment: PluginEnvironment) => BoundedPluginOption /** * Enforce plugin invocation tier similar to webpack loaders. Hooks ordering * is still subject to the `order` property in the hook object. @@ -319,8 +319,8 @@ export async function resolveBoundedPlugins( ): Promise { const resolvedPlugins: BoundedPlugin[] = [] for (const plugin of environment.config.plugins) { - if (plugin.split) { - const boundedPlugin = await plugin.split(environment) + if (plugin.create) { + const boundedPlugin = await plugin.create(environment) if (boundedPlugin) { const flatPlugins = await asyncFlattenBoundedPlugin( environment, From 431cd4b76a437a478c6645089cea4410e3c2565a Mon Sep 17 00:00:00 2001 From: patak-dev Date: Fri, 19 Apr 2024 22:10:47 +0200 Subject: [PATCH 006/123] fix: export missing types --- packages/vite/src/node/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index 3cedcfc2563d2e..fb6482ce7be62b 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -47,6 +47,8 @@ export type { UserConfigFn, UserConfigFnObject, UserConfigFnPromise, + DevEnvironmentOptions, + ResolvedDevEnvironmentOptions, } from './config' export type { PluginOption } from './plugin' export type { FilterPattern } from './utils' @@ -60,6 +62,8 @@ export type { ResolvedServerUrls, } from './server' export type { + ViteBuilder, + BuilderOptions, BuildOptions, BuildEnvironmentOptions, LibraryOptions, From 635aad5f6917405005f6d5ed1677dbc425aef39b Mon Sep 17 00:00:00 2001 From: patak-dev Date: Sat, 20 Apr 2024 07:02:12 +0200 Subject: [PATCH 007/123] release: v6.0.0-alpha.3 --- packages/vite/CHANGELOG.md | 16 ++++++++++++++++ packages/vite/package.json | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/vite/CHANGELOG.md b/packages/vite/CHANGELOG.md index 08e7293e470bf2..ddb69bb1781ebd 100644 --- a/packages/vite/CHANGELOG.md +++ b/packages/vite/CHANGELOG.md @@ -1,3 +1,19 @@ +## 6.0.0-alpha.3 (2024-04-20) + +* fix: add base to virtual html (#16442) ([721f94d](https://github.com/vitejs/vite/commit/721f94d)), closes [#16442](https://github.com/vitejs/vite/issues/16442) +* fix: adjust esm syntax judgment logic (#16436) ([af72eab](https://github.com/vitejs/vite/commit/af72eab)), closes [#16436](https://github.com/vitejs/vite/issues/16436) +* fix: don't add outDirs to watch.ignored if emptyOutDir is false (#16453) ([6a127d6](https://github.com/vitejs/vite/commit/6a127d6)), closes [#16453](https://github.com/vitejs/vite/issues/16453) +* fix: export missing types ([431cd4b](https://github.com/vitejs/vite/commit/431cd4b)) +* fix(cspNonce): don't overwrite existing nonce values (#16415) ([b872635](https://github.com/vitejs/vite/commit/b872635)), closes [#16415](https://github.com/vitejs/vite/issues/16415) +* chore: remove configureServer from more plugins ([ad2b0bf](https://github.com/vitejs/vite/commit/ad2b0bf)) +* chore: rename plugin.split to plugin.create ([5f6b62f](https://github.com/vitejs/vite/commit/5f6b62f)) +* chore(deps): update dependency eslint-plugin-n to v17 (#16381) ([6cccef7](https://github.com/vitejs/vite/commit/6cccef7)), closes [#16381](https://github.com/vitejs/vite/issues/16381) +* feat: environment api (#16129) ([f684d4c](https://github.com/vitejs/vite/commit/f684d4c)), closes [#16129](https://github.com/vitejs/vite/issues/16129) +* feat: show warning if root is in build.outDir (#16454) ([11444dc](https://github.com/vitejs/vite/commit/11444dc)), closes [#16454](https://github.com/vitejs/vite/issues/16454) +* feat: write cspNonce to style tags (#16419) ([8e54bbd](https://github.com/vitejs/vite/commit/8e54bbd)), closes [#16419](https://github.com/vitejs/vite/issues/16419) + + + ## 6.0.0-alpha.2 (2024-04-09) * chore: update ([46c8910](https://github.com/vitejs/vite/commit/46c8910)) diff --git a/packages/vite/package.json b/packages/vite/package.json index 3a0648671f0460..9ee5d6ced7af30 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -1,6 +1,6 @@ { "name": "vite", - "version": "6.0.0-alpha.2", + "version": "6.0.0-alpha.3", "type": "module", "license": "MIT", "author": "Evan You", From d7c6fc487c74275654b40a97a55795a1d5e22b02 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Sat, 20 Apr 2024 07:15:26 +0200 Subject: [PATCH 008/123] docs: Environment(name, config) update --- docs/guide/api-vite-environment.md | 46 ++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/docs/guide/api-vite-environment.md b/docs/guide/api-vite-environment.md index ee2905b69422b1..56b3edb5c72423 100644 --- a/docs/guide/api-vite-environment.md +++ b/docs/guide/api-vite-environment.md @@ -52,7 +52,17 @@ class DevEnvironment { * Graph of module nodes, with the imported relationship between * processed modules and the cached result of the processed code. */ - moduleGraph: ModuleGraph + moduleGraph: EnvironmentModuleGraph + /** + * Resolved plugins for this environment, including the ones + * created using the per-environment `create` hook + */ + plugins: BoundedPlugin[] + /** + * Allows to resolve, load, and transform code through the + * environment plugins pipeline + */ + pluginContainer: BoundedPluginContatiner /** * TBD: This abstraction isn't yet clear * Trigger the execution of a module using the associated module runner @@ -66,7 +76,7 @@ class DevEnvironment { */ config: ResolvedDevEnvironmentConfig - constructor(server, { name, hot, run, config }: DevEnvironmentOptions) + constructor(name, config, { hot, run, options }: DevEnvironmentOptions) /** * Resolve the URL to an id, load it, and process the code using the @@ -287,7 +297,7 @@ One of the goals of this feature is to provide a customizable API to process and ```ts import { DevEnvironment, RemoteEnvironmentTransport } from 'vite' -function createWorkerdDevEnvironment(server: ViteDevServer, name: string, config?: DevEnvironmentConfig) { +function createWorkerdDevEnvironment(name: string, config: ResolvedConfig, options?: DevEnvironmentOptions) { const hot = /* ... */ const connection = /* ... */ const transport = new RemoteEnvironmentTransport({ @@ -295,10 +305,10 @@ function createWorkerdDevEnvironment(server: ViteDevServer, name: string, config onMessage: (listener) => connection.on('message', listener), }) - const workerdDevEnvironment = new DevEnvironment(server, name, { - config: { + const workerdDevEnvironment = new DevEnvironment(name, config, { + options: { resolve: { conditions: ['custom'] }, - ...config, + ...options, }, hot, runner: { @@ -312,7 +322,7 @@ function createWorkerdDevEnvironment(server: ViteDevServer, name: string, config Then users can create a workerd environment to do SSR using: ```js -const ssrEnvironment = createWorkerdEnvironment(server, 'ssr') +const ssrEnvironment = createWorkerdEnvironment('ssr', config) ``` ## Environment Configuration @@ -384,10 +394,14 @@ export default { environments: { rsc: { dev: { - create: (server) => createNodeDevEnvironment(server), + createEnvironment(name, config) { + return createNodeDevEnvironment(name, config) + } }, build: { - create: (builder) => createNodeBuildEnvironment(builder), + createEnvironment(name, config) { + return createNodeBuildEnvironment(name, config) + } outDir: '/dist/rsc', }, }, @@ -409,12 +423,14 @@ function createWorkedEnvironment(userConfig) { ], }, dev: { - createEnvironment: (server, name) => - createWorkerdDevEnvironment(server, name), + createEnvironment(name, config) { + return createWorkerdDevEnvironment(name, config) + }, }, build: { - createEnvironment: (builder, name) => - createWorkerdBuildEnvironment(builder, name), + createEnvironment(name, config) { + return createWorkerdBuildEnvironment(builder, name) + }, }, }, userConfig, @@ -729,9 +745,9 @@ const runner = new ModuleRunner( import { BroadcastChannel } from 'node:worker_threads' import { createServer, RemoteEnvironmentTransport, DevEnvironment } from 'vite' -function createWorkerEnvironment(server) { +function createWorkerEnvironment(name, config) { const worker = new Worker('./worker.js') - return new DevEnvironment(server, 'worker', { + return new DevEnvironment(name, config, { runner: { transport: new RemoteEnvironmentTransport({ send: (data) => worker.postMessage(data), From 1904acd47df39a0594a3e9189b707c4541dcd4bc Mon Sep 17 00:00:00 2001 From: patak-dev Date: Sat, 20 Apr 2024 07:24:08 +0200 Subject: [PATCH 009/123] docs: remove alternatives section from the docs, better to keep that in the PR and discussions now --- docs/guide/api-vite-environment.md | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/docs/guide/api-vite-environment.md b/docs/guide/api-vite-environment.md index 56b3edb5c72423..311cd418b0b3bd 100644 --- a/docs/guide/api-vite-environment.md +++ b/docs/guide/api-vite-environment.md @@ -846,17 +846,3 @@ The current Vite server API will be deprecated but keep working during the next The last one is just an idea. We may want to keep `server.open(url)` around. The `server.moduleGraph` will keep returning a mixed view of the client and ssr module graphs. Backward compatible mixed module nodes will be returned from all previous functions. The same scheme is used for the module nodes passed to `handleHotUpdate`. This is the most difficult change to get right regarding backward compatibility. We may need to accept small breaking changes when we release the API in Vite 6, making it opt-in until then when releasing the API as experimental in Vite 5.2. - -## Open Questions and Alternatives - -There are some open questions and alternatives as info boxes interlined in the guide. - -Names for concepts and the API are the best we could currently find, which we should keep discussing before releasing if we end up adopting this proposal. Here are some of the alternative names we discussed in the process of creating this proposal. - -### ModuleLoader vs Environment - -Instead of `DevEnvironment`, we thought of calling the environment piece inside the Vite Server a `ModuleLoader`. So `server.environments.client` would be `server.moduleLoaders.client`. It has some advantages, `transformRequest(url)` could be renamed to `moduleLoader.load(url)`. We could pass to hooks a `loader` string instead of an `environment` string. `vite build --loader=node` could also be ok, but it is already a stretch. A `ModuleLoader` having a `run()` function that connects it to the `ModuleRunner` in the associated runtime also didn't seem like a good fit though. And `loader` could be confused with a node loader, or with the module loader in the target runtime. - -### Runtime vs Environment - -We also discussed naming runtime to the concept we call environment in this proposal. We decided to go with Environment because a Runtime refers to node, bun, deno, workerd, a browser. But we need to be able to define two different module execution "environments" for the same runtime. For example SSR and RSC environments, both running in the same node runtime. From d4b155c859dff9c66eca3ea98d20006ce90e44f5 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Sat, 20 Apr 2024 21:31:46 +0200 Subject: [PATCH 010/123] docs: update to latest implementation --- docs/guide/api-vite-environment.md | 43 ++++++++++++++++-------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/docs/guide/api-vite-environment.md b/docs/guide/api-vite-environment.md index 311cd418b0b3bd..01612d1036cfc5 100644 --- a/docs/guide/api-vite-environment.md +++ b/docs/guide/api-vite-environment.md @@ -5,14 +5,13 @@ Initial work for this API was introduced in Vite 5.1 with the name "Vite Runtime Resources: -- [PR for the new API docs](https://github.com/vitejs/vite/pull/16089). +- [Environment API PR](https://github.com/vitejs/vite/pull/16471) where the new API is implemented and reviewed. - [Feedback discussion](https://github.com/vitejs/vite/discussions/16358) where we are gathering feedback about the new APIs. -- [Environment API branch](https://github.com/vitejs/vite/pull/16129) where the new API is implemented. -Feel free to send us PRs against this branch to fix the issues you discover. Please share with us your feedback as you test the proposal. +Feel free to send us PRs against the `v6/environment-api` branch to fix the issues you discover. Please share with us your feedback as you test the proposal. ::: -A single Vite dev server can be used to interact with different module execution environments concurrently. We'll use the word environment to refer to a configured Vite processing pipeline that can resolve ids, load, and process source code and is connected to a runtime where the code is executed. The transformed source code is called a module, and the relationships between the modules processed in each environment are kept in a module graph. The code for these modules is sent to the runtimes associated with each environment to be executed. When a module is evaluated, the runtime will request its imported modules triggering the processing of a section of the module graph. In a typical Vite app, an environments will be used for the ES modules served to the client and for the server program that does SSR. An app can do SSR on a Node server, but also on an edge runtime like Workerd. So we can have different types of environments on the same Vite server: browser environments, node environments, and workerd environments to name a few. +A single Vite dev server can be used to interact with different module execution environments concurrently. We'll use the word environment to refer to a configured Vite processing pipeline that can resolve ids, load, and process source code and is connected to a runtime where the code is executed. The transformed source code is called a module, and the relationships between the modules processed in each environment are kept in a module graph. The code for these modules is sent to the runtimes associated with each environment to be executed. When a module is evaluated, the runtime will request its imported modules triggering the processing of a section of the module graph. In a typical Vite app, an environments will be used for the ES modules served to the client and for the server program that does SSR. An app can do SSR on a Node server, but also on an edge runtime like Workerd. So we can have different types of environments on the same Vite server: browser environments, node environments, and workerd environments, to name a few. A Vite Module Runner allows running any code by processing it with Vite plugins first. It is different from `server.ssrLoadModule` because the runner implementation is decoupled from the server. This allows library and framework authors to implement their layer of communication between the Vite server and the runner. The browser communicates with its corresponding environment using the server Web Socket and through HTTP requests. The Node Module runner can directly do function calls to process modules as it is running in the same process. Other environments could run modules connecting to an edge runtime like workerd, or a Worker Thread as Vitest does. @@ -27,14 +26,16 @@ A Vite dev server exposes two environments by default: a Client environment and The available environments can be accessed using `server.environments`: ```js -server.environments.client.transformRequest(url) +const environment = server.environments.client + +environment.transformRequest(url) console.log(server.environments.ssr.moduleGraph) ``` -Normally, the current `environment` instance will be available as part of the context of the code being run so the need to access them through `server.environments` should be rare. For example, inside plugin hooks. +Most of the time, the current `environment` instance will be available as part of the context of the code being run so the need to access them through `server.environments` should be rare. For example, inside plugin hooks the enviornment is exposed as part of the `PluginContext`, so it can be accessed using `this.environment`. -An dev environment is an instance of the `DevEnvironment` class: +A dev environment is an instance of the `DevEnvironment` class: ```ts class DevEnvironment { @@ -135,7 +136,7 @@ class ModuleRunner { ``` :::info -In the previous iteration, we had `executeUrl` and `executeEntryPoint` methods - they are now merged into a single `import` method. If you want to opt-out of the HMR support, create a runner with `hmr: false` flag. +In the v5.1 Runtime API, there were `executeUrl` and `executeEntryPoint` methods - they are now merged into a single `import` method. If you want to opt-out of the HMR support, create a runner with `hmr: false` flag. ::: The default SSR Node module runner is not exposed. You can use `createNodeEnvironment` API with `createServerModuleRunner` together to create a runner that runs code in the same thread, supports HMR and doesn't conflict with the SSR implementation (in case it's been overriden in the config). Given a Vite server configured in middleware mode as described by the [SSR setup guide](/guide/ssr#setting-up-the-dev-server), let's implement the SSR middleware using the environment API. Error handling is omitted. @@ -200,14 +201,16 @@ It isn't clear yet what APIs Vite should provide to cover the most common SSR us ## Separate module graphs -Vite currently has a mixed Client and SSR module graph. Given an unprocessed or invalidated node, it isn't possible to know if it corresponds to the Client, SSR, or both environments. Module nodes have some properties prefixed, like `clientImportedModules` and `ssrImportedModules` (and `importedModules` that returns the union of both). `importers` contains all importers from both the Client and SSR environment for each module node. A module node also has `transformResult` and `ssrTransformResult`. +Each environment has an isolated module graph. All module graphs have the same signature, so generic algorithms can be implemented to crawl or query the graph without depending on the environment. `hotUpdate` is a good example. When a file is modified, the module graph of each environment will be used to discover the affected modules and perform HMR for each environment independently. -In this proposal, each environment has its module graph (and a backward compatibility layer will be implemented to give time to the ecosystem to migrate). All module graphs have the same signature, so generic algorithms can be implemented to crawl or query the graph without depending on the environment. `hotUpdate` is a good example. When a file is modified, the module graph of each environment will be used to discover the affected modules and perform HMR for each environment independently. +::: info +Vite v5 had a mixed Client and SSR module graph. Given an unprocessed or invalidated node, it isn't possible to know if it corresponds to the Client, SSR, or both environments. Module nodes have some properties prefixed, like `clientImportedModules` and `ssrImportedModules` (and `importedModules` that returns the union of both). `importers` contains all importers from both the Client and SSR environment for each module node. A module node also has `transformResult` and `ssrTransformResult`. A backward compatibility layer allows the ecosystem to migrate from the deprecated `server.moduleGraph`. +::: -Each module is represented by a `ModuleNode` instance. Modules may be registered in the graph without yet being processed (`transformResult` would be `null` in that case). `importers` and `importedModules` are also updated after the module is processed. +Each module is represented by a `EnvironmentModuleNode` instance. Modules may be registered in the graph without yet being processed (`transformResult` would be `null` in that case). `importers` and `importedModules` are also updated after the module is processed. ```ts -class ModuleNode { +class EnvironmentModuleNode { environment: string url: string @@ -232,10 +235,10 @@ class ModuleNode { } ``` -`environment.moduleGraph` is an instance of `ModuleGraph`: +`environment.moduleGraph` is an instance of `EnvironmentModuleGraph`: ```ts -export class ModuleGraph { +export class EnvironmentModuleGraph { environment: string urlToModuleMap = new Map() @@ -361,10 +364,10 @@ export default { } ``` -The `EnvironmentConfig` interface exposes all the per-environment options. There are `SharedEnvironmentConfig` that apply to both `build` and `dev` environments, like `resolve`. And there are `DevOptions` and `BuildOptions` +The `EnvironmentOptions` interface exposes all the per-environment options. There are `SharedEnvironmentOptions` that apply to both `build` and `dev` environments, like `resolve`. And there are `DevOptions` and `BuildOptions` ```ts -interface EnvironmentConfig extends SharedEnvironmentConfig { +interface EnvironmentOptions extends SharedEnvironmentOptions { dev: DevOptions build: BuildOptions } @@ -373,15 +376,15 @@ interface EnvironmentConfig extends SharedEnvironmentConfig { As we explained, the `UserConfig` interface extends from `EnvironmentConfig`. Environment specific options defined at the root level of user config are used for the default client environment. And environments can be configured explicitely using the `environments` array. The Client and SSR environments, are always present, even if an empty object is set to `environments`. ```ts -interface UserConfig extends EnvironmentConfig { - environments: Record +interface UserConfig extends EnvironmentOptions { + environments: Record // other options } ``` ::: info -The `ssr` top level property has many options in common with `EnvironmentConfig`. This option was created for the same use case as `environments` but only allowed configuration of a small number of options. We're going to deprecate it in favour of a unified way to define environment configuration. +The `ssr` top level property has many options in common with `EnvironmentOptions`. This option was created for the same use case as `environments` but only allowed configuration of a small number of options. We're going to deprecate it in favour of a unified way to define environment configuration. ::: @@ -470,7 +473,7 @@ The Vite server has a shared plugin pipeline, but when a module is processed it A plugin could use the `environment` instance to: - Only apply logic for certain environments. -- Change the way they work depending on the configuration for the environment, which can be accessed using `environment.config`. The vite core resolve plugin modifies the way it resolves ids based on `environment.config.resolve.conditions` for example. +- Change the way they work depending on the configuration for the environment, which can be accessed using `environment.options`. The vite core resolve plugin modifies the way it resolves ids based on `environment.options.resolve.conditions` for example. ```ts transform(code, id) { From 5c5efe46948b0593a008475a0abcb65ebc6602a1 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Sun, 21 Apr 2024 14:22:30 +0900 Subject: [PATCH 011/123] fix(v6): fix `ssrEmitAssets` compat (#16480) Co-authored-by: patak-dev --- .../vite/src/node/__tests__/build.spec.ts | 66 ++++++++++++++++++- .../emit-assets/css-module.module.css | 6 ++ .../fixtures/emit-assets/css-normal.css | 6 ++ .../__tests__/fixtures/emit-assets/entry.mjs | 6 ++ packages/vite/src/node/config.ts | 6 ++ 5 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 packages/vite/src/node/__tests__/fixtures/emit-assets/css-module.module.css create mode 100644 packages/vite/src/node/__tests__/fixtures/emit-assets/css-normal.css create mode 100644 packages/vite/src/node/__tests__/fixtures/emit-assets/entry.mjs diff --git a/packages/vite/src/node/__tests__/build.spec.ts b/packages/vite/src/node/__tests__/build.spec.ts index 2dad85578812cc..10e8c783565378 100644 --- a/packages/vite/src/node/__tests__/build.spec.ts +++ b/packages/vite/src/node/__tests__/build.spec.ts @@ -4,7 +4,12 @@ import colors from 'picocolors' import { describe, expect, test, vi } from 'vitest' import type { OutputChunk, OutputOptions, RollupOutput } from 'rollup' import type { LibraryFormats, LibraryOptions } from '../build' -import { build, resolveBuildOutputs, resolveLibFilename } from '../build' +import { + build, + createViteBuilder, + resolveBuildOutputs, + resolveLibFilename, +} from '../build' import type { Logger } from '../logger' import { createLogger } from '../logger' @@ -576,6 +581,65 @@ describe('resolveBuildOutputs', () => { ), ) }) + + test('ssrEmitAssets', async () => { + const result = await build({ + root: resolve(__dirname, 'fixtures/emit-assets'), + logLevel: 'silent', + build: { + ssr: true, + ssrEmitAssets: true, + rollupOptions: { + input: { + index: '/entry', + }, + }, + }, + }) + expect(result).toMatchObject({ + output: [ + { + fileName: 'index.mjs', + }, + { + fileName: expect.stringMatching(/assets\/index-\w*\.css/), + }, + ], + }) + }) + + test('emitAssets', async () => { + const builder = await createViteBuilder( + {}, + { + root: resolve(__dirname, 'fixtures/emit-assets'), + environments: { + ssr: { + build: { + ssr: true, + emitAssets: true, + rollupOptions: { + input: { + index: '/entry', + }, + }, + }, + }, + }, + }, + ) + const result = await builder.build(builder.environments.ssr) + expect(result).toMatchObject({ + output: [ + { + fileName: 'index.mjs', + }, + { + fileName: expect.stringMatching(/assets\/index-\w*\.css/), + }, + ], + }) + }) }) /** diff --git a/packages/vite/src/node/__tests__/fixtures/emit-assets/css-module.module.css b/packages/vite/src/node/__tests__/fixtures/emit-assets/css-module.module.css new file mode 100644 index 00000000000000..ccb357b951b97a --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/emit-assets/css-module.module.css @@ -0,0 +1,6 @@ +.css-module { + background: rgb(200, 250, 250); + padding: 20px; + width: 200px; + border: 1px solid gray; +} diff --git a/packages/vite/src/node/__tests__/fixtures/emit-assets/css-normal.css b/packages/vite/src/node/__tests__/fixtures/emit-assets/css-normal.css new file mode 100644 index 00000000000000..b71240fe326ac3 --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/emit-assets/css-normal.css @@ -0,0 +1,6 @@ +#css-normal { + background: rgb(250, 250, 200); + padding: 20px; + width: 200px; + border: 1px solid gray; +} diff --git a/packages/vite/src/node/__tests__/fixtures/emit-assets/entry.mjs b/packages/vite/src/node/__tests__/fixtures/emit-assets/entry.mjs new file mode 100644 index 00000000000000..f62bad6569e92e --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/emit-assets/entry.mjs @@ -0,0 +1,6 @@ +import './css-normal.css' +import cssModule from './css-module.module.css' + +export default function Page() { + console.log(cssModule) +} diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index af2292c58014d5..a4d40564bfc3d0 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -901,6 +901,12 @@ export async function resolveConfig( } } + if (config.build?.ssrEmitAssets !== undefined) { + configEnvironmentsSsr ??= {} + configEnvironmentsSsr.build ??= {} + configEnvironmentsSsr.build.emitAssets = config.build.ssrEmitAssets + } + // The client and ssr environment configs can't be removed by the user in the config hook if ( !config.environments || From 29dd26e8384a44a2339cd1e775515b81104577a8 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Mon, 22 Apr 2024 11:04:16 +0200 Subject: [PATCH 012/123] refactor: build --app --- docs/guide/api-vite-environment.md | 7 ++--- packages/vite/src/node/build.ts | 27 ++++++------------- packages/vite/src/node/cli.ts | 6 ++--- .../environment-react-ssr/vite.config.ts | 6 ++--- playground/vitestSetup.ts | 2 +- 5 files changed, 19 insertions(+), 29 deletions(-) diff --git a/docs/guide/api-vite-environment.md b/docs/guide/api-vite-environment.md index 01612d1036cfc5..b108e783d839c4 100644 --- a/docs/guide/api-vite-environment.md +++ b/docs/guide/api-vite-environment.md @@ -821,13 +821,14 @@ Other non-default environments can be build using `vite build --environment=name ## Building all environments -Calling `vite build --all` will instantiate a `ViteBuilder` (build-time equivalent to a `ViteDevServer`) to build all configured environments for production. By default the build of environments is run in series respecting the order of the `environments` record. A framework or user can further configure how the environments are built using: +Calling `vite build --app` will instantiate a `ViteBuilder` (build-time equivalent to a `ViteDevServer`) to build all configured environments for production. By default the build of environments is run in series respecting the order of the `environments` record. A framework or user can further configure how the environments are built using: ```js export default { builder: { - buildEnvironments: asnyc (builder) => { - return Promise.all(Object.values(builder.environments).map( environment => builder.build(environment) )) + buildApp: asnyc (builder) => { + const environments = Object.values(builder.environments) + return Promise.all(environments.map(environment => builder.build(environment))) } } } diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 4e4f2944bd60a5..b2479f8ac90d87 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -1394,25 +1394,19 @@ export class BuildEnvironment extends Environment { export interface ViteBuilder { environments: Record config: ResolvedConfig - buildEnvironments(): Promise + buildApp(): Promise build( environment: BuildEnvironment, ): Promise } export interface BuilderOptions { - buildEnvironments?: ( - builder: ViteBuilder, - build: (environment: BuildEnvironment) => Promise, - ) => Promise + buildApp?: (builder: ViteBuilder) => Promise } -async function defaultBuildEnvironments( - builder: ViteBuilder, - build: (environment: BuildEnvironment) => Promise, -): Promise { +async function defaultBuildApp(builder: ViteBuilder): Promise { for (const environment of Object.values(builder.environments)) { - await build(environment) + await builder.build(environment) } } @@ -1420,7 +1414,7 @@ export function resolveBuilderOptions( options: BuilderOptions = {}, ): ResolvedBuilderOptions { return { - buildEnvironments: options.buildEnvironments ?? defaultBuildEnvironments, + buildApp: options.buildApp ?? defaultBuildApp, } } @@ -1463,18 +1457,13 @@ export async function createViteBuilder( const builder: ViteBuilder = { environments, config: defaultConfig, - async buildEnvironments() { + async buildApp() { if (defaultConfig.build.watch) { throw new Error( - 'Watch mode is not yet supported in viteBuilder.buildEnvironments()', + 'Watch mode is not yet supported in viteBuilder.buildApp()', ) } - return defaultConfig.builder.buildEnvironments( - builder, - async (environment) => { - await this.build(environment) - }, - ) + return defaultConfig.builder.buildApp(builder) }, async build(environment: BuildEnvironment) { return buildEnvironment(environment.config, environment) diff --git a/packages/vite/src/node/cli.ts b/packages/vite/src/node/cli.ts index 32a735a47c58e6..cb0031884dc694 100644 --- a/packages/vite/src/node/cli.ts +++ b/packages/vite/src/node/cli.ts @@ -281,7 +281,7 @@ cli ) .option('-w, --watch', `[boolean] rebuilds when modules have changed on disk`) .option('--environment [name]', `[string] build a single environment`) - .option('--all', `[boolean] build all environments`) + .option('--app', `[boolean] build all the environments`) .action( async ( root: string, @@ -316,8 +316,8 @@ cli } await builder.build(environment) } else { - // --all: build all environments - await builder.buildEnvironments() + // --app: build all environments + await builder.buildApp() } } else { await build(config) diff --git a/playground/environment-react-ssr/vite.config.ts b/playground/environment-react-ssr/vite.config.ts index af09a78f8e7f23..1c86468c25fe46 100644 --- a/playground/environment-react-ssr/vite.config.ts +++ b/playground/environment-react-ssr/vite.config.ts @@ -47,9 +47,9 @@ export default defineConfig((env) => ({ }, builder: { - async buildEnvironments(builder, build) { - await build(builder.environments.client) - await build(builder.environments.ssr) + async buildApp(builder) { + await builder.build(builder.environments.client) + await builder.build(builder.environments.ssr) }, }, })) diff --git a/playground/vitestSetup.ts b/playground/vitestSetup.ts index a1adaadd7ced21..251425b7d308ce 100644 --- a/playground/vitestSetup.ts +++ b/playground/vitestSetup.ts @@ -260,7 +260,7 @@ export async function startDefaultServe(): Promise { ) if (buildConfig.builder) { const builder = await createViteBuilder({}, { root: rootDir }) - await builder.buildEnvironments() + await builder.buildApp() } else { const rollupOutput = await build(buildConfig) const isWatch = !!resolvedConfig!.build.watch From 33ef0fb6e0d9f046183eb5fb986883be045b8511 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 22 Apr 2024 11:28:01 +0200 Subject: [PATCH 013/123] fix: disable hmr for ssrLoadModule --- packages/vite/src/node/ssr/ssrModuleLoader.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vite/src/node/ssr/ssrModuleLoader.ts b/packages/vite/src/node/ssr/ssrModuleLoader.ts index 49b4bcf14e1f82..d3471c7cef8167 100644 --- a/packages/vite/src/node/ssr/ssrModuleLoader.ts +++ b/packages/vite/src/node/ssr/ssrModuleLoader.ts @@ -33,6 +33,7 @@ export async function ssrLoadModule( server.environments.ssr, { sourcemapInterceptor: false, + hmr: false, }, )) From bf65476a31cff7e2a5a67768fff6713101631da5 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 22 Apr 2024 11:28:57 +0200 Subject: [PATCH 014/123] chore: remove the deprecation notice from ssrLoadModule for now --- packages/vite/src/node/ssr/ssrModuleLoader.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/vite/src/node/ssr/ssrModuleLoader.ts b/packages/vite/src/node/ssr/ssrModuleLoader.ts index d3471c7cef8167..663c20b1bb7fd3 100644 --- a/packages/vite/src/node/ssr/ssrModuleLoader.ts +++ b/packages/vite/src/node/ssr/ssrModuleLoader.ts @@ -18,14 +18,6 @@ export async function ssrLoadModule( _urlStack: string[] = [], fixStacktrace?: boolean, ): Promise { - 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.', - ), - ) - const runner = server._ssrCompatModuleRunner || (server._ssrCompatModuleRunner = createServerModuleRunner( From 9d6a152ddb381f0fc8631b2f66f26386446aa1e3 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 22 Apr 2024 11:29:51 +0200 Subject: [PATCH 015/123] refactor: remove server from createServerModuleRunner --- .../vite/src/node/ssr/runtime/__tests__/utils.ts | 2 +- .../vite/src/node/ssr/runtime/serverHmrConnector.ts | 12 +----------- .../vite/src/node/ssr/runtime/serverModuleRunner.ts | 12 ++++++------ packages/vite/src/node/ssr/ssrModuleLoader.ts | 1 - playground/environment-react-ssr/vite.config.ts | 2 +- playground/hmr-ssr/__tests__/hmr-ssr.spec.ts | 2 +- playground/ssr-html/test-network-imports.js | 10 +++------- playground/ssr-html/test-stacktrace-runtime.js | 2 +- 8 files changed, 14 insertions(+), 29 deletions(-) diff --git a/packages/vite/src/node/ssr/runtime/__tests__/utils.ts b/packages/vite/src/node/ssr/runtime/__tests__/utils.ts index 3a15a69fc2f13f..9d52ffa0c7cbd6 100644 --- a/packages/vite/src/node/ssr/runtime/__tests__/utils.ts +++ b/packages/vite/src/node/ssr/runtime/__tests__/utils.ts @@ -76,7 +76,7 @@ export async function createModuleRunnerTester( ...config, }) t.environment = t.server.environments.ssr - t.runner = await createServerModuleRunner(t.server, t.environment, { + t.runner = await createServerModuleRunner(t.environment, { hmr: { logger: false, }, diff --git a/packages/vite/src/node/ssr/runtime/serverHmrConnector.ts b/packages/vite/src/node/ssr/runtime/serverHmrConnector.ts index c67dfac32639d8..282da14eeca4bc 100644 --- a/packages/vite/src/node/ssr/runtime/serverHmrConnector.ts +++ b/packages/vite/src/node/ssr/runtime/serverHmrConnector.ts @@ -1,6 +1,5 @@ import type { CustomPayload, HMRPayload } from 'types/hmrPayload' import type { ModuleRunnerHMRConnection } from 'vite/module-runner' -import type { ViteDevServer } from '../../server' import type { HMRBroadcasterClient, ServerHMRChannel } from '../../server/hmr' class ServerHMRBroadcasterClient implements HMRBroadcasterClient { @@ -32,20 +31,11 @@ class ServerHMRBroadcasterClient implements HMRBroadcasterClient { */ export class ServerHMRConnector implements ModuleRunnerHMRConnection { private handlers: ((payload: HMRPayload) => void)[] = [] - private hmrChannel: ServerHMRChannel private hmrClient: ServerHMRBroadcasterClient private connected = false - constructor(server: ViteDevServer) { - const hmrChannel = server.hot?.channels.find( - (c) => c.name === 'ssr', - ) as ServerHMRChannel - if (!hmrChannel) { - throw new Error( - "Your version of Vite doesn't support HMR during SSR. Please, use Vite 5.1 or higher.", - ) - } + constructor(private hmrChannel: ServerHMRChannel) { this.hmrClient = new ServerHMRBroadcasterClient(hmrChannel) hmrChannel.api.outsideEmitter.on('send', (payload: HMRPayload) => { this.handlers.forEach((listener) => listener(payload)) diff --git a/packages/vite/src/node/ssr/runtime/serverModuleRunner.ts b/packages/vite/src/node/ssr/runtime/serverModuleRunner.ts index d4f6d42297b851..f24009ad4fd7be 100644 --- a/packages/vite/src/node/ssr/runtime/serverModuleRunner.ts +++ b/packages/vite/src/node/ssr/runtime/serverModuleRunner.ts @@ -6,8 +6,8 @@ import type { ModuleRunnerHmr, ModuleRunnerOptions, } from 'vite/module-runner' -import type { ViteDevServer } from '../../server' import type { DevEnvironment } from '../../server/environment' +import type { ServerHMRChannel } from '../../server/hmr' import { ServerHMRConnector } from './serverHmrConnector' /** @@ -34,10 +34,10 @@ export interface ServerModuleRunnerOptions } function createHMROptions( - server: ViteDevServer, + environment: DevEnvironment, options: ServerModuleRunnerOptions, ) { - if (server.config.server.hmr === false || options.hmr === false) { + if (environment.config.server.hmr === false || options.hmr === false) { return false } if (options.hmr?.connection) { @@ -46,7 +46,8 @@ function createHMROptions( logger: options.hmr.logger, } } - const connection = new ServerHMRConnector(server) + if (!('api' in environment.hot)) return false + const connection = new ServerHMRConnector(environment.hot as ServerHMRChannel) return { connection, logger: options.hmr?.logger, @@ -82,11 +83,10 @@ function resolveSourceMapOptions(options: ServerModuleRunnerOptions) { * @experimental */ export function createServerModuleRunner( - server: ViteDevServer, environment: DevEnvironment, options: ServerModuleRunnerOptions = {}, ): ModuleRunner { - const hmr = createHMROptions(server, options) + const hmr = createHMROptions(environment, options) return new ModuleRunner( { ...options, diff --git a/packages/vite/src/node/ssr/ssrModuleLoader.ts b/packages/vite/src/node/ssr/ssrModuleLoader.ts index 663c20b1bb7fd3..0d7223275fe7de 100644 --- a/packages/vite/src/node/ssr/ssrModuleLoader.ts +++ b/packages/vite/src/node/ssr/ssrModuleLoader.ts @@ -21,7 +21,6 @@ export async function ssrLoadModule( const runner = server._ssrCompatModuleRunner || (server._ssrCompatModuleRunner = createServerModuleRunner( - server, server.environments.ssr, { sourcemapInterceptor: false, diff --git a/playground/environment-react-ssr/vite.config.ts b/playground/environment-react-ssr/vite.config.ts index 1c86468c25fe46..96c193677316a3 100644 --- a/playground/environment-react-ssr/vite.config.ts +++ b/playground/environment-react-ssr/vite.config.ts @@ -66,7 +66,7 @@ export function vitePluginSsrMiddleware({ name: vitePluginSsrMiddleware.name, configureServer(server) { - const runner = createServerModuleRunner(server, server.environments.ssr) + const runner = createServerModuleRunner(server.environments.ssr) const handler: Connect.NextHandleFunction = async (req, res, next) => { try { const mod = await runner.import(entry) diff --git a/playground/hmr-ssr/__tests__/hmr-ssr.spec.ts b/playground/hmr-ssr/__tests__/hmr-ssr.spec.ts index 570ab42f507073..1f6c3385d1702b 100644 --- a/playground/hmr-ssr/__tests__/hmr-ssr.spec.ts +++ b/playground/hmr-ssr/__tests__/hmr-ssr.spec.ts @@ -1140,7 +1140,7 @@ async function setupModuleRunner( // @ts-expect-error not typed for HMR globalThis.log = (...msg) => logger.log(...msg) - runner = createServerModuleRunner(server, server.environments.ssr, { + runner = createServerModuleRunner(server.environments.ssr, { hmr: { logger, }, diff --git a/playground/ssr-html/test-network-imports.js b/playground/ssr-html/test-network-imports.js index 0d9ed8beb17663..6e6a87d93d4219 100644 --- a/playground/ssr-html/test-network-imports.js +++ b/playground/ssr-html/test-network-imports.js @@ -12,13 +12,9 @@ async function runTest(userRunner) { }) let mod if (userRunner) { - const runner = await createServerModuleRunner( - server, - server.environments.ssr, - { - hmr: false, - }, - ) + const runner = await createServerModuleRunner(server.environments.ssr, { + hmr: false, + }) mod = await runner.import('/src/network-imports.js') } else { mod = await server.ssrLoadModule('/src/network-imports.js') diff --git a/playground/ssr-html/test-stacktrace-runtime.js b/playground/ssr-html/test-stacktrace-runtime.js index ebcbc0513f0fb2..0f4914dcbfe599 100644 --- a/playground/ssr-html/test-stacktrace-runtime.js +++ b/playground/ssr-html/test-stacktrace-runtime.js @@ -13,7 +13,7 @@ const server = await createServer({ }, }) -const runner = await createServerModuleRunner(server, server.environments.ssr, { +const runner = await createServerModuleRunner(server.environments.ssr, { sourcemapInterceptor: 'prepareStackTrace', }) From 7099e12027a31e89eec2201ae0fe5f115b563187 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Wed, 24 Apr 2024 21:50:47 +0200 Subject: [PATCH 016/123] docs: create plugin hook --- docs/guide/api-vite-environment.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/guide/api-vite-environment.md b/docs/guide/api-vite-environment.md index b108e783d839c4..4e2815807e6a8d 100644 --- a/docs/guide/api-vite-environment.md +++ b/docs/guide/api-vite-environment.md @@ -579,6 +579,28 @@ The hook can choose to: } ``` +### Per-environment Plugins + +There is a new `create` hook that plugins can define to lazily create per-environment plugins. + +```js +function perEnvironmentPlugin() { + return { + name: 'per-environment-plugin', + // Return a plugin, an array, a Promise, or a falsy value for each environment + create(environment: Environment) { + if (!passesCondition(environment)) { + return undefined + } + return [ + createEnvironmentPlugin(environment), + otherPlugin(environment) + ] + } + } +} +``` + ## `ModuleRunner` A module runner is instantiated in the target runtime. All APIs in the next section are imported from `vite/module-runner` unless stated otherwise. This export entry point is kept as lightweight as possible, only exporting the minimal needed to create runners in the From 4cf322d16ac1a5cd13ab836f5ca6f4af44cb3c42 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Fri, 26 Apr 2024 06:49:13 +0200 Subject: [PATCH 017/123] fix(cli): -> --- packages/vite/src/node/cli.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/vite/src/node/cli.ts b/packages/vite/src/node/cli.ts index cb0031884dc694..7859925ec4775c 100644 --- a/packages/vite/src/node/cli.ts +++ b/packages/vite/src/node/cli.ts @@ -32,7 +32,7 @@ interface GlobalCLIOptions { interface BuilderCLIOptions { environment?: string - all?: boolean + app?: boolean } let profileSession = global.__vite_profile_session @@ -114,7 +114,7 @@ function cleanBuilderCLIOptions( ): Omit { const ret = { ...options } delete ret.environment - delete ret.all + delete ret.app return ret } @@ -305,7 +305,7 @@ cli } try { - if (options.all || options.environment) { + if (options.app || options.environment) { const builder = await createViteBuilder({}, config) if (options.environment) { const environment = builder.environments[options.environment] From 4dc2a757afd75d39d7d5579c57f6057e11f6d9f9 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Fri, 26 Apr 2024 06:52:22 +0200 Subject: [PATCH 018/123] refactor: rename createViteBuilder to createBuilder, align with createServer --- packages/vite/src/node/__tests__/build.spec.ts | 4 ++-- packages/vite/src/node/build.ts | 4 ++-- packages/vite/src/node/cli.ts | 4 ++-- packages/vite/src/node/index.ts | 2 +- playground/vitestSetup.ts | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/vite/src/node/__tests__/build.spec.ts b/packages/vite/src/node/__tests__/build.spec.ts index 10e8c783565378..83dc6dcae361b7 100644 --- a/packages/vite/src/node/__tests__/build.spec.ts +++ b/packages/vite/src/node/__tests__/build.spec.ts @@ -6,7 +6,7 @@ import type { OutputChunk, OutputOptions, RollupOutput } from 'rollup' import type { LibraryFormats, LibraryOptions } from '../build' import { build, - createViteBuilder, + createBuilder, resolveBuildOutputs, resolveLibFilename, } from '../build' @@ -609,7 +609,7 @@ describe('resolveBuildOutputs', () => { }) test('emitAssets', async () => { - const builder = await createViteBuilder( + const builder = await createBuilder( {}, { root: resolve(__dirname, 'fixtures/emit-assets'), diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index b2479f8ac90d87..fb3b260d4dcb1d 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -514,7 +514,7 @@ export async function resolveBuildPlugins(config: ResolvedConfig): Promise<{ export async function build( inlineConfig: InlineConfig = {}, ): Promise { - const builder = await createViteBuilder( + const builder = await createBuilder( {}, { ...inlineConfig, plugins: () => inlineConfig.plugins ?? [] }, ) @@ -1424,7 +1424,7 @@ export interface BuilderInlineConfig extends Omit { plugins?: () => PluginOption[] } -export async function createViteBuilder( +export async function createBuilder( builderOptions: BuilderOptions = {}, defaultBuilderInlineConfig: BuilderInlineConfig = {}, ): Promise { diff --git a/packages/vite/src/node/cli.ts b/packages/vite/src/node/cli.ts index 7859925ec4775c..712502c0655e30 100644 --- a/packages/vite/src/node/cli.ts +++ b/packages/vite/src/node/cli.ts @@ -288,7 +288,7 @@ cli options: BuildEnvironmentOptions & BuilderCLIOptions & GlobalCLIOptions, ) => { filterDuplicateOptions(options) - const { build, createViteBuilder } = await import('./build') + const { build, createBuilder } = await import('./build') const buildOptions: BuildEnvironmentOptions = cleanGlobalCLIOptions( cleanBuilderCLIOptions(options), @@ -306,7 +306,7 @@ cli try { if (options.app || options.environment) { - const builder = await createViteBuilder({}, config) + const builder = await createBuilder({}, config) if (options.environment) { const environment = builder.environments[options.environment] if (!environment) { diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index fb6482ce7be62b..7e5fc8c1b5647d 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -10,7 +10,7 @@ export { } from './config' export { createServer } from './server' export { preview } from './preview' -export { build, createViteBuilder } from './build' +export { build, createBuilder } from './build' // TODO: Can we remove this? // export { optimizeDeps } from './optimizer' diff --git a/playground/vitestSetup.ts b/playground/vitestSetup.ts index 251425b7d308ce..673845a4de2794 100644 --- a/playground/vitestSetup.ts +++ b/playground/vitestSetup.ts @@ -14,7 +14,7 @@ import type { import { build, createServer, - createViteBuilder, + createBuilder, loadConfigFromFile, mergeConfig, preview, @@ -259,7 +259,7 @@ export async function startDefaultServe(): Promise { }, ) if (buildConfig.builder) { - const builder = await createViteBuilder({}, { root: rootDir }) + const builder = await createBuilder({}, { root: rootDir }) await builder.buildApp() } else { const rollupOutput = await build(buildConfig) From 41a95ae79189a18aa2a176701f4689b837656a15 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Fri, 26 Apr 2024 06:57:08 +0200 Subject: [PATCH 019/123] chore: lint --- playground/vitestSetup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/vitestSetup.ts b/playground/vitestSetup.ts index 673845a4de2794..e77a777d706de2 100644 --- a/playground/vitestSetup.ts +++ b/playground/vitestSetup.ts @@ -13,8 +13,8 @@ import type { } from 'vite' import { build, - createServer, createBuilder, + createServer, loadConfigFromFile, mergeConfig, preview, From 8843221c22753b4c284eeaa87afea2cd383855f3 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Fri, 26 Apr 2024 10:13:06 +0200 Subject: [PATCH 020/123] feat: use environment.logger in buildEnvironment --- packages/vite/src/node/build.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index fb3b260d4dcb1d..a1e3db9c17885a 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -548,9 +548,10 @@ export async function buildEnvironment( libOptions: LibraryOptions | false = false, ): Promise { const options = config.build + const { logger } = environment const ssr = environment.name !== 'client' - config.logger.info( + logger.info( colors.cyan( `vite v${VERSION} ${colors.green( `building ${ssr ? `SSR bundle ` : ``}for ${config.mode}...`, @@ -636,7 +637,7 @@ export async function buildEnvironment( const outputBuildError = (e: RollupError) => { const msg = mergeRollupError(e) clearLine() - config.logger.error(msg, { error: e }) + logger.error(msg, { error: e }) } let bundle: RollupBuild | undefined @@ -645,7 +646,7 @@ export async function buildEnvironment( const buildOutputOptions = (output: OutputOptions = {}): OutputOptions => { // @ts-expect-error See https://github.com/vitejs/vite/issues/5812#issuecomment-984345618 if (output.output) { - config.logger.warn( + logger.warn( `You've set "rollupOptions.output.output" in your config. ` + `This is deprecated and will override all Vite.js default output options. ` + `Please use "rollupOptions.output" instead.`, @@ -658,7 +659,7 @@ export async function buildEnvironment( ) } if (output.sourcemap) { - config.logger.warnOnce( + logger.warnOnce( colors.yellow( `Vite does not support "rollupOptions.output.sourcemap". ` + `Please use "build.sourcemap" instead.`, @@ -723,7 +724,7 @@ export async function buildEnvironment( const outputs = resolveBuildOutputs( options.rollupOptions?.output, libOptions, - config.logger, + logger, ) const normalizedOutputs: OutputOptions[] = [] @@ -744,12 +745,12 @@ export async function buildEnvironment( options.emptyOutDir, config.root, resolvedOutDirs, - config.logger, + logger, ) // watch file changes with rollup if (config.build.watch) { - config.logger.info(colors.cyan(`\nwatching for file changes...`)) + logger.info(colors.cyan(`\nwatching for file changes...`)) const resolvedChokidarOptions = resolveChokidarOptions( config, @@ -770,13 +771,13 @@ export async function buildEnvironment( watcher.on('event', (event) => { if (event.code === 'BUNDLE_START') { - config.logger.info(colors.cyan(`\nbuild started...`)) + logger.info(colors.cyan(`\nbuild started...`)) if (options.write) { prepareOutDir(resolvedOutDirs, emptyOutDir, config) } } else if (event.code === 'BUNDLE_END') { event.result.close() - config.logger.info(colors.cyan(`built in ${event.duration}ms.`)) + logger.info(colors.cyan(`built in ${event.duration}ms.`)) } else if (event.code === 'ERROR') { outputBuildError(event.error) } @@ -798,7 +799,7 @@ export async function buildEnvironment( for (const output of normalizedOutputs) { res.push(await bundle[options.write ? 'write' : 'generate'](output)) } - config.logger.info( + logger.info( `${colors.green(`✓ built in ${displayTime(Date.now() - startTime)}`)}`, ) return Array.isArray(outputs) ? res : res[0] @@ -806,7 +807,7 @@ export async function buildEnvironment( e.message = mergeRollupError(e) clearLine() if (startTime) { - config.logger.error( + logger.error( `${colors.red('x')} Build failed in ${displayTime(Date.now() - startTime)}`, ) startTime = undefined From 62921e6bafd5a866b79d5cd23071d550f491f9cc Mon Sep 17 00:00:00 2001 From: patak-dev Date: Fri, 26 Apr 2024 11:00:05 +0200 Subject: [PATCH 021/123] refactor: align createBuilder params with createServer --- .../vite/src/node/__tests__/build.spec.ts | 25 +++++++--------- packages/vite/src/node/build.ts | 29 ++++--------------- packages/vite/src/node/cli.ts | 2 +- playground/vitestSetup.ts | 2 +- 4 files changed, 19 insertions(+), 39 deletions(-) diff --git a/packages/vite/src/node/__tests__/build.spec.ts b/packages/vite/src/node/__tests__/build.spec.ts index 83dc6dcae361b7..72082550bcebef 100644 --- a/packages/vite/src/node/__tests__/build.spec.ts +++ b/packages/vite/src/node/__tests__/build.spec.ts @@ -609,25 +609,22 @@ describe('resolveBuildOutputs', () => { }) test('emitAssets', async () => { - const builder = await createBuilder( - {}, - { - root: resolve(__dirname, 'fixtures/emit-assets'), - environments: { - ssr: { - build: { - ssr: true, - emitAssets: true, - rollupOptions: { - input: { - index: '/entry', - }, + const builder = await createBuilder({ + root: resolve(__dirname, 'fixtures/emit-assets'), + environments: { + ssr: { + build: { + ssr: true, + emitAssets: true, + rollupOptions: { + input: { + index: '/entry', }, }, }, }, }, - ) + }) const result = await builder.build(builder.environments.ssr) expect(result).toMatchObject({ output: [ diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index a1e3db9c17885a..120a76e793e9ce 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -34,7 +34,6 @@ import type { ResolvedConfig, ResolvedEnvironmentOptions, } from './config' -import type { PluginOption } from './plugin' import { getDefaultResolvedEnvironmentOptions, resolveConfig } from './config' import { buildReporterPlugin } from './plugins/reporter' import { buildEsbuildPlugin } from './plugins/esbuild' @@ -514,10 +513,7 @@ export async function resolveBuildPlugins(config: ResolvedConfig): Promise<{ export async function build( inlineConfig: InlineConfig = {}, ): Promise { - const builder = await createBuilder( - {}, - { ...inlineConfig, plugins: () => inlineConfig.plugins ?? [] }, - ) + const builder = await createBuilder(inlineConfig) if (builder.config.build.lib) { // TODO: temporal workaround. Should we support `libraries: Record` @@ -1421,34 +1417,21 @@ export function resolveBuilderOptions( export type ResolvedBuilderOptions = Required -export interface BuilderInlineConfig extends Omit { - plugins?: () => PluginOption[] -} - export async function createBuilder( - builderOptions: BuilderOptions = {}, - defaultBuilderInlineConfig: BuilderInlineConfig = {}, + inlineConfig: InlineConfig = {}, ): 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) - } + const environmentInlineConfig = environmentOptions + ? mergeConfig(inlineConfig, environmentOptions) + : inlineConfig // 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) + return resolveConfigToBuild(environmentInlineConfig) } const defaultConfig = await resolveConfig() diff --git a/packages/vite/src/node/cli.ts b/packages/vite/src/node/cli.ts index 712502c0655e30..e1dd58e5780210 100644 --- a/packages/vite/src/node/cli.ts +++ b/packages/vite/src/node/cli.ts @@ -306,7 +306,7 @@ cli try { if (options.app || options.environment) { - const builder = await createBuilder({}, config) + const builder = await createBuilder(config) if (options.environment) { const environment = builder.environments[options.environment] if (!environment) { diff --git a/playground/vitestSetup.ts b/playground/vitestSetup.ts index e77a777d706de2..50b23b0c8a5207 100644 --- a/playground/vitestSetup.ts +++ b/playground/vitestSetup.ts @@ -259,7 +259,7 @@ export async function startDefaultServe(): Promise { }, ) if (buildConfig.builder) { - const builder = await createBuilder({}, { root: rootDir }) + const builder = await createBuilder({ root: rootDir }) await builder.buildApp() } else { const rollupOutput = await build(buildConfig) From 294a84e0b9f5f95ba4baa51a2974c8323f7c3616 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Fri, 26 Apr 2024 11:06:57 +0200 Subject: [PATCH 022/123] fix: backcompat merging --- packages/vite/src/node/config.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index a4d40564bfc3d0..fa21963325f29f 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -860,11 +860,11 @@ export async function resolveConfig( // TODO: should entries and force be in EnvironmentOptions? const { entries, force, ...deprecatedClientOptimizeDepsConfig } = config.optimizeDeps ?? {} - let configEnvironmentsClient = config.environments!.client! + const configEnvironmentsClient = config.environments!.client! configEnvironmentsClient.dev ??= {} configEnvironmentsClient.dev.optimizeDeps = mergeConfig( - configEnvironmentsClient.dev.optimizeDeps ?? {}, deprecatedClientOptimizeDepsConfig, + configEnvironmentsClient.dev.optimizeDeps ?? {}, ) const deprecatedSsrOptimizeDepsConfig = config.ssr?.optimizeDeps ?? {} @@ -885,8 +885,8 @@ export async function resolveConfig( if (configEnvironmentsSsr) { configEnvironmentsSsr.dev ??= {} configEnvironmentsSsr.dev.optimizeDeps = mergeConfig( - configEnvironmentsSsr.dev.optimizeDeps ?? {}, deprecatedSsrOptimizeDepsConfig, + configEnvironmentsSsr.dev.optimizeDeps ?? {}, ) // TODO: should we merge here? configEnvironmentsSsr.resolve ??= {} @@ -946,10 +946,10 @@ export async function resolveConfig( ) // Backward compatibility: merge environments.client.dev.optimizeDeps back into optimizeDeps - configEnvironmentsClient = resolvedEnvironments.client + const resolvedConfigEnvironmentsClient = resolvedEnvironments.client const patchedOptimizeDeps = mergeConfig( - configEnvironmentsClient.dev?.optimizeDeps ?? {}, config.optimizeDeps ?? {}, + resolvedConfigEnvironmentsClient.dev?.optimizeDeps ?? {}, ) const backwardCompatibleOptimizeDeps = { holdUntilCrawlEnd: true, From 2866d4f2f6cbd184c9d94bc0cdda3c79e790b1aa Mon Sep 17 00:00:00 2001 From: patak-dev Date: Fri, 26 Apr 2024 12:09:35 +0200 Subject: [PATCH 023/123] feat: opt-in shared plugins during build --- packages/vite/src/node/build.ts | 73 +++++++++++++++++++++++--------- packages/vite/src/node/plugin.ts | 9 ++++ 2 files changed, 61 insertions(+), 21 deletions(-) diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 120a76e793e9ce..14a12b376579d8 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -1398,6 +1398,8 @@ export interface ViteBuilder { } export interface BuilderOptions { + sharedConfigBuild?: boolean + sharedPlugins?: boolean buildApp?: (builder: ViteBuilder) => Promise } @@ -1411,6 +1413,8 @@ export function resolveBuilderOptions( options: BuilderOptions = {}, ): ResolvedBuilderOptions { return { + sharedConfigBuild: options.sharedConfigBuild ?? false, + sharedPlugins: options.sharedPlugins ?? false, buildApp: options.buildApp ?? defaultBuildApp, } } @@ -1420,42 +1424,32 @@ export type ResolvedBuilderOptions = Required export async function createBuilder( inlineConfig: InlineConfig = {}, ): 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 environmentInlineConfig = environmentOptions - ? mergeConfig(inlineConfig, environmentOptions) - : inlineConfig - - // We resolve the whole config including plugins here but later on we - // need to refactor resolveConfig to only resolve the environments config - return resolveConfigToBuild(environmentInlineConfig) - } - - const defaultConfig = await resolveConfig() + const config = await resolveConfigToBuild(inlineConfig) const environments: Record = {} const builder: ViteBuilder = { environments, - config: defaultConfig, + config, async buildApp() { - if (defaultConfig.build.watch) { + if (config.build.watch) { throw new Error( 'Watch mode is not yet supported in viteBuilder.buildApp()', ) } - return defaultConfig.builder.buildApp(builder) + return config.builder.buildApp(builder) }, async build(environment: BuildEnvironment) { return buildEnvironment(environment.config, environment) }, } - for (const name of Object.keys(defaultConfig.environments)) { - const environmentOptions = defaultConfig.environments[name] + const allSharedPlugins = + config.builder.sharedPlugins || + config.plugins.every((plugin) => plugin.sharedDuringBuild === true) + + for (const name of Object.keys(config.environments)) { + const environmentOptions = config.environments[name] const createEnvironment = environmentOptions.build?.createEnvironment ?? ((name: string, config: ResolvedConfig) => @@ -1466,7 +1460,44 @@ export async function createBuilder( // 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 environmentConfig = + allSharedPlugins && config.builder.sharedConfigBuild + ? config + : await resolveConfigToBuild( + mergeConfig(inlineConfig, environmentOptions), + ) + if (environmentConfig !== config) { + // Force opt-in shared plugins + const plugins = [...environmentConfig.plugins] + let validPlugins = true + for (let i = 0; i < plugins.length; i++) { + const environmentPlugin = plugins[i] + const sharedPlugin = config.plugins[i] + if (environmentPlugin.sharedDuringBuild) { + if (environmentPlugin.name !== sharedPlugin.name) { + validPlugins = false + break + } + plugins[i] = sharedPlugin + } + } + if (validPlugins) { + ;(environmentConfig.plugins as Plugin[]) = plugins + } + } + /* + // TODO: This is implementing by merging environmentOptions into inlineConfig before resolving + // We should be instead doing the patching as the commented code below but we need to understand + // why some tests are failing first. + // + // Until the ecosystem updates to use `environment.options.build` instead of `config.build`, + // we need to make override `config.build` for the current environment. + // We can deprecate `config.build` in ResolvedConfig and push everyone to upgrade, and later + // remove the default values that shouldn't be used at all once the config is resolved + if (!config.builder.sharedConfigBuild) { + ;(environmentConfig.build as ResolvedBuildOptions) = { ...environmentConfig.environments[name].build, lib: false } + } + */ const environment = await createEnvironment(name, environmentConfig) environments[name] = environment diff --git a/packages/vite/src/node/plugin.ts b/packages/vite/src/node/plugin.ts index 872a0cf99b4d3b..b0960c44d90813 100644 --- a/packages/vite/src/node/plugin.ts +++ b/packages/vite/src/node/plugin.ts @@ -165,6 +165,15 @@ export interface BasePlugin extends RollupPlugin { export type BoundedPlugin = BasePlugin export interface Plugin extends BasePlugin { + /** + * Opt-in this plugin into the shared plugins pipeline. + * For backward-compatibility, plugins are re-recreated for each environment + * during `vite build --app` + * We have an opt-in per plugin, and a general `builder.sharedPlugins` + * In a future major, we'll flip the default to be shared by default + * @experimental + */ + sharedDuringBuild?: boolean /** * Spawn the plugin into multiple plugins based on the environment. * This hook is called when the config has already been resolved, allowing to From a63190c45165ad8860c916f3fa06f77eeed05ff0 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Fri, 26 Apr 2024 12:27:39 +0200 Subject: [PATCH 024/123] fix: sharedPlugins --- packages/vite/src/node/build.ts | 46 ++++++++++++++++----------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 14a12b376579d8..010539bc83e9f6 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -1444,10 +1444,6 @@ export async function createBuilder( }, } - const allSharedPlugins = - config.builder.sharedPlugins || - config.plugins.every((plugin) => plugin.sharedDuringBuild === true) - for (const name of Object.keys(config.environments)) { const environmentOptions = config.environments[name] const createEnvironment = @@ -1460,12 +1456,11 @@ export async function createBuilder( // 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 = - allSharedPlugins && config.builder.sharedConfigBuild - ? config - : await resolveConfigToBuild( - mergeConfig(inlineConfig, environmentOptions), - ) + const environmentConfig = config.builder.sharedConfigBuild + ? config + : await resolveConfigToBuild( + mergeConfig(inlineConfig, environmentOptions), + ) if (environmentConfig !== config) { // Force opt-in shared plugins const plugins = [...environmentConfig.plugins] @@ -1473,7 +1468,10 @@ export async function createBuilder( for (let i = 0; i < plugins.length; i++) { const environmentPlugin = plugins[i] const sharedPlugin = config.plugins[i] - if (environmentPlugin.sharedDuringBuild) { + if ( + config.builder.sharedPlugins || + environmentPlugin.sharedDuringBuild + ) { if (environmentPlugin.name !== sharedPlugin.name) { validPlugins = false break @@ -1484,20 +1482,20 @@ export async function createBuilder( if (validPlugins) { ;(environmentConfig.plugins as Plugin[]) = plugins } + /* + // TODO: This is implementing by merging environmentOptions into inlineConfig before resolving + // We should be instead doing the patching as the commented code below but we need to understand + // why some tests are failing first. + // + // Until the ecosystem updates to use `environment.options.build` instead of `config.build`, + // we need to make override `config.build` for the current environment. + // We can deprecate `config.build` in ResolvedConfig and push everyone to upgrade, and later + // remove the default values that shouldn't be used at all once the config is resolved + if (!config.builder.sharedConfigBuild) { + ;(environmentConfig.build as ResolvedBuildOptions) = { ...environmentConfig.environments[name].build, lib: false } + } + */ } - /* - // TODO: This is implementing by merging environmentOptions into inlineConfig before resolving - // We should be instead doing the patching as the commented code below but we need to understand - // why some tests are failing first. - // - // Until the ecosystem updates to use `environment.options.build` instead of `config.build`, - // we need to make override `config.build` for the current environment. - // We can deprecate `config.build` in ResolvedConfig and push everyone to upgrade, and later - // remove the default values that shouldn't be used at all once the config is resolved - if (!config.builder.sharedConfigBuild) { - ;(environmentConfig.build as ResolvedBuildOptions) = { ...environmentConfig.environments[name].build, lib: false } - } - */ const environment = await createEnvironment(name, environmentConfig) environments[name] = environment From 1925eeb430323f13568c0ee9492a80d749b7480e Mon Sep 17 00:00:00 2001 From: patak-dev Date: Fri, 26 Apr 2024 16:22:04 +0200 Subject: [PATCH 025/123] feat: builder.entireApp, remove --environment --- packages/vite/src/node/build.ts | 2 ++ packages/vite/src/node/cli.ts | 36 +++++++++++++++------------------ 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 010539bc83e9f6..904efbcc8ed5e1 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -1400,6 +1400,7 @@ export interface ViteBuilder { export interface BuilderOptions { sharedConfigBuild?: boolean sharedPlugins?: boolean + entireApp?: boolean buildApp?: (builder: ViteBuilder) => Promise } @@ -1415,6 +1416,7 @@ export function resolveBuilderOptions( return { sharedConfigBuild: options.sharedConfigBuild ?? false, sharedPlugins: options.sharedPlugins ?? false, + entireApp: options.entireApp ?? false, buildApp: options.buildApp ?? defaultBuildApp, } } diff --git a/packages/vite/src/node/cli.ts b/packages/vite/src/node/cli.ts index e1dd58e5780210..fa93b125113f3e 100644 --- a/packages/vite/src/node/cli.ts +++ b/packages/vite/src/node/cli.ts @@ -31,7 +31,6 @@ interface GlobalCLIOptions { } interface BuilderCLIOptions { - environment?: string app?: boolean } @@ -113,9 +112,7 @@ function cleanBuilderCLIOptions( options: Options, ): Omit { const ret = { ...options } - delete ret.environment delete ret.app - return ret } @@ -281,14 +278,14 @@ cli ) .option('-w, --watch', `[boolean] rebuilds when modules have changed on disk`) .option('--environment [name]', `[string] build a single environment`) - .option('--app', `[boolean] build all the environments`) + .option('--app', `[boolean] same as builder.entireApp`) .action( async ( root: string, options: BuildEnvironmentOptions & BuilderCLIOptions & GlobalCLIOptions, ) => { filterDuplicateOptions(options) - const { build, createBuilder } = await import('./build') + const { createBuilder, buildEnvironment } = await import('./build') const buildOptions: BuildEnvironmentOptions = cleanGlobalCLIOptions( cleanBuilderCLIOptions(options), @@ -305,22 +302,21 @@ cli } try { - if (options.app || options.environment) { - const builder = await createBuilder(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 { - // --app: build all environments - await builder.buildApp() - } + const builder = await createBuilder(config) + // TODO: Backward compatibility with lib and single environment build + // Ideally we would move to only building the entire app with this command + if (builder.config.build.lib) { + await buildEnvironment( + builder.config, + builder.environments.client, + builder.config.build.lib, + ) + } else if (builder.config.builder.entireApp || options.app) { + await builder.buildApp() } else { - await build(config) + const ssr = !!builder.config.build.ssr + const environment = builder.environments[ssr ? 'ssr' : 'client'] + await builder.build(environment) } } catch (e) { createLogger(options.logLevel).error( From 72150d6f750ece20f936f38922d61bd3a863f3bf Mon Sep 17 00:00:00 2001 From: patak-dev Date: Fri, 26 Apr 2024 17:38:29 +0200 Subject: [PATCH 026/123] fix: rework backcompat patching of environment config --- packages/vite/src/node/build.ts | 80 +++++++++++++++++--------------- packages/vite/src/node/config.ts | 5 ++ 2 files changed, 47 insertions(+), 38 deletions(-) diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 904efbcc8ed5e1..d77f4b16b16d95 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -531,8 +531,18 @@ export async function build( } } -function resolveConfigToBuild(inlineConfig: InlineConfig = {}) { - return resolveConfig(inlineConfig, 'build', 'production', 'production') +function resolveConfigToBuild( + inlineConfig: InlineConfig = {}, + patchConfig?: (config: ResolvedConfig) => void, +) { + return resolveConfig( + inlineConfig, + 'build', + 'production', + 'production', + false, + patchConfig, + ) } /** @@ -1458,45 +1468,39 @@ export async function createBuilder( // 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 = config.builder.sharedConfigBuild - ? config - : await resolveConfigToBuild( - mergeConfig(inlineConfig, environmentOptions), - ) - if (environmentConfig !== config) { - // Force opt-in shared plugins - const plugins = [...environmentConfig.plugins] - let validPlugins = true - for (let i = 0; i < plugins.length; i++) { - const environmentPlugin = plugins[i] - const sharedPlugin = config.plugins[i] - if ( - config.builder.sharedPlugins || - environmentPlugin.sharedDuringBuild - ) { - if (environmentPlugin.name !== sharedPlugin.name) { - validPlugins = false - break + let environmentConfig = config + if (!config.builder.sharedConfigBuild) { + const patchConfig = (resolved: ResolvedConfig) => { + // Force opt-in shared plugins + const environmentPlugins = [...resolved.plugins] + let validMixedPlugins = true + for (let i = 0; i < environmentPlugins.length; i++) { + const environmentPlugin = environmentPlugins[i] + const sharedPlugin = config.plugins[i] + if ( + config.builder.sharedPlugins || + environmentPlugin.sharedDuringBuild + ) { + if (environmentPlugin.name !== sharedPlugin.name) { + validMixedPlugins = false + break + } + environmentPlugins[i] = sharedPlugin } - plugins[i] = sharedPlugin + } + if (validMixedPlugins) { + ;(resolved.plugins as Plugin[]) = environmentPlugins + } + // Until the ecosystem updates to use `environment.options.build` instead of `config.build`, + // we need to make override `config.build` for the current environment. + // We can deprecate `config.build` in ResolvedConfig and push everyone to upgrade, and later + // remove the default values that shouldn't be used at all once the config is resolved + ;(resolved.build as ResolvedBuildOptions) = { + ...resolved.environments[name].build, + lib: false, } } - if (validPlugins) { - ;(environmentConfig.plugins as Plugin[]) = plugins - } - /* - // TODO: This is implementing by merging environmentOptions into inlineConfig before resolving - // We should be instead doing the patching as the commented code below but we need to understand - // why some tests are failing first. - // - // Until the ecosystem updates to use `environment.options.build` instead of `config.build`, - // we need to make override `config.build` for the current environment. - // We can deprecate `config.build` in ResolvedConfig and push everyone to upgrade, and later - // remove the default values that shouldn't be used at all once the config is resolved - if (!config.builder.sharedConfigBuild) { - ;(environmentConfig.build as ResolvedBuildOptions) = { ...environmentConfig.environments[name].build, lib: false } - } - */ + environmentConfig = await resolveConfigToBuild(inlineConfig, patchConfig) } const environment = await createEnvironment(name, environmentConfig) diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index fa21963325f29f..aa3ff8eec4d1da 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -758,6 +758,7 @@ export async function resolveConfig( defaultMode = 'development', defaultNodeEnv = 'development', isPreview = false, + patchConfig: ((config: ResolvedConfig) => void) | undefined = undefined, ): Promise { let config = inlineConfig let configFileDependencies: string[] = [] @@ -1289,6 +1290,10 @@ export async function resolveConfig( normalPlugins, postPlugins, ) + + // Backward compatibility hook used in builder + patchConfig?.(resolved) + Object.assign(resolved, createPluginHookUtils(resolved.plugins)) // call configResolved hooks From e6fa9d9e2f238cbc959a1a70e326c45b86ceea92 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Fri, 26 Apr 2024 17:41:43 +0200 Subject: [PATCH 027/123] release: v6.0.0-alpha.4 --- packages/vite/CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ packages/vite/package.json | 2 +- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/packages/vite/CHANGELOG.md b/packages/vite/CHANGELOG.md index 4e8282f8a24ad5..e9d950e0d9a792 100644 --- a/packages/vite/CHANGELOG.md +++ b/packages/vite/CHANGELOG.md @@ -1,3 +1,35 @@ +## 6.0.0-alpha.4 (2024-04-26) + +* fix: backcompat merging ([294a84e](https://github.com/vitejs/vite/commit/294a84e)) +* fix: disable hmr for ssrLoadModule ([33ef0fb](https://github.com/vitejs/vite/commit/33ef0fb)) +* fix: dynamic-import-vars plugin normalize path issue (#16518) ([f71ba5b](https://github.com/vitejs/vite/commit/f71ba5b)), closes [#16518](https://github.com/vitejs/vite/issues/16518) +* fix: rework backcompat patching of environment config ([72150d6](https://github.com/vitejs/vite/commit/72150d6)) +* fix: sharedPlugins ([a63190c](https://github.com/vitejs/vite/commit/a63190c)) +* fix(cli): -> ([4cf322d](https://github.com/vitejs/vite/commit/4cf322d)) +* fix(deps): update all non-major dependencies (#16488) ([2d50be2](https://github.com/vitejs/vite/commit/2d50be2)), closes [#16488](https://github.com/vitejs/vite/issues/16488) +* fix(dev): watch publicDir explicitly to include it outside the root (#16502) ([4d83eb5](https://github.com/vitejs/vite/commit/4d83eb5)), closes [#16502](https://github.com/vitejs/vite/issues/16502) +* fix(v6): fix `ssrEmitAssets` compat (#16480) ([5c5efe4](https://github.com/vitejs/vite/commit/5c5efe4)), closes [#16480](https://github.com/vitejs/vite/issues/16480) +* feat: builder.entireApp, remove --environment ([1925eeb](https://github.com/vitejs/vite/commit/1925eeb)) +* feat: opt-in shared plugins during build ([2866d4f](https://github.com/vitejs/vite/commit/2866d4f)) +* feat: use environment.logger in buildEnvironment ([8843221](https://github.com/vitejs/vite/commit/8843221)) +* refactor: align createBuilder params with createServer ([62921e6](https://github.com/vitejs/vite/commit/62921e6)) +* refactor: build --app ([29dd26e](https://github.com/vitejs/vite/commit/29dd26e)) +* refactor: remove server from createServerModuleRunner ([9d6a152](https://github.com/vitejs/vite/commit/9d6a152)) +* refactor: rename createViteBuilder to createBuilder, align with createServer ([4dc2a75](https://github.com/vitejs/vite/commit/4dc2a75)) +* chore: remove the deprecation notice from ssrLoadModule for now ([bf65476](https://github.com/vitejs/vite/commit/bf65476)) + + + +## 6.0.0-alpha.3 (2024-04-20) + +* release: v6.0.0-alpha.3 ([635aad5](https://github.com/vitejs/vite/commit/635aad5)) +* fix: export missing types ([431cd4b](https://github.com/vitejs/vite/commit/431cd4b)) +* chore: remove configureServer from more plugins ([ad2b0bf](https://github.com/vitejs/vite/commit/ad2b0bf)) +* chore: rename plugin.split to plugin.create ([5f6b62f](https://github.com/vitejs/vite/commit/5f6b62f)) +* feat: environment api (#16129) ([f684d4c](https://github.com/vitejs/vite/commit/f684d4c)), closes [#16129](https://github.com/vitejs/vite/issues/16129) + + + ## 6.0.0-alpha.3 (2024-04-20) * fix: add base to virtual html (#16442) ([721f94d](https://github.com/vitejs/vite/commit/721f94d)), closes [#16442](https://github.com/vitejs/vite/issues/16442) diff --git a/packages/vite/package.json b/packages/vite/package.json index 04cd84739194b6..a38495fa7474f6 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -1,6 +1,6 @@ { "name": "vite", - "version": "6.0.0-alpha.3", + "version": "6.0.0-alpha.4", "type": "module", "license": "MIT", "author": "Evan You", From a44810a7d08e59ff29db4d57fb7985bf600c67c1 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Fri, 26 Apr 2024 22:36:49 +0200 Subject: [PATCH 028/123] fix: use environment.plugins during build, support create hook --- packages/vite/src/node/build.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index d77f4b16b16d95..32d13a1b490a26 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -49,6 +49,7 @@ import { partialEncodeURIPath, requireResolveFromRootWithFallback, } from './utils' +import { resolveBoundedPlugins } from './plugin' import { manifestPlugin } from './plugins/manifest' import type { Logger } from './logger' import { dataURIPlugin } from './plugins/dataUri' @@ -605,11 +606,10 @@ export async function buildEnvironment( const outDir = resolve(options.outDir) // inject environment and ssr arg to plugin load/transform hooks - const plugins = ( - environment || ssr - ? config.plugins.map((p) => injectEnvironmentToHooks(p, environment)) - : config.plugins - ) as Plugin[] + // TODO: rework lib mode + const plugins = (libOptions ? config : environment).plugins.map((p) => + injectEnvironmentToHooks(p, environment), + ) const rollupOptions: RollupOptions = { preserveEntrySignatures: ssr @@ -1396,6 +1396,14 @@ export class BuildEnvironment extends Environment { } super(name, config, options) } + + async init(): Promise { + if (this._inited) { + return + } + this._inited = true + this._plugins = await resolveBoundedPlugins(this) + } } export interface ViteBuilder { @@ -1504,6 +1512,9 @@ export async function createBuilder( } const environment = await createEnvironment(name, environmentConfig) + + await environment.init() + environments[name] = environment } From a8adcac0b05874d1e0f44cdef50a374312a77d58 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Fri, 26 Apr 2024 22:38:28 +0200 Subject: [PATCH 029/123] release: v6.0.0-alpha.5 --- packages/vite/CHANGELOG.md | 6 ++++++ packages/vite/package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/vite/CHANGELOG.md b/packages/vite/CHANGELOG.md index e9d950e0d9a792..74450ba412901f 100644 --- a/packages/vite/CHANGELOG.md +++ b/packages/vite/CHANGELOG.md @@ -1,3 +1,9 @@ +## 6.0.0-alpha.5 (2024-04-26) + +* fix: use environment.plugins during build, support create hook ([a44810a](https://github.com/vitejs/vite/commit/a44810a)) + + + ## 6.0.0-alpha.4 (2024-04-26) * fix: backcompat merging ([294a84e](https://github.com/vitejs/vite/commit/294a84e)) diff --git a/packages/vite/package.json b/packages/vite/package.json index a38495fa7474f6..723784ad63280c 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -1,6 +1,6 @@ { "name": "vite", - "version": "6.0.0-alpha.4", + "version": "6.0.0-alpha.5", "type": "module", "license": "MIT", "author": "Evan You", From 00079da0e20203eb9b789b74e1d221c8fb7de62d Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Sun, 28 Apr 2024 14:36:31 +0900 Subject: [PATCH 030/123] fix: custom environment preload injection (#16541) Co-authored-by: patak-dev --- .../vite/src/node/__tests__/build.spec.ts | 40 ++++++++++++++++++ .../__tests__/fixtures/dynamic-import/dep.mjs | 1 + .../fixtures/dynamic-import/entry.mjs | 4 ++ packages/vite/src/node/build.ts | 41 +++++++++++-------- packages/vite/src/node/config.ts | 14 ++++++- 5 files changed, 81 insertions(+), 19 deletions(-) create mode 100644 packages/vite/src/node/__tests__/fixtures/dynamic-import/dep.mjs create mode 100644 packages/vite/src/node/__tests__/fixtures/dynamic-import/entry.mjs diff --git a/packages/vite/src/node/__tests__/build.spec.ts b/packages/vite/src/node/__tests__/build.spec.ts index 72082550bcebef..c7cdf38055e9bd 100644 --- a/packages/vite/src/node/__tests__/build.spec.ts +++ b/packages/vite/src/node/__tests__/build.spec.ts @@ -637,6 +637,46 @@ describe('resolveBuildOutputs', () => { ], }) }) + + test('ssr builtin', async () => { + const builder = await createBuilder({ + root: resolve(__dirname, 'fixtures/dynamic-import'), + environments: { + ssr: { + build: { + ssr: true, + rollupOptions: { + input: { + index: '/entry', + }, + }, + }, + }, + }, + }) + const result = await builder.build(builder.environments.ssr) + expect((result as RollupOutput).output[0].code).not.toContain('preload') + }) + + test('ssr custom', async () => { + const builder = await createBuilder({ + root: resolve(__dirname, 'fixtures/dynamic-import'), + environments: { + custom: { + build: { + ssr: true, + rollupOptions: { + input: { + index: '/entry', + }, + }, + }, + }, + }, + }) + const result = await builder.build(builder.environments.custom) + expect((result as RollupOutput).output[0].code).not.toContain('preload') + }) }) /** diff --git a/packages/vite/src/node/__tests__/fixtures/dynamic-import/dep.mjs b/packages/vite/src/node/__tests__/fixtures/dynamic-import/dep.mjs new file mode 100644 index 00000000000000..76805196e3d27d --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/dynamic-import/dep.mjs @@ -0,0 +1 @@ +export const hello = 'hello' diff --git a/packages/vite/src/node/__tests__/fixtures/dynamic-import/entry.mjs b/packages/vite/src/node/__tests__/fixtures/dynamic-import/entry.mjs new file mode 100644 index 00000000000000..997d636183c8a9 --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/dynamic-import/entry.mjs @@ -0,0 +1,4 @@ +export async function main() { + const mod = await import('./dep.mjs') + console.log(mod) +} diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 32d13a1b490a26..eb64d936ddd12e 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -8,13 +8,11 @@ import type { LoggingFunction, ModuleFormat, OutputOptions, - Plugin, RollupBuild, RollupError, RollupLog, RollupOptions, RollupOutput, - PluginContext as RollupPluginContext, RollupWatcher, WatcherOptions, } from 'rollup' @@ -68,6 +66,7 @@ import { mergeConfig } from './publicUtils' import { webWorkerPostPlugin } from './plugins/worker' import { getHookHandler } from './plugins' import { Environment } from './environment' +import type { Plugin, PluginContext } from './plugin' export interface BuildEnvironmentOptions { /** @@ -535,6 +534,7 @@ export async function build( function resolveConfigToBuild( inlineConfig: InlineConfig = {}, patchConfig?: (config: ResolvedConfig) => void, + patchPlugins?: (plugins: Plugin[]) => void, ) { return resolveConfig( inlineConfig, @@ -543,6 +543,7 @@ function resolveConfigToBuild( 'production', false, patchConfig, + patchPlugins, ) } @@ -1125,7 +1126,6 @@ function wrapEnvironmentLoad( 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), ) } @@ -1152,7 +1152,6 @@ function wrapEnvironmentTransform( injectEnvironmentInContext(this, environment), code, importer, - // @ts-expect-error: Receiving options param to be future-proof if Rollup adds it injectSsrFlag(args[0], environment), ) } @@ -1167,8 +1166,8 @@ function wrapEnvironmentTransform( } } -function injectEnvironmentInContext( - context: RollupPluginContext, +function injectEnvironmentInContext( + context: Context, environment?: BuildEnvironment, ) { return new Proxy(context, { @@ -1479,8 +1478,18 @@ export async function createBuilder( let environmentConfig = config if (!config.builder.sharedConfigBuild) { const patchConfig = (resolved: ResolvedConfig) => { + // Until the ecosystem updates to use `environment.options.build` instead of `config.build`, + // we need to make override `config.build` for the current environment. + // We can deprecate `config.build` in ResolvedConfig and push everyone to upgrade, and later + // remove the default values that shouldn't be used at all once the config is resolved + ;(resolved.build as ResolvedBuildOptions) = { + ...resolved.environments[name].build, + lib: false, + } + } + const patchPlugins = (resolvedPlugins: Plugin[]) => { // Force opt-in shared plugins - const environmentPlugins = [...resolved.plugins] + const environmentPlugins = [...resolvedPlugins] let validMixedPlugins = true for (let i = 0; i < environmentPlugins.length; i++) { const environmentPlugin = environmentPlugins[i] @@ -1497,18 +1506,16 @@ export async function createBuilder( } } if (validMixedPlugins) { - ;(resolved.plugins as Plugin[]) = environmentPlugins - } - // Until the ecosystem updates to use `environment.options.build` instead of `config.build`, - // we need to make override `config.build` for the current environment. - // We can deprecate `config.build` in ResolvedConfig and push everyone to upgrade, and later - // remove the default values that shouldn't be used at all once the config is resolved - ;(resolved.build as ResolvedBuildOptions) = { - ...resolved.environments[name].build, - lib: false, + for (let i = 0; i < environmentPlugins.length; i++) { + resolvedPlugins[i] = environmentPlugins[i] + } } } - environmentConfig = await resolveConfigToBuild(inlineConfig, patchConfig) + environmentConfig = await resolveConfigToBuild( + inlineConfig, + patchConfig, + patchPlugins, + ) } const environment = await createEnvironment(name, environmentConfig) diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index aa3ff8eec4d1da..f3a05f0862faa8 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -759,6 +759,7 @@ export async function resolveConfig( defaultNodeEnv = 'development', isPreview = false, patchConfig: ((config: ResolvedConfig) => void) | undefined = undefined, + patchPlugins: ((plugins: Plugin[]) => void) | undefined = undefined, ): Promise { let config = inlineConfig let configFileDependencies: string[] = [] @@ -1284,7 +1285,15 @@ export async function resolveConfig( ...config, ...resolved, } - ;(resolved.plugins as Plugin[]) = await resolvePlugins( + + // Backward compatibility hook, modify the resolved config before it is used + // to create inernal plugins. For example, `config.build.ssr`. Once we rework + // internal plugins to use environment.options, we can remove the dual + // patchConfig/patchPlugins and have a single patchConfig before configResolved + // gets called + patchConfig?.(resolved) + + const resolvedPlugins = await resolvePlugins( resolved, prePlugins, normalPlugins, @@ -1292,7 +1301,8 @@ export async function resolveConfig( ) // Backward compatibility hook used in builder - patchConfig?.(resolved) + patchPlugins?.(resolvedPlugins) + ;(resolved.plugins as Plugin[]) = resolvedPlugins Object.assign(resolved, createPluginHookUtils(resolved.plugins)) From e8473a635caa0bdc9f0e42335d52e4c2669428e9 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Sun, 28 Apr 2024 07:37:58 +0200 Subject: [PATCH 031/123] release: v6.0.0-alpha.6 --- packages/vite/CHANGELOG.md | 6 ++++++ packages/vite/package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/vite/CHANGELOG.md b/packages/vite/CHANGELOG.md index 74450ba412901f..e88378bbc54263 100644 --- a/packages/vite/CHANGELOG.md +++ b/packages/vite/CHANGELOG.md @@ -1,3 +1,9 @@ +## 6.0.0-alpha.6 (2024-04-28) + +* fix: custom environment preload injection (#16541) ([00079da](https://github.com/vitejs/vite/commit/00079da)), closes [#16541](https://github.com/vitejs/vite/issues/16541) + + + ## 6.0.0-alpha.5 (2024-04-26) * fix: use environment.plugins during build, support create hook ([a44810a](https://github.com/vitejs/vite/commit/a44810a)) diff --git a/packages/vite/package.json b/packages/vite/package.json index 723784ad63280c..1ebca55b4d476c 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -1,6 +1,6 @@ { "name": "vite", - "version": "6.0.0-alpha.5", + "version": "6.0.0-alpha.6", "type": "module", "license": "MIT", "author": "Evan You", From a6fc1ddcf921236cb914f0f3a0c7926bd66c6a65 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Mon, 29 Apr 2024 11:37:11 +0200 Subject: [PATCH 032/123] feat: this.environment in renderChunk and generateBundle --- packages/vite/src/node/build.ts | 31 ++++++++++++++++++- packages/vite/src/node/plugin.ts | 28 +++++++++++++++++ .../src/node/plugins/importAnalysisBuild.ts | 29 +++++++++++------ 3 files changed, 77 insertions(+), 11 deletions(-) diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index eb64d936ddd12e..135426c5791de5 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -1076,16 +1076,21 @@ function isExternal(id: string, test: string | RegExp) { } } +// TODO: +// - Could we get Rollup to let us extends PluginContext in a more performant way? +// - Extend for all hooks? export function injectEnvironmentToHooks( plugin: Plugin, environment?: BuildEnvironment, ): Plugin { - const { resolveId, load, transform } = plugin + const { resolveId, load, transform, generateBundle, renderChunk } = plugin return { ...plugin, resolveId: wrapEnvironmentResolveId(resolveId, environment), load: wrapEnvironmentLoad(load, environment), transform: wrapEnvironmentTransform(transform, environment), + generateBundle: wrapEnvironmentHook(generateBundle, environment), + renderChunk: wrapEnvironmentHook(renderChunk, environment), } } @@ -1166,6 +1171,30 @@ function wrapEnvironmentTransform( } } +function wrapEnvironmentHook( + hook?: Plugin[HookName], + environment?: BuildEnvironment, +): Plugin[HookName] { + if (!hook) return + + const fn = getHookHandler(hook) + const handler: Plugin[HookName] = function ( + this: PluginContext, + ...args: any[] + ) { + return fn.call(injectEnvironmentInContext(this, environment), ...args) + } + + if ('handler' in hook) { + return { + ...hook, + handler, + } as Plugin[HookName] + } else { + return handler + } +} + function injectEnvironmentInContext( context: Context, environment?: BuildEnvironment, diff --git a/packages/vite/src/node/plugin.ts b/packages/vite/src/node/plugin.ts index b0960c44d90813..7c7104c9b21aad 100644 --- a/packages/vite/src/node/plugin.ts +++ b/packages/vite/src/node/plugin.ts @@ -82,6 +82,27 @@ export interface TransformPluginContext extends RollupTransformPluginContext { * app level hooks (like config, configResolved, configureServer, etc). */ +type ModifyFunctionContext = Function_ extends ( + this: infer This, + ...parameters: infer Arguments +) => infer Return + ? (this: NewContext, ...parameters: Arguments) => Return + : never + +type ModifyObjectHookContext< + Handler, + Object_ extends { handler: Handler }, + NewContext, +> = Object_ & { + handler: ModifyFunctionContext +} + +type ModifyHookContext = Hook extends { + handler: infer Handler +} + ? ModifyObjectHookContext + : ModifyFunctionContext + export interface BasePlugin extends RollupPlugin { /** * Perform custom handling of HMR updates. @@ -160,6 +181,13 @@ export interface BasePlugin extends RollupPlugin { }, ) => Promise | TransformResult > + + // TODO: abstract to every hook in RollupPlugin? + generateBundle?: ModifyHookContext< + RollupPlugin['generateBundle'], + PluginContext + > + renderChunk?: ModifyHookContext['renderChunk'], PluginContext> } export type BoundedPlugin = BasePlugin diff --git a/packages/vite/src/node/plugins/importAnalysisBuild.ts b/packages/vite/src/node/plugins/importAnalysisBuild.ts index 2c3fdcee0e7e23..f190d18cc0cda4 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -156,9 +156,7 @@ function preload( * Build only. During serve this is performed as part of ./importAnalysis. */ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { - const ssr = !!config.build.ssr const isWorker = config.isWorker - const insertPreload = !(ssr || !!config.build.lib || isWorker) const resolveModulePreloadDependencies = config.build.modulePreload && config.build.modulePreload.resolveDependencies @@ -213,7 +211,11 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { }, async transform(source, importer) { - if (isInNodeModules(importer) && !dynamicImportPrefixRE.test(source)) { + const { environment } = this + if ( + !environment || + (isInNodeModules(importer) && !dynamicImportPrefixRE.test(source)) + ) { return } @@ -239,6 +241,9 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { const str = () => s || (s = new MagicString(source)) let needPreloadHelper = false + const ssr = environment.options.build.ssr + const insertPreload = !(ssr || !!config.build.lib || isWorker) + for (let index = 0; index < imports.length; index++) { const { e: end, @@ -280,7 +285,7 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { if (s) { return { code: s.toString(), - map: config.build.sourcemap + map: environment.options.build.sourcemap ? s.generateMap({ hires: 'boundary' }) : null, } @@ -289,10 +294,11 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { renderChunk(code, _, { format }) { // make sure we only perform the preload logic in modern builds. - if (code.indexOf(isModernFlag) > -1) { + const { environment } = this + if (environment && code.indexOf(isModernFlag) > -1) { const re = new RegExp(isModernFlag, 'g') const isModern = String(format === 'es') - if (config.build.sourcemap) { + if (environment.options.build.sourcemap) { const s = new MagicString(code) let match: RegExpExecArray | null while ((match = re.exec(code))) { @@ -310,9 +316,12 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { }, generateBundle({ format }, bundle) { - if (format !== 'es' || ssr || isWorker) { + const { environment } = this + const ssr = environment?.options.build.ssr + if (!environment || format !== 'es' || ssr || isWorker) { return } + const buildSourcemap = environment.options.build.sourcemap for (const file in bundle) { const chunk = bundle[file] @@ -544,7 +553,7 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { if (s.hasChanged()) { chunk.code = s.toString() - if (config.build.sourcemap && chunk.map) { + if (buildSourcemap && chunk.map) { const nextMap = s.generateMap({ source: chunk.fileName, hires: 'boundary', @@ -556,13 +565,13 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { map.toUrl = () => genSourceMapUrl(map) chunk.map = map - if (config.build.sourcemap === 'inline') { + if (buildSourcemap === 'inline') { chunk.code = chunk.code.replace( convertSourceMap.mapFileCommentRegex, '', ) chunk.code += `\n//# sourceMappingURL=${genSourceMapUrl(map)}` - } else if (config.build.sourcemap) { + } else if (buildSourcemap) { const mapAsset = bundle[chunk.fileName + '.map'] if (mapAsset && mapAsset.type === 'asset') { mapAsset.source = map.toString() From 8231283065fa79509cc42934bb15f7a485d1d786 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Mon, 29 Apr 2024 12:00:12 +0200 Subject: [PATCH 033/123] feat: remove config.build from dynamicImportVars plugin --- packages/vite/src/node/environment.ts | 14 ++++++++++++++ .../vite/src/node/plugins/dynamicImportVars.ts | 15 ++++++++++----- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/vite/src/node/environment.ts b/packages/vite/src/node/environment.ts index 7b5b71b61a98ff..bfa03927b8d8bf 100644 --- a/packages/vite/src/node/environment.ts +++ b/packages/vite/src/node/environment.ts @@ -91,3 +91,17 @@ const environmentColors = [ colors.green, colors.gray, ] + +export function cachedByEnvironment( + create: (environment: Environment) => Data, +): (environment: Environment) => Data { + const cache = new WeakMap() + return function (environment: Environment) { + let data = cache.get(environment) + if (!data) { + data = create(environment) + cache.set(environment, data) + } + return data + } +} diff --git a/packages/vite/src/node/plugins/dynamicImportVars.ts b/packages/vite/src/node/plugins/dynamicImportVars.ts index e615a3d1442e65..903902fe28f609 100644 --- a/packages/vite/src/node/plugins/dynamicImportVars.ts +++ b/packages/vite/src/node/plugins/dynamicImportVars.ts @@ -5,6 +5,8 @@ import type { ImportSpecifier } from 'es-module-lexer' import { parse as parseJS } from 'acorn' import { dynamicImportToGlob } from '@rollup/plugin-dynamic-import-vars' import type { Plugin } from '../plugin' +import type { Environment } from '../environment' +import { cachedByEnvironment } from '../environment' import type { ResolvedConfig } from '../config' import { CLIENT_ENTRY } from '../constants' import { createIdResolver } from '../idResolver' @@ -159,9 +161,12 @@ export function dynamicImportVarsPlugin(config: ResolvedConfig): Plugin { tryIndex: false, extensions: [], }) - const { include, exclude, warnOnError } = - config.build.dynamicImportVarsOptions - const filter = createFilter(include, exclude) + + const getFilter = cachedByEnvironment((environment: Environment) => { + const { include, exclude } = + environment.options.build.dynamicImportVarsOptions + return createFilter(include, exclude) + }) return { name: 'vite:dynamic-import-vars', @@ -182,7 +187,7 @@ export function dynamicImportVarsPlugin(config: ResolvedConfig): Plugin { const { environment } = this if ( !environment || - !filter(importer) || + !getFilter(environment)(importer) || importer === CLIENT_ENTRY || !hasDynamicImportRE.test(source) ) { @@ -233,7 +238,7 @@ export function dynamicImportVarsPlugin(config: ResolvedConfig): Plugin { config.root, ) } catch (error) { - if (warnOnError) { + if (environment.options.build.dynamicImportVarsOptions.warnOnError) { this.warn(error) } else { this.error(error) From bda0dc50b446340f6e83e24c18c305c39e289aeb Mon Sep 17 00:00:00 2001 From: patak-dev Date: Mon, 29 Apr 2024 14:29:48 +0200 Subject: [PATCH 034/123] feat: this.environment in buildStart, rework more internal plugins --- packages/vite/src/node/build.ts | 13 ++- packages/vite/src/node/config.ts | 7 +- packages/vite/src/node/plugin.ts | 1 + packages/vite/src/node/plugins/asset.ts | 82 +++++++++-------- .../src/node/plugins/assetImportMetaUrl.ts | 4 +- packages/vite/src/node/plugins/css.ts | 32 +++---- packages/vite/src/node/plugins/html.ts | 6 +- .../src/node/plugins/importAnalysisBuild.ts | 89 +++++++++++-------- packages/vite/src/node/plugins/manifest.ts | 22 +++-- packages/vite/src/node/plugins/wasm.ts | 2 +- packages/vite/src/node/plugins/worker.ts | 7 +- .../src/node/plugins/workerImportMetaUrl.ts | 2 +- 12 files changed, 155 insertions(+), 112 deletions(-) diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 135426c5791de5..43364268a9b2a8 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -496,7 +496,7 @@ export async function resolveBuildPlugins(config: ResolvedConfig): Promise<{ ...(options.minify ? [terserPlugin(config)] : []), ...(!config.isWorker ? [ - ...(options.manifest ? [manifestPlugin(config)] : []), + ...(options.manifest ? [manifestPlugin()] : []), ...(options.ssrManifest ? [ssrManifestPlugin(config)] : []), buildReporterPlugin(config), ] @@ -1083,12 +1083,20 @@ export function injectEnvironmentToHooks( plugin: Plugin, environment?: BuildEnvironment, ): Plugin { - const { resolveId, load, transform, generateBundle, renderChunk } = plugin + const { + buildStart, + resolveId, + load, + transform, + generateBundle, + renderChunk, + } = plugin return { ...plugin, resolveId: wrapEnvironmentResolveId(resolveId, environment), load: wrapEnvironmentLoad(load, environment), transform: wrapEnvironmentTransform(transform, environment), + buildStart: wrapEnvironmentHook(buildStart, environment), generateBundle: wrapEnvironmentHook(generateBundle, environment), renderChunk: wrapEnvironmentHook(renderChunk, environment), } @@ -1303,6 +1311,7 @@ export type RenderBuiltAssetUrl = ( }, ) => string | { relative?: boolean; runtime?: string } | undefined +// TODO: experimental.renderBuiltUrl => environment.build.renderBuiltUrl? export function toOutputFilePathInJS( filename: string, type: 'asset' | 'public', diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index f3a05f0862faa8..57ef0c356669f4 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -491,7 +491,9 @@ export interface LegacyOptions { export interface ResolvedWorkerOptions { format: 'es' | 'iife' - plugins: (bundleChain: string[]) => Promise + plugins: ( + bundleChain: string[], + ) => Promise<{ plugins: Plugin[]; config: ResolvedConfig }> rollupOptions: RollupOptions } @@ -1084,6 +1086,7 @@ export async function resolveConfig( ) } + // TODO: Workers as environments could allow us to remove a lot of complexity const createWorkerPlugins = async function (bundleChain: string[]) { // Some plugins that aren't intended to work in the bundling of workers (doing post-processing at build time for example). // And Plugins may also have cached that could be corrupted by being used in these extra rollup calls. @@ -1130,7 +1133,7 @@ export async function resolveConfig( .map((hook) => hook(workerResolved)), ) - return resolvedWorkerPlugins + return { plugins: resolvedWorkerPlugins, config: workerResolved } } const resolvedWorkerOptions: ResolvedWorkerOptions = { diff --git a/packages/vite/src/node/plugin.ts b/packages/vite/src/node/plugin.ts index 7c7104c9b21aad..98297653deb96d 100644 --- a/packages/vite/src/node/plugin.ts +++ b/packages/vite/src/node/plugin.ts @@ -183,6 +183,7 @@ export interface BasePlugin extends RollupPlugin { > // TODO: abstract to every hook in RollupPlugin? + buildStart?: ModifyHookContext['buildStart'], PluginContext> generateBundle?: ModifyHookContext< RollupPlugin['generateBundle'], PluginContext diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index 1a15022e03d423..36a6b510b66a51 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -3,18 +3,15 @@ import { parse as parseUrl } from 'node:url' import fsp from 'node:fs/promises' import { Buffer } from 'node:buffer' import * as mrmime from 'mrmime' -import type { - NormalizedOutputOptions, - PluginContext, - RenderedChunk, -} from 'rollup' +import type { NormalizedOutputOptions, RenderedChunk } from 'rollup' import MagicString from 'magic-string' import colors from 'picocolors' import { createToImportMetaURLBasedRelativeRuntime, toOutputFilePathInJS, } from '../build' -import type { Plugin } from '../plugin' +import type { Plugin, PluginContext } from '../plugin' +import type { Environment } from '../environment' import type { ResolvedConfig } from '../config' import { checkPublicFile } from '../publicDir' import { @@ -36,7 +33,7 @@ export const assetUrlRE = /__VITE_ASSET__([\w$]+)__(?:\$_(.*?)__)?/g const jsSourceMapRE = /\.[cm]?js\.map$/ -const assetCache = new WeakMap>() +const assetCache = new WeakMap>() // chunk.name is the basename for the asset ignoring the directory structure // For the manifest, we need to preserve the original file path and isEntry @@ -45,8 +42,8 @@ export interface GeneratedAssetMeta { originalName: string isEntry?: boolean } -export const generatedAssets = new WeakMap< - ResolvedConfig, +export const generatedAssetsMap = new WeakMap< + Environment, Map >() @@ -62,11 +59,13 @@ export function registerCustomMime(): void { export function renderAssetUrlInJS( ctx: PluginContext, - config: ResolvedConfig, chunk: RenderedChunk, opts: NormalizedOutputOptions, code: string, ): MagicString | undefined { + const environment = ctx.environment! + const { config } = environment + const toRelativeRuntime = createToImportMetaURLBasedRelativeRuntime( opts.format, config.isWorker, @@ -145,8 +144,12 @@ export function assetPlugin(config: ResolvedConfig): Plugin { name: 'vite:asset', buildStart() { - assetCache.set(config, new Map()) - generatedAssets.set(config, new Map()) + const { environment } = this + if (!environment) { + return + } + assetCache.set(environment, new Map()) + generatedAssetsMap.set(environment, new Map()) }, resolveId(id) { @@ -189,7 +192,7 @@ export function assetPlugin(config: ResolvedConfig): Plugin { } id = removeUrlQuery(id) - let url = await fileToUrl(id, config, this) + let url = await fileToUrl(this, id) // Inherit HMR timestamp if this asset was invalidated const environment = this.environment @@ -212,12 +215,12 @@ export function assetPlugin(config: ResolvedConfig): Plugin { }, renderChunk(code, chunk, opts) { - const s = renderAssetUrlInJS(this, config, chunk, opts, code) + const s = renderAssetUrlInJS(this, chunk, opts, code) if (s) { return { code: s.toString(), - map: config.build.sourcemap + map: this.environment?.options.build.sourcemap ? s.generateMap({ hires: 'boundary' }) : null, } @@ -227,6 +230,8 @@ export function assetPlugin(config: ResolvedConfig): Plugin { }, generateBundle(_, bundle) { + const environment = this.environment! + // Remove empty entry point file for (const file in bundle) { const chunk = bundle[file] @@ -241,11 +246,7 @@ export function assetPlugin(config: ResolvedConfig): Plugin { } // do not emit assets for SSR build - if ( - config.command === 'build' && - config.build.ssr && - !config.build.emitAssets - ) { + if (config.command === 'build' && !environment.options.build.emitAssets) { for (const file in bundle) { if ( bundle[file].type === 'asset' && @@ -261,14 +262,14 @@ export function assetPlugin(config: ResolvedConfig): Plugin { } export async function fileToUrl( - id: string, - config: ResolvedConfig, ctx: PluginContext, + id: string, ): Promise { - if (config.command === 'serve') { - return fileToDevUrl(id, config) + const environment = ctx.environment! + if (environment.config.command === 'serve') { + return fileToDevUrl(id, environment.config) } else { - return fileToBuiltUrl(id, config, ctx) + return fileToBuiltUrl(ctx, id) } } @@ -336,17 +337,18 @@ function isGitLfsPlaceholder(content: Buffer): boolean { * and returns the resolved public URL */ async function fileToBuiltUrl( - id: string, - config: ResolvedConfig, pluginContext: PluginContext, + id: string, skipPublicCheck = false, forceInline?: boolean, ): Promise { + const environment = pluginContext.environment! + const { config } = environment if (!skipPublicCheck && checkPublicFile(id, config)) { return publicFileToBuiltUrl(id, config) } - const cache = assetCache.get(config)! + const cache = assetCache.get(environment)! const cached = cache.get(id) if (cached) { return cached @@ -356,7 +358,7 @@ async function fileToBuiltUrl( const content = await fsp.readFile(file) let url: string - if (shouldInline(config, file, id, content, pluginContext, forceInline)) { + if (shouldInline(pluginContext, file, id, content, forceInline)) { if (config.build.lib && isGitLfsPlaceholder(content)) { config.logger.warn( colors.yellow(`Inlined file ${id} was not downloaded via Git LFS`), @@ -383,7 +385,7 @@ async function fileToBuiltUrl( }) const originalName = normalizePath(path.relative(config.root, file)) - generatedAssets.get(config)!.set(referenceId, { originalName }) + generatedAssetsMap.get(environment)!.set(referenceId, { originalName }) url = `__VITE_ASSET__${referenceId}__${postfix ? `$_${postfix}__` : ``}` // TODO_BASE } @@ -393,12 +395,13 @@ async function fileToBuiltUrl( } export async function urlToBuiltUrl( + pluginContext: PluginContext, url: string, importer: string, - config: ResolvedConfig, - pluginContext: PluginContext, forceInline?: boolean, ): Promise { + const environment = pluginContext.environment! + const { config } = environment if (checkPublicFile(url, config)) { return publicFileToBuiltUrl(url, config) } @@ -407,9 +410,8 @@ export async function urlToBuiltUrl( ? path.join(config.root, url) : path.join(path.dirname(importer), url) return fileToBuiltUrl( - file, - config, pluginContext, + file, // skip public check since we just did it above true, forceInline, @@ -417,23 +419,25 @@ export async function urlToBuiltUrl( } const shouldInline = ( - config: ResolvedConfig, + pluginContext: PluginContext, file: string, id: string, content: Buffer, - pluginContext: PluginContext, forceInline: boolean | undefined, ): boolean => { + const environment = pluginContext.environment! + const { config } = environment + const { assetsInlineLimit } = environment.options.build if (config.build.lib) return true if (pluginContext.getModuleInfo(id)?.isEntry) return false if (forceInline !== undefined) return forceInline let limit: number - if (typeof config.build.assetsInlineLimit === 'function') { - const userShouldInline = config.build.assetsInlineLimit(file, content) + if (typeof assetsInlineLimit === 'function') { + const userShouldInline = assetsInlineLimit(file, content) if (userShouldInline != null) return userShouldInline limit = DEFAULT_ASSETS_INLINE_LIMIT } else { - limit = Number(config.build.assetsInlineLimit) + limit = Number(assetsInlineLimit) } if (file.endsWith('.html')) return false // Don't inline SVG with fragments, as they are meant to be reused diff --git a/packages/vite/src/node/plugins/assetImportMetaUrl.ts b/packages/vite/src/node/plugins/assetImportMetaUrl.ts index fd5b4a8e379e8e..9b68ff7553d42a 100644 --- a/packages/vite/src/node/plugins/assetImportMetaUrl.ts +++ b/packages/vite/src/node/plugins/assetImportMetaUrl.ts @@ -126,9 +126,9 @@ export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin { try { if (publicDir && isParentDirectory(publicDir, file)) { const publicPath = '/' + path.posix.relative(publicDir, file) - builtUrl = await fileToUrl(publicPath, config, this) + builtUrl = await fileToUrl(this, publicPath) } else { - builtUrl = await fileToUrl(file, config, this) + builtUrl = await fileToUrl(this, file) } } catch { // do nothing, we'll log a warning after this diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 8304306ad2d896..38556c154a9397 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -75,7 +75,7 @@ import { addToHTMLProxyTransformResult } from './html' import { assetUrlRE, fileToUrl, - generatedAssets, + generatedAssetsMap, publicAssetUrlCache, publicAssetUrlRE, publicFileToBuiltUrl, @@ -343,7 +343,7 @@ export function cssPlugin(config: ResolvedConfig): Plugin { } const resolved = await resolveUrl(decodedUrl, importer) if (resolved) { - return fileToUrl(resolved, config, this) + return fileToUrl(this, resolved) } if (config.command === 'build') { const isExternal = config.build.rollupOptions.external @@ -556,6 +556,12 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { }, async renderChunk(code, chunk, opts) { + const { environment } = this + if (!environment) { + return + } + const generatedAssets = generatedAssetsMap.get(environment)! + let chunkCSS = '' let isPureCssChunk = true const ids = Object.keys(chunk.modules) @@ -710,9 +716,7 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { type: 'asset', source: content, }) - generatedAssets - .get(config)! - .set(referenceId, { originalName: originalFilename }) + generatedAssets.set(referenceId, { originalName: originalFilename }) const replacement = toOutputFilePathInJS( this.getFileName(referenceId), @@ -767,9 +771,10 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { type: 'asset', source: chunkCSS, }) - generatedAssets - .get(config)! - .set(referenceId, { originalName: originalFilename, isEntry }) + generatedAssets.set(referenceId, { + originalName: originalFilename, + isEntry, + }) chunk.viteMetadata!.importedCss.add(this.getFileName(referenceId)) } else if (!config.build.ssr) { // legacy build and inline css @@ -783,13 +788,8 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { chunkCSS = await finalizeCss(chunkCSS, true, config) let cssString = JSON.stringify(chunkCSS) cssString = - renderAssetUrlInJS( - this, - config, - chunk, - opts, - cssString, - )?.toString() || cssString + renderAssetUrlInJS(this, chunk, opts, cssString)?.toString() || + cssString const style = `__vite_style__` const injectCode = `var ${style} = document.createElement('style');` + @@ -980,7 +980,7 @@ export function cssAnalysisPlugin(config: ResolvedConfig): Plugin { ? moduleGraph!.createFileOnlyEntry(file) : await moduleGraph!.ensureEntryFromUrl( stripBase( - await fileToUrl(file, config, this), + await fileToUrl(this, file), (config.server?.origin ?? '') + devBase, ), ), diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index 7d971375040b5c..f386f5b6e05c44 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -411,7 +411,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { !namedOutput.includes(removeLeadingSlash(url)) // Allow for absolute references as named output can't be an absolute path ) { try { - return await urlToBuiltUrl(url, id, config, this, shouldInline) + return await urlToBuiltUrl(this, url, id, shouldInline) } catch (e) { if (e.code !== 'ENOENT') { throw e @@ -646,7 +646,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { s.update( start, end, - partialEncodeURIPath(await urlToBuiltUrl(url, id, config, this)), + partialEncodeURIPath(await urlToBuiltUrl(this, url, id)), ) } } @@ -862,7 +862,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { } // inject css link when cssCodeSplit is false - if (!config.build.cssCodeSplit) { + if (this.environment?.options.build.cssCodeSplit === false) { const cssChunk = Object.values(bundle).find( (chunk) => chunk.type === 'asset' && chunk.name === 'style.css', ) as OutputAsset | undefined diff --git a/packages/vite/src/node/plugins/importAnalysisBuild.ts b/packages/vite/src/node/plugins/importAnalysisBuild.ts index f190d18cc0cda4..af6244804e8a4c 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -15,6 +15,7 @@ import { numberToPos, } from '../utils' import type { Plugin } from '../plugin' +import type { Environment } from '../environment' import type { ResolvedConfig } from '../config' import { toOutputFilePathInJS } from '../build' import { genSourceMapUrl } from '../server/sourcemap' @@ -152,14 +153,11 @@ function preload( }) } -/** - * Build only. During serve this is performed as part of ./importAnalysis. - */ -export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { - const isWorker = config.isWorker - +function getModulePreloadData(environment: Environment) { + const { modulePreload } = environment.options.build + const { config } = environment const resolveModulePreloadDependencies = - config.build.modulePreload && config.build.modulePreload.resolveDependencies + modulePreload && modulePreload.resolveDependencies const renderBuiltUrl = config.experimental.renderBuiltUrl const customModulePreloadPaths = !!( resolveModulePreloadDependencies || renderBuiltUrl @@ -167,34 +165,17 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { const isRelativeBase = config.base === './' || config.base === '' const optimizeModulePreloadRelativePaths = isRelativeBase && !customModulePreloadPaths + return { + customModulePreloadPaths, + optimizeModulePreloadRelativePaths, + } +} - const { modulePreload } = config.build - const scriptRel = - modulePreload && modulePreload.polyfill - ? `'modulepreload'` - : `(${detectScriptRel.toString()})()` - - // There are three different cases for the preload list format in __vitePreload - // - // __vitePreload(() => import(asyncChunk), [ ...deps... ]) - // - // This is maintained to keep backwards compatibility as some users developed plugins - // using regex over this list to workaround the fact that module preload wasn't - // configurable. - const assetsURL = customModulePreloadPaths - ? // If `experimental.renderBuiltUrl` or `build.modulePreload.resolveDependencies` are used - // the dependencies are already resolved. To avoid the need for `new URL(dep, import.meta.url)` - // a helper `__vitePreloadRelativeDep` is used to resolve from relative paths which can be minimized. - `function(dep, importerUrl) { return dep[0] === '.' ? new URL(dep, importerUrl).href : dep }` - : optimizeModulePreloadRelativePaths - ? // If there isn't custom resolvers affecting the deps list, deps in the list are relative - // to the current chunk and are resolved to absolute URL by the __vitePreload helper itself. - // The importerUrl is passed as third parameter to __vitePreload in this case - `function(dep, importerUrl) { return new URL(dep, importerUrl).href }` - : // If the base isn't relative, then the deps are relative to the projects `outDir` and the base - // is appended inside __vitePreload too. - `function(dep) { return ${JSON.stringify(config.base)}+dep }` - const preloadCode = `const scriptRel = ${scriptRel};const assetsURL = ${assetsURL};const seen = {};export const ${preloadMethod} = ${preload.toString()}` +/** + * Build only. During serve this is performed as part of ./importAnalysis. + */ +export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { + const isWorker = config.isWorker return { name: 'vite:build-import-analysis', @@ -205,7 +186,39 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { }, load(id) { - if (id === preloadHelperId) { + const { environment } = this + if (environment && id === preloadHelperId) { + const { modulePreload } = environment.options.build + + const { customModulePreloadPaths, optimizeModulePreloadRelativePaths } = + getModulePreloadData(environment) + + const scriptRel = + modulePreload && modulePreload.polyfill + ? `'modulepreload'` + : `(${detectScriptRel.toString()})()` + + // There are three different cases for the preload list format in __vitePreload + // + // __vitePreload(() => import(asyncChunk), [ ...deps... ]) + // + // This is maintained to keep backwards compatibility as some users developed plugins + // using regex over this list to workaround the fact that module preload wasn't + // configurable. + const assetsURL = customModulePreloadPaths + ? // If `experimental.renderBuiltUrl` or `build.modulePreload.resolveDependencies` are used + // the dependencies are already resolved. To avoid the need for `new URL(dep, import.meta.url)` + // a helper `__vitePreloadRelativeDep` is used to resolve from relative paths which can be minimized. + `function(dep, importerUrl) { return dep[0] === '.' ? new URL(dep, importerUrl).href : dep }` + : optimizeModulePreloadRelativePaths + ? // If there isn't custom resolvers affecting the deps list, deps in the list are relative + // to the current chunk and are resolved to absolute URL by the __vitePreload helper itself. + // The importerUrl is passed as third parameter to __vitePreload in this case + `function(dep, importerUrl) { return new URL(dep, importerUrl).href }` + : // If the base isn't relative, then the deps are relative to the projects `outDir` and the base + // is appended inside __vitePreload too. + `function(dep) { return ${JSON.stringify(config.base)}+dep }` + const preloadCode = `const scriptRel = ${scriptRel};const assetsURL = ${assetsURL};const seen = {};export const ${preloadMethod} = ${preload.toString()}` return preloadCode } }, @@ -244,6 +257,9 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { const ssr = environment.options.build.ssr const insertPreload = !(ssr || !!config.build.lib || isWorker) + const { customModulePreloadPaths, optimizeModulePreloadRelativePaths } = + getModulePreloadData(environment) + for (let index = 0; index < imports.length; index++) { const { e: end, @@ -322,6 +338,9 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { return } const buildSourcemap = environment.options.build.sourcemap + const { modulePreload } = environment.options.build + const { customModulePreloadPaths, optimizeModulePreloadRelativePaths } = + getModulePreloadData(environment) for (const file in bundle) { const chunk = bundle[file] diff --git a/packages/vite/src/node/plugins/manifest.ts b/packages/vite/src/node/plugins/manifest.ts index 84602e78268866..fe92c8df193b47 100644 --- a/packages/vite/src/node/plugins/manifest.ts +++ b/packages/vite/src/node/plugins/manifest.ts @@ -5,10 +5,9 @@ import type { OutputChunk, RenderedChunk, } from 'rollup' -import type { ResolvedConfig } from '..' import type { Plugin } from '../plugin' import { normalizePath, sortObjectKeys } from '../utils' -import { generatedAssets } from './asset' +import { generatedAssetsMap } from './asset' import type { GeneratedAssetMeta } from './asset' const endsWithJSRE = /\.[cm]?js$/ @@ -27,7 +26,7 @@ export interface ManifestChunk { dynamicImports?: string[] } -export function manifestPlugin(config: ResolvedConfig): Plugin { +export function manifestPlugin(): Plugin { const manifest: Manifest = {} let outputCount: number @@ -40,8 +39,15 @@ export function manifestPlugin(config: ResolvedConfig): Plugin { }, generateBundle({ format }, bundle) { + const { environment } = this + if (!environment) { + return + } + const { root } = environment.config + const buildOptions = environment.options.build + function getChunkName(chunk: OutputChunk) { - return getChunkOriginalFileName(chunk, config.root, format) + return getChunkOriginalFileName(chunk, root, format) } function getInternalImports(imports: string[]): string[] { @@ -112,7 +118,7 @@ export function manifestPlugin(config: ResolvedConfig): Plugin { } const fileNameToAssetMeta = new Map() - const assets = generatedAssets.get(config)! + const assets = generatedAssetsMap.get(environment)! assets.forEach((asset, referenceId) => { try { const fileName = this.getFileName(referenceId) @@ -158,13 +164,13 @@ export function manifestPlugin(config: ResolvedConfig): Plugin { }) outputCount++ - const output = config.build.rollupOptions?.output + const output = buildOptions.rollupOptions?.output const outputLength = Array.isArray(output) ? output.length : 1 if (outputCount >= outputLength) { this.emitFile({ fileName: - typeof config.build.manifest === 'string' - ? config.build.manifest + typeof buildOptions.manifest === 'string' + ? buildOptions.manifest : '.vite/manifest.json', type: 'asset', source: JSON.stringify(sortObjectKeys(manifest), undefined, 2), diff --git a/packages/vite/src/node/plugins/wasm.ts b/packages/vite/src/node/plugins/wasm.ts index f5adab8e1b43d5..22ab2ab00eade4 100644 --- a/packages/vite/src/node/plugins/wasm.ts +++ b/packages/vite/src/node/plugins/wasm.ts @@ -65,7 +65,7 @@ export const wasmHelperPlugin = (config: ResolvedConfig): Plugin => { return } - const url = await fileToUrl(id, config, this) + const url = await fileToUrl(this, id) return ` import initWasm from "${wasmHelperId}" diff --git a/packages/vite/src/node/plugins/worker.ts b/packages/vite/src/node/plugins/worker.ts index 6ac5f470b65082..0d38b559ad820e 100644 --- a/packages/vite/src/node/plugins/worker.ts +++ b/packages/vite/src/node/plugins/worker.ts @@ -69,8 +69,9 @@ 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 { plugins: resolvedPlugins, config: workerConfig } = + await plugins(newBundleChain) + const workerEnvironment = new BuildEnvironment('client', workerConfig) // TODO: should this be 'worker'? const bundle = await rollup({ ...rollupOptions, input, @@ -356,7 +357,7 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin { urlCode = JSON.stringify(await workerFileToUrl(config, id)) } } else { - let url = await fileToUrl(cleanUrl(id), config, this) + let url = await fileToUrl(this, cleanUrl(id)) url = injectQuery(url, `${WORKER_FILE_ID}&type=${workerType}`) urlCode = JSON.stringify(url) } diff --git a/packages/vite/src/node/plugins/workerImportMetaUrl.ts b/packages/vite/src/node/plugins/workerImportMetaUrl.ts index 53f867a004a4b1..ebae94e14d38ab 100644 --- a/packages/vite/src/node/plugins/workerImportMetaUrl.ts +++ b/packages/vite/src/node/plugins/workerImportMetaUrl.ts @@ -182,7 +182,7 @@ export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin { if (isBuild) { builtUrl = await workerFileToUrl(config, file) } else { - builtUrl = await fileToUrl(cleanUrl(file), config, this) + builtUrl = await fileToUrl(this, cleanUrl(file)) builtUrl = injectQuery( builtUrl, `${WORKER_FILE_ID}&type=${workerType}`, From 05943cf28bce536aab5b26eadc35bcf67a337c41 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Mon, 29 Apr 2024 14:42:23 +0200 Subject: [PATCH 035/123] release: v6.0.0-alpha.7 --- packages/vite/CHANGELOG.md | 8 ++++++++ packages/vite/package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/vite/CHANGELOG.md b/packages/vite/CHANGELOG.md index e88378bbc54263..119220c78c9f89 100644 --- a/packages/vite/CHANGELOG.md +++ b/packages/vite/CHANGELOG.md @@ -1,3 +1,11 @@ +## 6.0.0-alpha.7 (2024-04-29) + +* feat: remove config.build from dynamicImportVars plugin ([8231283](https://github.com/vitejs/vite/commit/8231283)) +* feat: this.environment in buildStart, rework more internal plugins ([bda0dc5](https://github.com/vitejs/vite/commit/bda0dc5)) +* feat: this.environment in renderChunk and generateBundle ([a6fc1dd](https://github.com/vitejs/vite/commit/a6fc1dd)) + + + ## 6.0.0-alpha.6 (2024-04-28) * fix: custom environment preload injection (#16541) ([00079da](https://github.com/vitejs/vite/commit/00079da)), closes [#16541](https://github.com/vitejs/vite/issues/16541) diff --git a/packages/vite/package.json b/packages/vite/package.json index 1ebca55b4d476c..0d23a6e0d39559 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -1,6 +1,6 @@ { "name": "vite", - "version": "6.0.0-alpha.6", + "version": "6.0.0-alpha.7", "type": "module", "license": "MIT", "author": "Evan You", From 7566aae1ce4769130da48a1c58fcceff4438d419 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Mon, 29 Apr 2024 21:14:22 +0200 Subject: [PATCH 036/123] feat: define and html plugins --- .../vite/src/node/plugins/clientInjections.ts | 25 ++++++++++--------- packages/vite/src/node/plugins/define.ts | 11 +++++--- packages/vite/src/node/plugins/html.ts | 14 +++++++++-- 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/packages/vite/src/node/plugins/clientInjections.ts b/packages/vite/src/node/plugins/clientInjections.ts index c66f3877eca822..c0cd54a36931ee 100644 --- a/packages/vite/src/node/plugins/clientInjections.ts +++ b/packages/vite/src/node/plugins/clientInjections.ts @@ -89,26 +89,27 @@ export function clientInjectionsPlugin(config: ResolvedConfig): Plugin { .replace(`__HMR_CONFIG_NAME__`, hmrConfigNameReplacement) } }, - async transform(code, id, options) { + async transform(code, id) { + const { environment } = this + if (!environment) { + return + } + // TODO: !environment.options.nodeCompatible ? + const ssr = environment.name !== 'client' if (id === normalizedClientEntry || id === normalizedEnvEntry) { return injectConfigValues(code) - } else if (!options?.ssr && code.includes('process.env.NODE_ENV')) { + } else if (!ssr && code.includes('process.env.NODE_ENV')) { // replace process.env.NODE_ENV instead of defining a global // for it to avoid shimming a `process` object during dev, // avoiding inconsistencies between dev and build const nodeEnv = config.define?.['process.env.NODE_ENV'] || JSON.stringify(process.env.NODE_ENV || config.mode) - return await replaceDefine( - code, - id, - { - 'process.env.NODE_ENV': nodeEnv, - 'global.process.env.NODE_ENV': nodeEnv, - 'globalThis.process.env.NODE_ENV': nodeEnv, - }, - config, - ) + return await replaceDefine(environment, code, id, { + 'process.env.NODE_ENV': nodeEnv, + 'global.process.env.NODE_ENV': nodeEnv, + 'globalThis.process.env.NODE_ENV': nodeEnv, + }) } }, } diff --git a/packages/vite/src/node/plugins/define.ts b/packages/vite/src/node/plugins/define.ts index 607eb8d4a43a55..5257a754886ae6 100644 --- a/packages/vite/src/node/plugins/define.ts +++ b/packages/vite/src/node/plugins/define.ts @@ -148,16 +148,16 @@ export function definePlugin(config: ResolvedConfig): Plugin { pattern.lastIndex = 0 if (!pattern.test(code)) return - return await replaceDefine(code, id, define, config) + return await replaceDefine(environment, code, id, define) }, } } export async function replaceDefine( + environment: Environment, code: string, id: string, define: Record, - config: ResolvedConfig, ): Promise<{ code: string; map: string | null }> { // Because esbuild only allows JSON-serializable values, and `import.meta.env` // may contain values with raw identifiers, making it non-JSON-serializable, @@ -172,7 +172,7 @@ export async function replaceDefine( define = { ...define, 'import.meta.env': marker } } - const esbuildOptions = config.esbuild || {} + const esbuildOptions = environment.config.esbuild || {} const result = await transform(code, { loader: 'js', @@ -180,7 +180,10 @@ export async function replaceDefine( platform: 'neutral', define, sourcefile: id, - sourcemap: config.command === 'build' ? !!config.build.sourcemap : true, + sourcemap: + environment.config.command === 'build' + ? !!environment.options.build.sourcemap + : true, }) // remove esbuild's source entries diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index f386f5b6e05c44..508b06b95fa4c5 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -328,6 +328,12 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { async transform(html, id) { if (id.endsWith('.html')) { + const { environment } = this + if (!environment) { + return + } + const { modulePreload } = environment.options.build + id = normalizePath(id) const relativeUrlPath = path.posix.relative(config.root, id) const publicPath = `/${relativeUrlPath}` @@ -673,7 +679,6 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { processedHtml.set(id, s.toString()) // inject module preload polyfill only when configured and needed - const { modulePreload } = config.build if ( modulePreload !== false && modulePreload.polyfill && @@ -689,6 +694,12 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { }, async generateBundle(options, bundle) { + const { environment } = this + if (!environment) { + return + } + const { modulePreload } = environment.options.build + const analyzedChunk: Map = new Map() const inlineEntryChunk = new Set() const getImportedChunks = ( @@ -837,7 +848,6 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { ) } else { assetTags = [toScriptTag(chunk, toOutputAssetFilePath, isAsync)] - const { modulePreload } = config.build if (modulePreload !== false) { const resolveDependencies = typeof modulePreload === 'object' && From 30b2da7801e8489c3a2ade812f12d2749b0e0516 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Mon, 29 Apr 2024 21:15:52 +0200 Subject: [PATCH 037/123] docs: environment.config -> environment.options --- docs/guide/api-vite-environment.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/guide/api-vite-environment.md b/docs/guide/api-vite-environment.md index 4e2815807e6a8d..5315fc8190c33d 100644 --- a/docs/guide/api-vite-environment.md +++ b/docs/guide/api-vite-environment.md @@ -477,7 +477,7 @@ A plugin could use the `environment` instance to: ```ts transform(code, id) { - console.log(this.enviroment.config.resolve.conditions) + console.log(this.enviroment.options.resolve.conditions) } ``` @@ -499,9 +499,9 @@ While the `config` hook is running, the complete list of environments isn't yet Plugins should set default values using the `config` hook. To configure each environment, they can use the new `configEnvironment` hook. This hook is called for each environment with its partially resolved config including resolution of final defaults. ```ts - configEnvironment(name: string, config: EnvironmentConfig) { + configEnvironment(name: string, options: EnvironmentOptions) { if (name === 'rsc') { - config.resolve.conditions = // ... + options.resolve.conditions = // ... ``` ### The `hotUpdate` hook From 527621e72e007e8b1dd0c7381c5c91e875b72120 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Mon, 29 Apr 2024 21:41:44 +0200 Subject: [PATCH 038/123] refactor: environment hooks guard --- packages/vite/src/node/plugins/asset.ts | 9 +++------ packages/vite/src/node/plugins/clientInjections.ts | 9 +++------ packages/vite/src/node/plugins/css.ts | 7 ++----- packages/vite/src/node/plugins/define.ts | 11 ++++------- packages/vite/src/node/plugins/html.ts | 14 ++++---------- packages/vite/src/node/plugins/importAnalysis.ts | 12 ++++-------- .../vite/src/node/plugins/importAnalysisBuild.ts | 12 ++++++------ packages/vite/src/node/plugins/loadFallback.ts | 6 ++---- packages/vite/src/node/plugins/manifest.ts | 11 ++++------- 9 files changed, 32 insertions(+), 59 deletions(-) diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index 36a6b510b66a51..bf36130d8580ca 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -144,12 +144,9 @@ export function assetPlugin(config: ResolvedConfig): Plugin { name: 'vite:asset', buildStart() { - const { environment } = this - if (!environment) { - return - } - assetCache.set(environment, new Map()) - generatedAssetsMap.set(environment, new Map()) + if (!this.environment) return + assetCache.set(this.environment, new Map()) + generatedAssetsMap.set(this.environment, new Map()) }, resolveId(id) { diff --git a/packages/vite/src/node/plugins/clientInjections.ts b/packages/vite/src/node/plugins/clientInjections.ts index c0cd54a36931ee..b874b27d9944dd 100644 --- a/packages/vite/src/node/plugins/clientInjections.ts +++ b/packages/vite/src/node/plugins/clientInjections.ts @@ -90,12 +90,9 @@ export function clientInjectionsPlugin(config: ResolvedConfig): Plugin { } }, async transform(code, id) { - const { environment } = this - if (!environment) { - return - } + if (!this.environment) return // TODO: !environment.options.nodeCompatible ? - const ssr = environment.name !== 'client' + const ssr = this.environment.name !== 'client' if (id === normalizedClientEntry || id === normalizedEnvEntry) { return injectConfigValues(code) } else if (!ssr && code.includes('process.env.NODE_ENV')) { @@ -105,7 +102,7 @@ export function clientInjectionsPlugin(config: ResolvedConfig): Plugin { const nodeEnv = config.define?.['process.env.NODE_ENV'] || JSON.stringify(process.env.NODE_ENV || config.mode) - return await replaceDefine(environment, code, id, { + return await replaceDefine(this.environment, code, id, { 'process.env.NODE_ENV': nodeEnv, 'global.process.env.NODE_ENV': nodeEnv, 'globalThis.process.env.NODE_ENV': nodeEnv, diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 38556c154a9397..114cc9f092f083 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -556,11 +556,8 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { }, async renderChunk(code, chunk, opts) { - const { environment } = this - if (!environment) { - return - } - const generatedAssets = generatedAssetsMap.get(environment)! + if (!this.environment) return + const generatedAssets = generatedAssetsMap.get(this.environment)! let chunkCSS = '' let isPureCssChunk = true diff --git a/packages/vite/src/node/plugins/define.ts b/packages/vite/src/node/plugins/define.ts index 5257a754886ae6..284a7253abd2bc 100644 --- a/packages/vite/src/node/plugins/define.ts +++ b/packages/vite/src/node/plugins/define.ts @@ -119,12 +119,9 @@ export function definePlugin(config: ResolvedConfig): Plugin { name: 'vite:define', async transform(code, id) { - const { environment } = this - if (!environment) { - return - } + if (!this.environment) return - if (environment.name === 'client' && !isBuild) { + if (this.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. @@ -141,14 +138,14 @@ export function definePlugin(config: ResolvedConfig): Plugin { return } - const [define, pattern] = getPattern(environment) + const [define, pattern] = getPattern(this.environment) if (!pattern) return // Check if our code needs any replacements before running esbuild pattern.lastIndex = 0 if (!pattern.test(code)) return - return await replaceDefine(environment, code, id, define) + return await replaceDefine(this.environment, code, id, define) }, } } diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index 508b06b95fa4c5..c0781fa8fa6a62 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -328,11 +328,8 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { async transform(html, id) { if (id.endsWith('.html')) { - const { environment } = this - if (!environment) { - return - } - const { modulePreload } = environment.options.build + if (!this.environment) return + const { modulePreload } = this.environment.options.build id = normalizePath(id) const relativeUrlPath = path.posix.relative(config.root, id) @@ -694,11 +691,8 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { }, async generateBundle(options, bundle) { - const { environment } = this - if (!environment) { - return - } - const { modulePreload } = environment.options.build + if (!this.environment) return + const { modulePreload } = this.environment.options.build const analyzedChunk: Map = new Map() const inlineEntryChunk = new Set() diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index b6e71d2c235510..e65b240940ca21 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -204,14 +204,10 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { return { name: 'vite:import-analysis', - async transform(source, importer, options) { - const ssr = options?.ssr === true - - const environment = this.environment as DevEnvironment | undefined - if (!environment) { - return - } - + async transform(source, importer) { + if (!this.environment) return + const environment = this.environment as DevEnvironment + const ssr = environment.name !== 'client' // TODO const moduleGraph = environment.moduleGraph if (canSkipImportAnalysis(importer)) { diff --git a/packages/vite/src/node/plugins/importAnalysisBuild.ts b/packages/vite/src/node/plugins/importAnalysisBuild.ts index af6244804e8a4c..a30ef2cfb80201 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -332,15 +332,15 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { }, generateBundle({ format }, bundle) { - const { environment } = this - const ssr = environment?.options.build.ssr - if (!environment || format !== 'es' || ssr || isWorker) { + if (!this.environment) return + const ssr = this.environment.name !== 'client' // TODO + if (format !== 'es' || ssr || isWorker) { return } - const buildSourcemap = environment.options.build.sourcemap - const { modulePreload } = environment.options.build + const buildSourcemap = this.environment.options.build.sourcemap + const { modulePreload } = this.environment.options.build const { customModulePreloadPaths, optimizeModulePreloadRelativePaths } = - getModulePreloadData(environment) + getModulePreloadData(this.environment) for (const file in bundle) { const chunk = bundle[file] diff --git a/packages/vite/src/node/plugins/loadFallback.ts b/packages/vite/src/node/plugins/loadFallback.ts index 8b289944ac378e..761ef1846edadd 100644 --- a/packages/vite/src/node/plugins/loadFallback.ts +++ b/packages/vite/src/node/plugins/loadFallback.ts @@ -23,10 +23,8 @@ export function loadFallbackPlugin(config: ResolvedConfig): Plugin { return { name: 'vite:load-fallback', async load(id, options) { + if (!this.environment) return const environment = this.environment as DevEnvironment - if (!environment) { - return - } let code: string | null = null let map: SourceMap | null = null @@ -43,7 +41,7 @@ export function loadFallbackPlugin(config: ResolvedConfig): Plugin { // like /service-worker.js or /api/users const file = cleanUrl(id) if ( - environment.options.nodeCompatible || + this.environment.options.nodeCompatible || isFileLoadingAllowed(config, file) // Do we need fsPathFromId here? ) { try { diff --git a/packages/vite/src/node/plugins/manifest.ts b/packages/vite/src/node/plugins/manifest.ts index fe92c8df193b47..71227d2d64de59 100644 --- a/packages/vite/src/node/plugins/manifest.ts +++ b/packages/vite/src/node/plugins/manifest.ts @@ -39,12 +39,9 @@ export function manifestPlugin(): Plugin { }, generateBundle({ format }, bundle) { - const { environment } = this - if (!environment) { - return - } - const { root } = environment.config - const buildOptions = environment.options.build + if (!this.environment) return + const { root } = this.environment.config + const buildOptions = this.environment.options.build function getChunkName(chunk: OutputChunk) { return getChunkOriginalFileName(chunk, root, format) @@ -118,7 +115,7 @@ export function manifestPlugin(): Plugin { } const fileNameToAssetMeta = new Map() - const assets = generatedAssetsMap.get(environment)! + const assets = generatedAssetsMap.get(this.environment)! assets.forEach((asset, referenceId) => { try { const fileName = this.getFileName(referenceId) From 12d467f642a0d71fcc718d036d20dc4b6efda549 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Tue, 30 Apr 2024 09:53:22 +0200 Subject: [PATCH 039/123] fix: keep plugins with create hook --- packages/vite/src/node/plugin.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/vite/src/node/plugin.ts b/packages/vite/src/node/plugin.ts index 98297653deb96d..9635f6c29a54c4 100644 --- a/packages/vite/src/node/plugin.ts +++ b/packages/vite/src/node/plugin.ts @@ -357,6 +357,7 @@ export async function resolveBoundedPlugins( ): Promise { const resolvedPlugins: BoundedPlugin[] = [] for (const plugin of environment.config.plugins) { + resolvedPlugins.push(plugin) if (plugin.create) { const boundedPlugin = await plugin.create(environment) if (boundedPlugin) { @@ -366,8 +367,6 @@ export async function resolveBoundedPlugins( ) resolvedPlugins.push(...flatPlugins) } - } else { - resolvedPlugins.push(plugin) } } return resolvedPlugins From 1d9caefe5a54c00a3245cfc90179309288422ef3 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Tue, 30 Apr 2024 09:55:36 +0200 Subject: [PATCH 040/123] release: v6.0.0-alpha.8 --- packages/vite/CHANGELOG.md | 8 ++++++++ packages/vite/package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/vite/CHANGELOG.md b/packages/vite/CHANGELOG.md index 119220c78c9f89..955d20ae74ca6a 100644 --- a/packages/vite/CHANGELOG.md +++ b/packages/vite/CHANGELOG.md @@ -1,3 +1,11 @@ +## 6.0.0-alpha.8 (2024-04-30) + +* fix: keep plugins with create hook ([12d467f](https://github.com/vitejs/vite/commit/12d467f)) +* refactor: environment hooks guard ([527621e](https://github.com/vitejs/vite/commit/527621e)) +* feat: define and html plugins ([7566aae](https://github.com/vitejs/vite/commit/7566aae)) + + + ## 6.0.0-alpha.7 (2024-04-29) * feat: remove config.build from dynamicImportVars plugin ([8231283](https://github.com/vitejs/vite/commit/8231283)) diff --git a/packages/vite/package.json b/packages/vite/package.json index 0d23a6e0d39559..550a9471dc4bfe 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -1,6 +1,6 @@ { "name": "vite", - "version": "6.0.0-alpha.7", + "version": "6.0.0-alpha.8", "type": "module", "license": "MIT", "author": "Evan You", From f6dce1062e21336f49deb3dd804c535d3dfa02cc Mon Sep 17 00:00:00 2001 From: patak <583075+patak-dev@users.noreply.github.com> Date: Wed, 1 May 2024 15:51:42 +0200 Subject: [PATCH 041/123] chore: update Co-authored-by: Dario Piotrowicz --- docs/guide/api-vite-environment.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/api-vite-environment.md b/docs/guide/api-vite-environment.md index 5315fc8190c33d..6dd9fcfd102a28 100644 --- a/docs/guide/api-vite-environment.md +++ b/docs/guide/api-vite-environment.md @@ -603,7 +603,7 @@ function perEnvironmentPlugin() { ## `ModuleRunner` -A module runner is instantiated in the target runtime. All APIs in the next section are imported from `vite/module-runner` unless stated otherwise. This export entry point is kept as lightweight as possible, only exporting the minimal needed to create runners in the +A module runner is instantiated in the target runtime. All APIs in the next section are imported from `vite/module-runner` unless stated otherwise. This export entry point is kept as lightweight as possible, only exporting the minimal needed to create module runners. **Type Signature:** From 1bcb67dee22e624182fa6f360d1e9ff53e3d7741 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Wed, 1 May 2024 20:30:21 +0200 Subject: [PATCH 042/123] refactor: move reload on tsconfig change to the server --- packages/vite/src/node/plugins/esbuild.ts | 62 +++++++++++------------ packages/vite/src/node/server/index.ts | 5 ++ 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/packages/vite/src/node/plugins/esbuild.ts b/packages/vite/src/node/plugins/esbuild.ts index 0107a7a30d5b50..8e69a4b937fef8 100644 --- a/packages/vite/src/node/plugins/esbuild.ts +++ b/packages/vite/src/node/plugins/esbuild.ts @@ -7,6 +7,7 @@ import type { TransformResult, } from 'esbuild' import { transform } from 'esbuild' +import type { FSWatcher } from 'chokidar' import type { RawSourceMap } from '@ampproject/remapping' import type { InternalModuleFormat, SourceMap } from 'rollup' import type { TSConfckParseResult } from 'tsconfck' @@ -41,8 +42,6 @@ export const defaultEsbuildSupported = { 'import-meta': true, } -let server: ViteDevServer - export interface ESBuildOptions extends TransformOptions { include?: string | RegExp | string[] | RegExp[] exclude?: string | RegExp | string[] | RegExp[] @@ -81,6 +80,8 @@ export async function transformWithEsbuild( filename: string, options?: TransformOptions, inMap?: object, + root?: string, + watcher?: FSWatcher, ): Promise { let loader = options?.loader @@ -121,7 +122,11 @@ export async function transformWithEsbuild( ] const compilerOptionsForFile: TSCompilerOptions = {} if (loader === 'ts' || loader === 'tsx') { - const loadedTsconfig = await loadTsconfigJsonForFile(filename) + const loadedTsconfig = await loadTsconfigJsonForFile( + filename, + root, + watcher, + ) const loadedCompilerOptions = loadedTsconfig.compilerOptions ?? {} for (const field of meaningfulFields) { @@ -251,19 +256,6 @@ 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 - .on('add', reloadOnTsconfigChange) - .on('change', reloadOnTsconfigChange) - .on('unlink', reloadOnTsconfigChange) - }, - buildEnd() { - // recycle serve to avoid preventing Node self-exit (#6815) - server = null as any - }, async transform(code, id) { if (filter(id) || filter(cleanUrl(id))) { const result = await transformWithEsbuild(code, id, transformOptions) @@ -452,6 +444,8 @@ let tsconfckCache: TSConfckCache | undefined export async function loadTsconfigJsonForFile( filename: string, + root?: string, + watcher?: FSWatcher, ): Promise { try { if (!tsconfckCache) { @@ -462,30 +456,35 @@ export async function loadTsconfigJsonForFile( ignoreNodeModules: true, }) // tsconfig could be out of root, make sure it is watched on dev - if (server && result.tsconfigFile) { - ensureWatchedFile(server.watcher, result.tsconfigFile, server.config.root) + if (root && watcher && result.tsconfigFile) { + ensureWatchedFile(watcher, result.tsconfigFile, root) } return result.tsconfig } catch (e) { if (e instanceof TSConfckParseError) { // tsconfig could be out of root, make sure it is watched on dev - if (server && e.tsconfigFile) { - ensureWatchedFile(server.watcher, e.tsconfigFile, server.config.root) + if (root && watcher && e.tsconfigFile) { + ensureWatchedFile(watcher, e.tsconfigFile, root) } } throw e } } -async function reloadOnTsconfigChange(changedFile: string) { - // server could be closed externally after a file change is detected - if (!server) return +export async function reloadOnTsconfigChange( + server: ViteDevServer, + changedFile: string, +): Promise { // any tsconfig.json that's added in the workspace could be closer to a code file than a previously cached one // any json file in the tsconfig cache could have been used to compile ts if ( path.basename(changedFile) === 'tsconfig.json' || - (changedFile.endsWith('.json') && - tsconfckCache?.hasParseResult(changedFile)) + changedFile.endsWith('.json') /* + TODO: the tsconfckCache?.clear() line will make this fail if there are several servers + we may need a cache per server if we don't want all servers to share the reset + leaving it commented for now because it should still work + && tsconfckCache?.hasParseResult(changedFile) + */ ) { server.config.logger.info( `changed tsconfig file detected: ${changedFile} - Clearing cache and forcing full-reload to ensure TypeScript is compiled with updated config values.`, @@ -500,13 +499,10 @@ async function reloadOnTsconfigChange(changedFile: string) { // reset tsconfck so that recompile works with up2date configs tsconfckCache?.clear() - // server may not be available if vite config is updated at the same time - if (server) { - // force full reload - server.hot.send({ - type: 'full-reload', - path: '*', - }) - } + // force full reload + server.hot.send({ + type: 'full-reload', + path: '*', + }) } } diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index d35c767ccfa365..8f8a66022df113 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -39,6 +39,7 @@ 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 { reloadOnTsconfigChange } from '../plugins/esbuild' import { bindCLIShortcuts } from '../shortcuts' import type { BindCLIShortcutsOptions } from '../shortcuts' import { CLIENT_DIR, DEFAULT_DEV_PORT } from '../constants' @@ -779,6 +780,8 @@ export async function _createServer( const onFileAddUnlink = async (file: string, isUnlink: boolean) => { file = normalizePath(file) + reloadOnTsconfigChange(server, file) + await pluginContainer.watchChange(file, { event: isUnlink ? 'delete' : 'create', }) @@ -811,6 +814,8 @@ export async function _createServer( watcher.on('change', async (file) => { file = normalizePath(file) + reloadOnTsconfigChange(server, file) + await pluginContainer.watchChange(file, { event: 'update' }) // invalidate module graph cache on file change for (const environment of Object.values(server.environments)) { From 6b74221266aed7bb413fc2eca6f24c08accb9a73 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Wed, 1 May 2024 21:30:01 +0200 Subject: [PATCH 043/123] refactor: bounded plugins factory instead of create hook --- docs/guide/api-vite-environment.md | 19 ++++---- packages/vite/src/node/build.ts | 17 ++++--- packages/vite/src/node/config.ts | 55 +++++++++++++++-------- packages/vite/src/node/index.ts | 7 ++- packages/vite/src/node/plugin.ts | 60 ++++++++++++------------- packages/vite/src/node/plugins/index.ts | 41 ++++++++++++----- 6 files changed, 123 insertions(+), 76 deletions(-) diff --git a/docs/guide/api-vite-environment.md b/docs/guide/api-vite-environment.md index 6dd9fcfd102a28..7d74ba941d74c0 100644 --- a/docs/guide/api-vite-environment.md +++ b/docs/guide/api-vite-environment.md @@ -581,22 +581,19 @@ The hook can choose to: ### Per-environment Plugins -There is a new `create` hook that plugins can define to lazily create per-environment plugins. +A plugin can now also be a constructor to lazily create per-environment plugins. ```js function perEnvironmentPlugin() { - return { - name: 'per-environment-plugin', + return (environment: Environment) => { // Return a plugin, an array, a Promise, or a falsy value for each environment - create(environment: Environment) { - if (!passesCondition(environment)) { - return undefined - } - return [ - createEnvironmentPlugin(environment), - otherPlugin(environment) - ] + if (!passesCondition(environment)) { + return undefined } + return [ + createEnvironmentPlugin(environment), + otherPlugin(environment) + ] } } ``` diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 43364268a9b2a8..84fcad12c0f4b6 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -66,7 +66,7 @@ import { mergeConfig } from './publicUtils' import { webWorkerPostPlugin } from './plugins/worker' import { getHookHandler } from './plugins' import { Environment } from './environment' -import type { Plugin, PluginContext } from './plugin' +import type { BoundedPluginConstructor, Plugin, PluginContext } from './plugin' export interface BuildEnvironmentOptions { /** @@ -534,7 +534,7 @@ export async function build( function resolveConfigToBuild( inlineConfig: InlineConfig = {}, patchConfig?: (config: ResolvedConfig) => void, - patchPlugins?: (plugins: Plugin[]) => void, + patchPlugins?: (rawPlugins: (Plugin | BoundedPluginConstructor)[]) => void, ) { return resolveConfig( inlineConfig, @@ -1525,13 +1525,18 @@ export async function createBuilder( lib: false, } } - const patchPlugins = (resolvedPlugins: Plugin[]) => { + const patchPlugins = ( + rawPlugins: (Plugin | BoundedPluginConstructor)[], + ) => { // Force opt-in shared plugins - const environmentPlugins = [...resolvedPlugins] + const environmentPlugins = [...rawPlugins] let validMixedPlugins = true for (let i = 0; i < environmentPlugins.length; i++) { const environmentPlugin = environmentPlugins[i] - const sharedPlugin = config.plugins[i] + if (typeof environmentPlugin === 'function') { + continue + } + const sharedPlugin = config.rawPlugins[i] if ( config.builder.sharedPlugins || environmentPlugin.sharedDuringBuild @@ -1545,7 +1550,7 @@ export async function createBuilder( } if (validMixedPlugins) { for (let i = 0; i < environmentPlugins.length; i++) { - resolvedPlugins[i] = environmentPlugins[i] + rawPlugins[i] = environmentPlugins[i] } } } diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 57ef0c356669f4..57fec1ab79208a 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -21,6 +21,7 @@ import { FS_PREFIX, } from './constants' import type { + BoundedPluginConstructor, HookHandler, Plugin, PluginEnvironment, @@ -71,9 +72,9 @@ import { import { getFsUtils } from './fsUtils' import { createPluginHookUtils, + createResolvePlugins, getHookHandler, getSortedPluginsByHook, - resolvePlugins, } from './plugins' import type { ESBuildOptions } from './plugins/esbuild' import type { InternalResolveOptions, ResolveOptions } from './plugins/resolve' @@ -538,6 +539,13 @@ export type ResolvedConfig = Readonly< alias: Alias[] } plugins: readonly Plugin[] + rawPlugins: readonly (Plugin | BoundedPluginConstructor)[] + /** @internal inject user plugins into the shared vite pipeline */ + resolvePlugins: ( + prePlugins: Plugin[], + normalPlugins: Plugin[], + postPlugins: Plugin[], + ) => Plugin[] css: ResolvedCSSOptions esbuild: ESBuildOptions | false server: ResolvedServerOptions @@ -761,7 +769,9 @@ export async function resolveConfig( defaultNodeEnv = 'development', isPreview = false, patchConfig: ((config: ResolvedConfig) => void) | undefined = undefined, - patchPlugins: ((plugins: Plugin[]) => void) | undefined = undefined, + patchPlugins: + | ((plugins: (Plugin | BoundedPluginConstructor)[]) => void) + | undefined = undefined, ): Promise { let config = inlineConfig let configFileDependencies: string[] = [] @@ -802,10 +812,10 @@ export async function resolveConfig( mode = inlineConfig.mode || config.mode || mode configEnv.mode = mode - const filterPlugin = (p: Plugin) => { + const filterPlugin = (p: Plugin | BoundedPluginConstructor) => { if (!p) { return false - } else if (!p.apply) { + } else if (typeof p === 'function' || !p.apply) { return true } else if (typeof p.apply === 'function') { return p.apply({ ...config, mode }, configEnv) @@ -815,12 +825,22 @@ export async function resolveConfig( } // resolve plugins - const rawUserPlugins = ( - (await asyncFlatten(config.plugins || [])) as Plugin[] + const rawPlugins = ( + (await asyncFlatten(config.plugins || [])) as ( + | Plugin + | BoundedPluginConstructor + )[] ).filter(filterPlugin) + // Backward compatibility hook used in builder, opt-in to shared plugins during build + patchPlugins?.(rawPlugins) + + const sharedPlugins = rawPlugins.filter( + (plugin) => typeof plugin !== 'function', + ) as Plugin[] + const [prePlugins, normalPlugins, postPlugins] = - sortUserPlugins(rawUserPlugins) + sortUserPlugins(sharedPlugins) const isBuild = command === 'build' @@ -1119,8 +1139,8 @@ export async function resolveConfig( mainConfig: resolved, bundleChain, } - const resolvedWorkerPlugins = await resolvePlugins( - workerResolved, + const resolveWorkerPlugins = await createResolvePlugins(workerResolved) + const resolvedWorkerPlugins = resolveWorkerPlugins( workerPrePlugins, workerNormalPlugins, workerPostPlugins, @@ -1159,7 +1179,9 @@ export async function resolveConfig( mainConfig: null, bundleChain: [], isProduction, - plugins: userPlugins, + plugins: userPlugins, // placeholder to be replaced + rawPlugins, + resolvePlugins: null as any, // placeholder to be replaced css: resolveCSSOptions(config.css), esbuild: config.esbuild === false @@ -1296,15 +1318,12 @@ export async function resolveConfig( // gets called patchConfig?.(resolved) - const resolvedPlugins = await resolvePlugins( - resolved, - prePlugins, - normalPlugins, - postPlugins, - ) + const resolvePlugins = await createResolvePlugins(resolved) + + ;(resolved.resolvePlugins as any) = resolvePlugins + + const resolvedPlugins = resolvePlugins(prePlugins, normalPlugins, postPlugins) - // Backward compatibility hook used in builder - patchPlugins?.(resolvedPlugins) ;(resolved.plugins as Plugin[]) = resolvedPlugins Object.assign(resolved, createPluginHookUtils(resolved.plugins)) diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index 7e5fc8c1b5647d..197b787c4d7be3 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -94,7 +94,12 @@ export type { SSROptions, SSRTarget, } from './ssr' -export type { Plugin, HookHandler } from './plugin' +export type { + BoundedPlugin, + BoundedPluginConstructor, + Plugin, + HookHandler, +} from './plugin' export type { Logger, LogOptions, diff --git a/packages/vite/src/node/plugin.ts b/packages/vite/src/node/plugin.ts index 9635f6c29a54c4..ebb5a8737eed50 100644 --- a/packages/vite/src/node/plugin.ts +++ b/packages/vite/src/node/plugin.ts @@ -14,6 +14,7 @@ import type { ResolvedConfig, UserConfig, } from './config' +import { sortUserPlugins } from './config' import type { ServerHook, ViteDevServer } from './server' import type { IndexHtmlTransform } from './plugins/html' import type { EnvironmentModuleNode } from './server/moduleGraph' @@ -104,6 +105,20 @@ type ModifyHookContext = Hook extends { : ModifyFunctionContext export interface BasePlugin extends RollupPlugin { + /** + * Enforce plugin invocation tier similar to webpack loaders. Hooks ordering + * is still subject to the `order` property in the hook object. + * + * Plugin invocation order: + * - alias resolution + * - `enforce: 'pre'` plugins + * - vite core plugins + * - normal plugins + * - vite build plugins + * - `enforce: 'post'` plugins + * - vite build post plugins + */ + enforce?: 'pre' | 'post' /** * Perform custom handling of HMR updates. * The handler receives a context containing changed filename, timestamp, a @@ -203,27 +218,6 @@ export interface Plugin extends BasePlugin { * @experimental */ sharedDuringBuild?: boolean - /** - * Spawn 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. - */ - create?: (environment: PluginEnvironment) => BoundedPluginOption - /** - * Enforce plugin invocation tier similar to webpack loaders. Hooks ordering - * is still subject to the `order` property in the hook object. - * - * Plugin invocation order: - * - alias resolution - * - `enforce: 'pre'` plugins - * - vite core plugins - * - normal plugins - * - vite build plugins - * - `enforce: 'post'` plugins - * - vite build post plugins - */ - enforce?: 'pre' | 'post' /** * Apply the plugin only for serve or build, or on certain conditions. */ @@ -345,7 +339,12 @@ export type BoundedPluginOption = | BoundedPluginOption[] | Promise -export type MaybePlugin = Plugin | false | null | undefined +export type MaybePlugin = + | Plugin + | BoundedPluginConstructor + | false + | null + | undefined export type PluginOption = | MaybePlugin @@ -355,21 +354,22 @@ export type PluginOption = export async function resolveBoundedPlugins( environment: PluginEnvironment, ): Promise { - const resolvedPlugins: BoundedPlugin[] = [] - for (const plugin of environment.config.plugins) { - resolvedPlugins.push(plugin) - if (plugin.create) { - const boundedPlugin = await plugin.create(environment) + const userPlugins: BoundedPlugin[] = [] + for (const plugin of environment.config.rawPlugins) { + if (typeof plugin === 'function') { + const boundedPlugin = await plugin(environment) if (boundedPlugin) { const flatPlugins = await asyncFlattenBoundedPlugin( environment, boundedPlugin, ) - resolvedPlugins.push(...flatPlugins) + userPlugins.push(...flatPlugins) } + } else { + userPlugins.push(plugin) } } - return resolvedPlugins + return environment.config.resolvePlugins(...sortUserPlugins(userPlugins)) } async function asyncFlattenBoundedPlugin( @@ -382,7 +382,7 @@ async function asyncFlattenBoundedPlugin( do { plugins = ( await Promise.all( - plugins.map((p: any) => (p && p.split ? p.split(environment) : p)), + plugins.map((p: any) => (typeof p === 'function' ? p(environment) : p)), ) ) .flat(Infinity) diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index 1a4a8f68792694..0cfb7091182606 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -26,12 +26,15 @@ import { dynamicImportVarsPlugin } from './dynamicImportVars' import { importGlobPlugin } from './importMetaGlob' // TODO: import { loadFallbackPlugin } from './loadFallback' -export async function resolvePlugins( +export async function createResolvePlugins( config: ResolvedConfig, - prePlugins: Plugin[], - normalPlugins: Plugin[], - postPlugins: Plugin[], -): Promise { +): Promise< + ( + prePlugins: Plugin[], + normalPlugins: Plugin[], + postPlugins: Plugin[], + ) => Plugin[] +> { const isBuild = config.command === 'build' const isWorker = config.isWorker const buildPlugins = isBuild @@ -42,7 +45,8 @@ export async function resolvePlugins( !isBuild && (isDepsOptimizerEnabled(config, false) || isDepsOptimizerEnabled(config, true)) - return [ + + const preVitePlugins = [ depsOptimizerEnabled ? optimizedDepsPlugin(config) : null, isBuild ? metadataPlugin() : null, !isWorker ? watchPackageDataPlugin(config.packageCache) : null, @@ -51,7 +55,9 @@ export async function resolvePlugins( entries: config.resolve.alias, customResolver: viteAliasCustomResolver, }), - ...prePlugins, + ] + // then ...prePlugins + const normalVitePlugins = [ modulePreload !== false && modulePreload.polyfill ? modulePreloadPolyfillPlugin(config) : null, @@ -81,7 +87,9 @@ export async function resolvePlugins( wasmHelperPlugin(config), webWorkerPlugin(config), assetPlugin(config), - ...normalPlugins, + ] + // then ...normalPlugins + const postVitePlugins = [ wasmFallbackPlugin(), definePlugin(config), cssPostPlugin(config), @@ -91,7 +99,9 @@ export async function resolvePlugins( ...buildPlugins.pre, dynamicImportVarsPlugin(config), importGlobPlugin(config), - ...postPlugins, + ] + // then ...postVitePlugins + const finalVitePlugins = [ ...buildPlugins.post, // internal server-only plugins are always applied after everything else ...(isBuild @@ -102,7 +112,18 @@ export async function resolvePlugins( importAnalysisPlugin(config), // TODO: loadFallbackPlugin(config), ]), - ].filter(Boolean) as Plugin[] + ] + return (prePlugins, normalPlugins, postPlugins) => { + return [ + ...preVitePlugins, + ...prePlugins, + ...normalVitePlugins, + ...normalPlugins, + ...postVitePlugins, + ...postPlugins, + ...finalVitePlugins, + ].filter(Boolean) as Plugin[] + } } export function createPluginHookUtils( From b8ef8ffe9444ddd455f8066c2cb867c2f9865b5f Mon Sep 17 00:00:00 2001 From: patak-dev Date: Wed, 1 May 2024 21:35:03 +0200 Subject: [PATCH 044/123] fix: module-runner bundle issue with chokidar dep --- packages/vite/src/node/plugins/esbuild.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/vite/src/node/plugins/esbuild.ts b/packages/vite/src/node/plugins/esbuild.ts index 8e69a4b937fef8..cf368094e4ed01 100644 --- a/packages/vite/src/node/plugins/esbuild.ts +++ b/packages/vite/src/node/plugins/esbuild.ts @@ -7,7 +7,7 @@ import type { TransformResult, } from 'esbuild' import { transform } from 'esbuild' -import type { FSWatcher } from 'chokidar' +// TODO: import type { FSWatcher } from 'chokidar' import type { RawSourceMap } from '@ampproject/remapping' import type { InternalModuleFormat, SourceMap } from 'rollup' import type { TSConfckParseResult } from 'tsconfck' @@ -81,7 +81,7 @@ export async function transformWithEsbuild( options?: TransformOptions, inMap?: object, root?: string, - watcher?: FSWatcher, + watcher?: any, // TODO: module-runner bundling issue with FSWatcher, ): Promise { let loader = options?.loader @@ -445,7 +445,7 @@ let tsconfckCache: TSConfckCache | undefined export async function loadTsconfigJsonForFile( filename: string, root?: string, - watcher?: FSWatcher, + watcher?: any, // TODO: module-runner issue with FSWatcher, ): Promise { try { if (!tsconfckCache) { From a09353ec43b179e7cd7d3bac3d340f815f3f6aa6 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Wed, 1 May 2024 21:44:21 +0200 Subject: [PATCH 045/123] chore: fix lock --- pnpm-lock.yaml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1f3c63d1143bdc..8aeb814ecc8dab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -690,10 +690,10 @@ importers: version: 18.2.23 react: specifier: ^18.2.0 - version: 18.2.0 + version: 18.3.1 react-dom: specifier: ^18.2.0 - version: 18.2.0(react@18.2.0) + version: 18.3.1(react@18.3.1) playground/extensions: dependencies: @@ -9237,14 +9237,8 @@ packages: react: ^18.3.1 dependencies: loose-envify: 1.4.0 -<<<<<<< HEAD - react: 18.2.0 - scheduler: 0.23.0 -======= react: 18.3.1 scheduler: 0.23.2 - dev: false ->>>>>>> origin/main /react-is@18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} From 848af54aa72b9f0badc59599630140c13de68d09 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Wed, 1 May 2024 21:56:17 +0200 Subject: [PATCH 046/123] fix: lint --- packages/vite/index.cjs | 1 - packages/vite/src/node/ssr/runtime/__tests__/package.json | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/vite/index.cjs b/packages/vite/index.cjs index ef2a182045698e..7e5fec44e90637 100644 --- a/packages/vite/index.cjs +++ b/packages/vite/index.cjs @@ -6,7 +6,6 @@ warnCjsUsage() module.exports.defineConfig = (config) => config // proxy cjs utils (sync functions) -// eslint-disable-next-line n/no-missing-require -- will be generated by build Object.assign(module.exports, require('./dist/node-cjs/publicUtils.cjs')) // async functions, can be redirect from ESM build diff --git a/packages/vite/src/node/ssr/runtime/__tests__/package.json b/packages/vite/src/node/ssr/runtime/__tests__/package.json index 89fe86abc39d19..40a971f043f8a9 100644 --- a/packages/vite/src/node/ssr/runtime/__tests__/package.json +++ b/packages/vite/src/node/ssr/runtime/__tests__/package.json @@ -2,6 +2,9 @@ "name": "@vitejs/unit-runtime", "private": true, "version": "0.0.0", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, "dependencies": { "@vitejs/cjs-external": "link:./fixtures/cjs-external", "@vitejs/esm-external": "link:./fixtures/esm-external", From 2e6abb34cf1449207079d37a0d6d281ad67e3ba0 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Wed, 1 May 2024 22:19:05 +0200 Subject: [PATCH 047/123] release: v6.0.0-alpha.9 --- packages/vite/CHANGELOG.md | 14 ++++++++++++++ packages/vite/package.json | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/vite/CHANGELOG.md b/packages/vite/CHANGELOG.md index 955d20ae74ca6a..68e23c4775b16c 100644 --- a/packages/vite/CHANGELOG.md +++ b/packages/vite/CHANGELOG.md @@ -1,3 +1,17 @@ +## 6.0.0-alpha.9 (2024-05-01) + +* fix: lint ([848af54](https://github.com/vitejs/vite/commit/848af54)) +* fix: module-runner bundle issue with chokidar dep ([b8ef8ff](https://github.com/vitejs/vite/commit/b8ef8ff)) +* fix: scripts and styles were missing from built HTML on Windows (#16421) ([0e93f58](https://github.com/vitejs/vite/commit/0e93f58)), closes [#16421](https://github.com/vitejs/vite/issues/16421) +* fix(deps): update all non-major dependencies (#16549) ([2d6a13b](https://github.com/vitejs/vite/commit/2d6a13b)), closes [#16549](https://github.com/vitejs/vite/issues/16549) +* fix(preload): skip preload for non-static urls (#16556) ([bb79c9b](https://github.com/vitejs/vite/commit/bb79c9b)), closes [#16556](https://github.com/vitejs/vite/issues/16556) +* fix(ssr): handle class declaration and expression name scoping (#16569) ([c071eb3](https://github.com/vitejs/vite/commit/c071eb3)), closes [#16569](https://github.com/vitejs/vite/issues/16569) +* fix(ssr): handle function expression name scoping (#16563) ([02db947](https://github.com/vitejs/vite/commit/02db947)), closes [#16563](https://github.com/vitejs/vite/issues/16563) +* refactor: bounded plugins factory instead of create hook ([6b74221](https://github.com/vitejs/vite/commit/6b74221)) +* refactor: move reload on tsconfig change to the server ([1bcb67d](https://github.com/vitejs/vite/commit/1bcb67d)) + + + ## 6.0.0-alpha.8 (2024-04-30) * fix: keep plugins with create hook ([12d467f](https://github.com/vitejs/vite/commit/12d467f)) diff --git a/packages/vite/package.json b/packages/vite/package.json index dda53b609819e6..2ca09ace48d44c 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -1,6 +1,6 @@ { "name": "vite", - "version": "6.0.0-alpha.8", + "version": "6.0.0-alpha.9", "type": "module", "license": "MIT", "author": "Evan You", From d2138656258781fac70b59e8ed808aac5d9be884 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Thu, 2 May 2024 09:05:06 +0200 Subject: [PATCH 048/123] feat: sharedDuringBuild for bounded plugins --- packages/vite/src/node/build.ts | 3 --- packages/vite/src/node/plugin.ts | 7 ++++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 84fcad12c0f4b6..7bb83438dd7996 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -1533,9 +1533,6 @@ export async function createBuilder( let validMixedPlugins = true for (let i = 0; i < environmentPlugins.length; i++) { const environmentPlugin = environmentPlugins[i] - if (typeof environmentPlugin === 'function') { - continue - } const sharedPlugin = config.rawPlugins[i] if ( config.builder.sharedPlugins || diff --git a/packages/vite/src/node/plugin.ts b/packages/vite/src/node/plugin.ts index ebb5a8737eed50..faa7a097431d76 100644 --- a/packages/vite/src/node/plugin.ts +++ b/packages/vite/src/node/plugin.ts @@ -328,9 +328,10 @@ export type PluginWithRequiredHook = Plugin & { [P in K]: NonNullable } -export type BoundedPluginConstructor = ( - Environment: PluginEnvironment, -) => BoundedPluginOption +export type BoundedPluginConstructor = { + (environment: PluginEnvironment): BoundedPluginOption + sharedDuringBuild?: boolean +} export type MaybeBoundedPlugin = BoundedPlugin | false | null | undefined From 57871a4bdc7ef2a70d6483a9ec8ffd71fed6ae1d Mon Sep 17 00:00:00 2001 From: patak-dev Date: Thu, 2 May 2024 09:05:27 +0200 Subject: [PATCH 049/123] release: v6.0.0-alpha.10 --- packages/vite/CHANGELOG.md | 6 ++++++ packages/vite/package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/vite/CHANGELOG.md b/packages/vite/CHANGELOG.md index 68e23c4775b16c..4c03d4904a66fa 100644 --- a/packages/vite/CHANGELOG.md +++ b/packages/vite/CHANGELOG.md @@ -1,3 +1,9 @@ +## 6.0.0-alpha.10 (2024-05-02) + +* feat: sharedDuringBuild for bounded plugins ([d213865](https://github.com/vitejs/vite/commit/d213865)) + + + ## 6.0.0-alpha.9 (2024-05-01) * fix: lint ([848af54](https://github.com/vitejs/vite/commit/848af54)) diff --git a/packages/vite/package.json b/packages/vite/package.json index 2ca09ace48d44c..7e759b6fa830f9 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -1,6 +1,6 @@ { "name": "vite", - "version": "6.0.0-alpha.9", + "version": "6.0.0-alpha.10", "type": "module", "license": "MIT", "author": "Evan You", From 35d2d87dfa258e7326ee48cdb9051f9db5b30c55 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Thu, 2 May 2024 10:03:21 +0200 Subject: [PATCH 050/123] docs: shared during build plugins --- docs/guide/api-vite-environment.md | 73 ++++++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 10 deletions(-) diff --git a/docs/guide/api-vite-environment.md b/docs/guide/api-vite-environment.md index 7d74ba941d74c0..1e2237ecb3f664 100644 --- a/docs/guide/api-vite-environment.md +++ b/docs/guide/api-vite-environment.md @@ -830,17 +830,9 @@ The callback is queued and it will wait for the current update to be resolved be ## Environments during build -Plugin hooks also receive the environment instance during build. This replaces the `ssr` boolean we have been passing them so far. +In the CLI, calling `vite build` and `vite build --ssr` will still build the client only and ssr only environments for backward compatibility. -In the CLI, calling `vite build` will build the Client. It is equivalent to calling `vite build --environment=client`. - -The build the SSR server, the `--ssr` shourcut can be used: `vite build --ssr`. This is equivalent to calling `vite build --environment=ssr`. - -Other non-default environments can be build using `vite build --environment=name`. - -## Building all environments - -Calling `vite build --app` will instantiate a `ViteBuilder` (build-time equivalent to a `ViteDevServer`) to build all configured environments for production. By default the build of environments is run in series respecting the order of the `environments` record. A framework or user can further configure how the environments are built using: +When `builder.entireApp` is `true` (or when calling `vite build --app`), `vite build` will opt-in into building the entire app instead. This would later on become the default in a future major. A `ViteBuilder` instance will be created (build-time equivalent to a `ViteDevServer`) to build all configured environments for production. By default the build of environments is run in series respecting the order of the `environments` record. A framework or user can further configure how the environments are built using: ```js export default { @@ -853,6 +845,67 @@ export default { } ``` +### Environment in build hooks + +In the same way as during dev, plugin hooks also receive the environment instance during build, replacing the `ssr` boolean. +This also works for `renderChunk`, `generateBundle`, and other build only hooks. + +### Shared plugins during build + +Before Vite 6, the plugins pipelines worked in a different way during dev and build: + +- **During dev:** plugins are shared +- **During Build:** plugins are isolated for each environment (in different processes: `vite build` then `vite build --ssr`). + +This forced frameworks to share state between the `client` build and the `ssr` build through manifest files written to the file system. In Vite 6, we are now building all environments in a single process so the way the plugins pipeline and inter-environment communication can be aligned with dev. + +In a future major (Vite 7 or 8), we aim to have complete alignment: + +- **During both dev and build:** plugins are shared, opt-in to [per-environment isolation with functional plugins](#per-environment-plugins) + +There will also be a single `ResolvedConfig` instance shared during build, allowing for caching at entire app build process level in the same way as we have been doing with `WeakMap` during dev. + +For Vite 6, we need to do a smaller step to keep backward compatibility. Ecosystem plugins are currently using `config.build` instead of `environment.options.build` to access configuration, so we need to create a new `ResolvedConfig` per environment by default. A project can opt-in into sharing the full config and plugins pipeline setting `builder.sharedConfig` to `true`. + +This option would only work of a small subset of projects at first, so plugin authors can opt-in for a particular plugin to be shared by setting the `sharedDuringBuild` flag to `true`. This allows for easily sharing state both for regular plugins: + +```js +function myPlugin() { + // Share state between all environments in dev and build + const sharedState = ... + return { + name: 'shared-plugin', + transform(code, id) { ... }, + + // Opt-in into a single instance for all environments + sharedDuringBuild: true, + } +} +``` + +And for per-environment plugins: + +```js +function myPlugin() { + // Share state between all environments in dev and build + const sharedState = ... + + const plugin = (environment) => { + // Isolated state for each environment during dev and build + const isolatedState = ... + + return { + name: 'bounded-plugin', + transform(code, id) { ... } + } + } + + // Opt-in into a single instance for all environments + plugin.sharedDuringBuild = true + return plugin +} +``` + ## Backward Compatibility The current Vite server API will be deprecated but keep working during the next major. From 1ec07a4fe6337cec6da9637bcbf0d93d14e1c5db Mon Sep 17 00:00:00 2001 From: patak-dev Date: Thu, 2 May 2024 10:13:41 +0200 Subject: [PATCH 051/123] refactor: BoundedPlugin -> IsolatedPlugin --- docs/guide/api-vite-environment.md | 10 ++--- packages/vite/src/node/build.ts | 10 ++--- packages/vite/src/node/config.ts | 26 ++++++------ packages/vite/src/node/environment.ts | 6 +-- packages/vite/src/node/idResolver.ts | 12 +++--- packages/vite/src/node/index.ts | 4 +- packages/vite/src/node/optimizer/scan.ts | 16 +++---- packages/vite/src/node/plugin.ts | 42 +++++++++---------- packages/vite/src/node/server/environment.ts | 14 +++---- .../vite/src/node/server/pluginContainer.ts | 10 ++--- 10 files changed, 75 insertions(+), 75 deletions(-) diff --git a/docs/guide/api-vite-environment.md b/docs/guide/api-vite-environment.md index 1e2237ecb3f664..319541c2f5f51b 100644 --- a/docs/guide/api-vite-environment.md +++ b/docs/guide/api-vite-environment.md @@ -58,12 +58,12 @@ class DevEnvironment { * Resolved plugins for this environment, including the ones * created using the per-environment `create` hook */ - plugins: BoundedPlugin[] + plugins: IsolatedPlugin[] /** * Allows to resolve, load, and transform code through the * environment plugins pipeline */ - pluginContainer: BoundedPluginContatiner + pluginContainer: IsolatedPluginContatiner /** * TBD: This abstraction isn't yet clear * Trigger the execution of a module using the associated module runner @@ -871,7 +871,7 @@ This option would only work of a small subset of projects at first, so plugin au ```js function myPlugin() { - // Share state between all environments in dev and build + // Share state among all environments in dev and build const sharedState = ... return { name: 'shared-plugin', @@ -887,7 +887,7 @@ And for per-environment plugins: ```js function myPlugin() { - // Share state between all environments in dev and build + // Share state among all environments in dev and build const sharedState = ... const plugin = (environment) => { @@ -895,7 +895,7 @@ function myPlugin() { const isolatedState = ... return { - name: 'bounded-plugin', + name: 'isolated-plugin', transform(code, id) { ... } } } diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 7bb83438dd7996..127450fe815802 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -47,7 +47,7 @@ import { partialEncodeURIPath, requireResolveFromRootWithFallback, } from './utils' -import { resolveBoundedPlugins } from './plugin' +import { resolveIsolatedPlugins } from './plugin' import { manifestPlugin } from './plugins/manifest' import type { Logger } from './logger' import { dataURIPlugin } from './plugins/dataUri' @@ -66,7 +66,7 @@ import { mergeConfig } from './publicUtils' import { webWorkerPostPlugin } from './plugins/worker' import { getHookHandler } from './plugins' import { Environment } from './environment' -import type { BoundedPluginConstructor, Plugin, PluginContext } from './plugin' +import type { IsolatedPluginConstructor, Plugin, PluginContext } from './plugin' export interface BuildEnvironmentOptions { /** @@ -534,7 +534,7 @@ export async function build( function resolveConfigToBuild( inlineConfig: InlineConfig = {}, patchConfig?: (config: ResolvedConfig) => void, - patchPlugins?: (rawPlugins: (Plugin | BoundedPluginConstructor)[]) => void, + patchPlugins?: (rawPlugins: (Plugin | IsolatedPluginConstructor)[]) => void, ) { return resolveConfig( inlineConfig, @@ -1439,7 +1439,7 @@ export class BuildEnvironment extends Environment { return } this._inited = true - this._plugins = await resolveBoundedPlugins(this) + this._plugins = await resolveIsolatedPlugins(this) } } @@ -1526,7 +1526,7 @@ export async function createBuilder( } } const patchPlugins = ( - rawPlugins: (Plugin | BoundedPluginConstructor)[], + rawPlugins: (Plugin | IsolatedPluginConstructor)[], ) => { // Force opt-in shared plugins const environmentPlugins = [...rawPlugins] diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 57fec1ab79208a..5e64c90efae636 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -21,8 +21,8 @@ import { FS_PREFIX, } from './constants' import type { - BoundedPluginConstructor, HookHandler, + IsolatedPluginConstructor, Plugin, PluginEnvironment, PluginOption, @@ -83,8 +83,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 { BoundedPluginContainer } from './server/pluginContainer' -import { createBoundedPluginContainer } from './server/pluginContainer' +import type { IsolatedPluginContainer } from './server/pluginContainer' +import { createIsolatedPluginContainer } from './server/pluginContainer' import type { PackageCache } from './packages' import { findNearestPackageData } from './packages' import { loadEnv, resolveEnvPrefix } from './env' @@ -539,7 +539,7 @@ export type ResolvedConfig = Readonly< alias: Alias[] } plugins: readonly Plugin[] - rawPlugins: readonly (Plugin | BoundedPluginConstructor)[] + rawPlugins: readonly (Plugin | IsolatedPluginConstructor)[] /** @internal inject user plugins into the shared vite pipeline */ resolvePlugins: ( prePlugins: Plugin[], @@ -770,7 +770,7 @@ export async function resolveConfig( isPreview = false, patchConfig: ((config: ResolvedConfig) => void) | undefined = undefined, patchPlugins: - | ((plugins: (Plugin | BoundedPluginConstructor)[]) => void) + | ((plugins: (Plugin | IsolatedPluginConstructor)[]) => void) | undefined = undefined, ): Promise { let config = inlineConfig @@ -812,7 +812,7 @@ export async function resolveConfig( mode = inlineConfig.mode || config.mode || mode configEnv.mode = mode - const filterPlugin = (p: Plugin | BoundedPluginConstructor) => { + const filterPlugin = (p: Plugin | IsolatedPluginConstructor) => { if (!p) { return false } else if (typeof p === 'function' || !p.apply) { @@ -828,7 +828,7 @@ export async function resolveConfig( const rawPlugins = ( (await asyncFlatten(config.plugins || [])) as ( | Plugin - | BoundedPluginConstructor + | IsolatedPluginConstructor )[] ).filter(filterPlugin) @@ -1235,12 +1235,12 @@ export async function resolveConfig( // optimizer & handling css @imports createResolver(options) { const alias: { - client?: BoundedPluginContainer - ssr?: BoundedPluginContainer + client?: IsolatedPluginContainer + ssr?: IsolatedPluginContainer } = {} const resolver: { - client?: BoundedPluginContainer - ssr?: BoundedPluginContainer + client?: IsolatedPluginContainer + ssr?: IsolatedPluginContainer } = {} const environments = this.environments ?? resolvedEnvironments const createPluginContainer = async ( @@ -1251,7 +1251,7 @@ export async function resolveConfig( // 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( + const pluginContainer = await createIsolatedPluginContainer( environment as PluginEnvironment, plugins, ) @@ -1265,7 +1265,7 @@ export async function resolveConfig( ssr?: boolean, ): Promise { const environmentName = ssr ? 'ssr' : 'client' - let container: BoundedPluginContainer + let container: IsolatedPluginContainer if (aliasOnly) { let aliasContainer = alias[environmentName] if (!aliasContainer) { diff --git a/packages/vite/src/node/environment.ts b/packages/vite/src/node/environment.ts index bfa03927b8d8bf..0148f1874e0d3e 100644 --- a/packages/vite/src/node/environment.ts +++ b/packages/vite/src/node/environment.ts @@ -1,7 +1,7 @@ import colors from 'picocolors' import type { Logger } from './logger' import type { ResolvedConfig, ResolvedEnvironmentOptions } from './config' -import type { BoundedPlugin } from './plugin' +import type { IsolatedPlugin } from './plugin' export class Environment { name: string @@ -9,7 +9,7 @@ export class Environment { config: ResolvedConfig options: ResolvedEnvironmentOptions - get plugins(): BoundedPlugin[] { + get plugins(): IsolatedPlugin[] { if (!this._plugins) throw new Error( `${this.name} environment.plugins called before initialized`, @@ -19,7 +19,7 @@ export class Environment { /** * @internal */ - _plugins: BoundedPlugin[] | undefined + _plugins: IsolatedPlugin[] | undefined /** * @internal */ diff --git a/packages/vite/src/node/idResolver.ts b/packages/vite/src/node/idResolver.ts index 33e5b53a67ddd9..5d60b6b0821c51 100644 --- a/packages/vite/src/node/idResolver.ts +++ b/packages/vite/src/node/idResolver.ts @@ -3,8 +3,8 @@ 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 type { IsolatedPluginContainer } from './server/pluginContainer' +import { createIsolatedPluginContainer } from './server/pluginContainer' import { resolvePlugin } from './plugins/resolve' import type { InternalResolveOptions } from './plugins/resolve' import { getFsUtils } from './fsUtils' @@ -26,7 +26,7 @@ export function createIdResolver( ): ResolveIdFn { const scan = options?.scan - const pluginContainerMap = new Map() + const pluginContainerMap = new Map() async function resolve( environment: PluginEnvironment, id: string, @@ -34,7 +34,7 @@ export function createIdResolver( ): Promise { let pluginContainer = pluginContainerMap.get(environment) if (!pluginContainer) { - pluginContainer = await createBoundedPluginContainer(environment, [ + pluginContainer = await createIsolatedPluginContainer(environment, [ aliasPlugin({ entries: config.resolve.alias }), // TODO: resolve.alias per environment? resolvePlugin( { @@ -59,7 +59,7 @@ export function createIdResolver( const aliasOnlyPluginContainerMap = new Map< Environment, - BoundedPluginContainer + IsolatedPluginContainer >() async function resolveAlias( environment: PluginEnvironment, @@ -68,7 +68,7 @@ export function createIdResolver( ): Promise { let pluginContainer = aliasOnlyPluginContainerMap.get(environment) if (!pluginContainer) { - pluginContainer = await createBoundedPluginContainer(environment, [ + pluginContainer = await createIsolatedPluginContainer(environment, [ aliasPlugin({ entries: config.resolve.alias }), // TODO: resolve.alias per environment? ]) aliasOnlyPluginContainerMap.set(environment, pluginContainer) diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index 197b787c4d7be3..ce66f87ee9c7c1 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -95,8 +95,8 @@ export type { SSRTarget, } from './ssr' export type { - BoundedPlugin, - BoundedPluginConstructor, + IsolatedPlugin, + IsolatedPluginConstructor, Plugin, HookHandler, } from './plugin' diff --git a/packages/vite/src/node/optimizer/scan.ts b/packages/vite/src/node/optimizer/scan.ts index 5eef2dde658b8d..5691a34726285d 100644 --- a/packages/vite/src/node/optimizer/scan.ts +++ b/packages/vite/src/node/optimizer/scan.ts @@ -35,9 +35,9 @@ import { virtualModulePrefix, virtualModuleRE, } from '../utils' -import { resolveBoundedPlugins } from '../plugin' -import type { BoundedPluginContainer } from '../server/pluginContainer' -import { createBoundedPluginContainer } from '../server/pluginContainer' +import { resolveIsolatedPlugins } from '../plugin' +import type { IsolatedPluginContainer } from '../server/pluginContainer' +import { createIsolatedPluginContainer } from '../server/pluginContainer' import { Environment } from '../environment' import type { DevEnvironment } from '../server/environment' import { transformGlobImport } from '../plugins/importMetaGlob' @@ -47,7 +47,7 @@ import { loadTsconfigJsonForFile } from '../plugins/esbuild' export class ScanEnvironment extends Environment { mode = 'scan' as const - get pluginContainer(): BoundedPluginContainer { + get pluginContainer(): IsolatedPluginContainer { if (!this._pluginContainer) throw new Error( `${this.name} environment.pluginContainer called before initialized`, @@ -57,15 +57,15 @@ export class ScanEnvironment extends Environment { /** * @internal */ - _pluginContainer: BoundedPluginContainer | undefined + _pluginContainer: IsolatedPluginContainer | undefined async init(): Promise { if (this._inited) { return } this._inited = true - this._plugins = await resolveBoundedPlugins(this) - this._pluginContainer = await createBoundedPluginContainer( + this._plugins = await resolveIsolatedPlugins(this) + this._pluginContainer = await createIsolatedPluginContainer( this, this.plugins, ) @@ -101,7 +101,7 @@ export function devToScanEnvironment( } type ResolveIdOptions = Omit< - Parameters[2], + Parameters[2], 'environment' > diff --git a/packages/vite/src/node/plugin.ts b/packages/vite/src/node/plugin.ts index faa7a097431d76..5417dd89b40ad2 100644 --- a/packages/vite/src/node/plugin.ts +++ b/packages/vite/src/node/plugin.ts @@ -206,7 +206,7 @@ export interface BasePlugin extends RollupPlugin { renderChunk?: ModifyHookContext['renderChunk'], PluginContext> } -export type BoundedPlugin = BasePlugin +export type IsolatedPlugin = BasePlugin export interface Plugin extends BasePlugin { /** @@ -328,21 +328,21 @@ export type PluginWithRequiredHook = Plugin & { [P in K]: NonNullable } -export type BoundedPluginConstructor = { - (environment: PluginEnvironment): BoundedPluginOption +export type IsolatedPluginConstructor = { + (environment: PluginEnvironment): IsolatedPluginOption sharedDuringBuild?: boolean } -export type MaybeBoundedPlugin = BoundedPlugin | false | null | undefined +export type MaybeIsolatedPlugin = IsolatedPlugin | false | null | undefined -export type BoundedPluginOption = - | MaybeBoundedPlugin - | BoundedPluginOption[] - | Promise +export type IsolatedPluginOption = + | MaybeIsolatedPlugin + | IsolatedPluginOption[] + | Promise export type MaybePlugin = | Plugin - | BoundedPluginConstructor + | IsolatedPluginConstructor | false | null | undefined @@ -352,17 +352,17 @@ export type PluginOption = | PluginOption[] | Promise -export async function resolveBoundedPlugins( +export async function resolveIsolatedPlugins( environment: PluginEnvironment, -): Promise { - const userPlugins: BoundedPlugin[] = [] +): Promise { + const userPlugins: IsolatedPlugin[] = [] for (const plugin of environment.config.rawPlugins) { if (typeof plugin === 'function') { - const boundedPlugin = await plugin(environment) - if (boundedPlugin) { - const flatPlugins = await asyncFlattenBoundedPlugin( + const isolatedPlugin = await plugin(environment) + if (isolatedPlugin) { + const flatPlugins = await asyncFlattenIsolatedPlugin( environment, - boundedPlugin, + isolatedPlugin, ) userPlugins.push(...flatPlugins) } @@ -373,10 +373,10 @@ export async function resolveBoundedPlugins( return environment.config.resolvePlugins(...sortUserPlugins(userPlugins)) } -async function asyncFlattenBoundedPlugin( +async function asyncFlattenIsolatedPlugin( environment: PluginEnvironment, - plugins: BoundedPluginOption, -): Promise { + plugins: IsolatedPluginOption, +): Promise { if (!Array.isArray(plugins)) { plugins = [plugins] } @@ -387,7 +387,7 @@ async function asyncFlattenBoundedPlugin( ) ) .flat(Infinity) - .filter(Boolean) as BoundedPluginOption[] + .filter(Boolean) as IsolatedPluginOption[] } while (plugins.some((v: any) => v?.then || v?.split)) - return plugins as BoundedPlugin[] + return plugins as IsolatedPlugin[] } diff --git a/packages/vite/src/node/server/environment.ts b/packages/vite/src/node/server/environment.ts index 69d490969c0fed..253c380b6d8296 100644 --- a/packages/vite/src/node/server/environment.ts +++ b/packages/vite/src/node/server/environment.ts @@ -16,7 +16,7 @@ import { createDepsOptimizer, createExplicitDepsOptimizer, } from '../optimizer/optimizer' -import { resolveBoundedPlugins } from '../plugin' +import { resolveIsolatedPlugins } from '../plugin' import type { DepsOptimizer } from '../optimizer' import { EnvironmentModuleGraph } from './moduleGraph' import type { HMRChannel } from './hmr' @@ -25,10 +25,10 @@ import { transformRequest } from './transformRequest' import type { TransformResult } from './transformRequest' import { ERR_CLOSED_SERVER, - createBoundedPluginContainer, + createIsolatedPluginContainer, } from './pluginContainer' import type { RemoteEnvironmentTransport } from './environmentTransport' -import type { BoundedPluginContainer } from './pluginContainer' +import type { IsolatedPluginContainer } from './pluginContainer' export interface DevEnvironmentSetup { hot?: false | HMRChannel @@ -52,7 +52,7 @@ export class DevEnvironment extends Environment { */ _ssrRunnerOptions: FetchModuleOptions | undefined - get pluginContainer(): BoundedPluginContainer { + get pluginContainer(): IsolatedPluginContainer { if (!this._pluginContainer) throw new Error( `${this.name} environment.pluginContainer called before initialized`, @@ -62,7 +62,7 @@ export class DevEnvironment extends Environment { /** * @internal */ - _pluginContainer: BoundedPluginContainer | undefined + _pluginContainer: IsolatedPluginContainer | undefined /** * TODO: should this be public? @@ -165,8 +165,8 @@ export class DevEnvironment extends Environment { return } this._inited = true - this._plugins = await resolveBoundedPlugins(this) - this._pluginContainer = await createBoundedPluginContainer( + this._plugins = await resolveIsolatedPlugins(this) + this._pluginContainer = await createIsolatedPluginContainer( this, this._plugins, ) diff --git a/packages/vite/src/node/server/pluginContainer.ts b/packages/vite/src/node/server/pluginContainer.ts index 72e89aa6e7bd49..8639fc0ee7053d 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 { BoundedPlugin, Plugin, PluginEnvironment } from '../plugin' +import type { IsolatedPlugin, Plugin, PluginEnvironment } from '../plugin' import { combineSourcemaps, createDebugger, @@ -103,7 +103,7 @@ export interface PluginContainerOptions { writeFile?: (name: string, source: string | Uint8Array) => void } -export interface BoundedPluginContainer { +export interface IsolatedPluginContainer { options: InputOptions buildStart(options: InputOptions): Promise resolveId( @@ -146,11 +146,11 @@ type PluginContext = Omit< * instead of using environment.plugins to allow the creation of different * pipelines working with the same environment (used for createIdResolver). */ -export async function createBoundedPluginContainer( +export async function createIsolatedPluginContainer( environment: PluginEnvironment, - plugins: BoundedPlugin[], + plugins: IsolatedPlugin[], watcher?: FSWatcher, -): Promise { +): Promise { const { config, logger, From 8851d9d1c97cdce0807edd45e33e70446e545956 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Sat, 4 May 2024 08:16:15 +0200 Subject: [PATCH 052/123] release: v6.0.0-alpha.11 --- packages/vite/CHANGELOG.md | 85 ++++++++++++++++++++++++++++++++++++++ packages/vite/package.json | 2 +- 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/packages/vite/CHANGELOG.md b/packages/vite/CHANGELOG.md index 6fa7d4e27d043e..3d481ac08803bc 100644 --- a/packages/vite/CHANGELOG.md +++ b/packages/vite/CHANGELOG.md @@ -1,3 +1,88 @@ +## 6.0.0-alpha.11 (2024-05-04) + +* refactor: BoundedPlugin -> IsolatedPlugin ([1ec07a4](https://github.com/vitejs/vite/commit/1ec07a4)) + + + +## 6.0.0-alpha.10 (2024-05-02) + +* release: v6.0.0-alpha.10 ([57871a4](https://github.com/vitejs/vite/commit/57871a4)) +* feat: sharedDuringBuild for bounded plugins ([d213865](https://github.com/vitejs/vite/commit/d213865)) + + + +## 6.0.0-alpha.9 (2024-05-01) + +* release: v6.0.0-alpha.9 ([2e6abb3](https://github.com/vitejs/vite/commit/2e6abb3)) +* fix: lint ([848af54](https://github.com/vitejs/vite/commit/848af54)) +* fix: module-runner bundle issue with chokidar dep ([b8ef8ff](https://github.com/vitejs/vite/commit/b8ef8ff)) +* refactor: bounded plugins factory instead of create hook ([6b74221](https://github.com/vitejs/vite/commit/6b74221)) +* refactor: move reload on tsconfig change to the server ([1bcb67d](https://github.com/vitejs/vite/commit/1bcb67d)) + + + +## 6.0.0-alpha.8 (2024-04-30) + +* release: v6.0.0-alpha.8 ([1d9caef](https://github.com/vitejs/vite/commit/1d9caef)) +* fix: keep plugins with create hook ([12d467f](https://github.com/vitejs/vite/commit/12d467f)) +* refactor: environment hooks guard ([527621e](https://github.com/vitejs/vite/commit/527621e)) +* feat: define and html plugins ([7566aae](https://github.com/vitejs/vite/commit/7566aae)) + + + +## 6.0.0-alpha.7 (2024-04-29) + +* release: v6.0.0-alpha.7 ([05943cf](https://github.com/vitejs/vite/commit/05943cf)) +* feat: remove config.build from dynamicImportVars plugin ([8231283](https://github.com/vitejs/vite/commit/8231283)) +* feat: this.environment in buildStart, rework more internal plugins ([bda0dc5](https://github.com/vitejs/vite/commit/bda0dc5)) +* feat: this.environment in renderChunk and generateBundle ([a6fc1dd](https://github.com/vitejs/vite/commit/a6fc1dd)) + + + +## 6.0.0-alpha.6 (2024-04-28) + +* release: v6.0.0-alpha.6 ([e8473a6](https://github.com/vitejs/vite/commit/e8473a6)) +* fix: custom environment preload injection (#16541) ([00079da](https://github.com/vitejs/vite/commit/00079da)), closes [#16541](https://github.com/vitejs/vite/issues/16541) + + + +## 6.0.0-alpha.5 (2024-04-26) + +* release: v6.0.0-alpha.5 ([a8adcac](https://github.com/vitejs/vite/commit/a8adcac)) +* fix: use environment.plugins during build, support create hook ([a44810a](https://github.com/vitejs/vite/commit/a44810a)) + + + +## 6.0.0-alpha.4 (2024-04-26) + +* release: v6.0.0-alpha.4 ([e6fa9d9](https://github.com/vitejs/vite/commit/e6fa9d9)) +* fix: backcompat merging ([294a84e](https://github.com/vitejs/vite/commit/294a84e)) +* fix: disable hmr for ssrLoadModule ([33ef0fb](https://github.com/vitejs/vite/commit/33ef0fb)) +* fix: rework backcompat patching of environment config ([72150d6](https://github.com/vitejs/vite/commit/72150d6)) +* fix: sharedPlugins ([a63190c](https://github.com/vitejs/vite/commit/a63190c)) +* fix(cli): -> ([4cf322d](https://github.com/vitejs/vite/commit/4cf322d)) +* fix(v6): fix `ssrEmitAssets` compat (#16480) ([5c5efe4](https://github.com/vitejs/vite/commit/5c5efe4)), closes [#16480](https://github.com/vitejs/vite/issues/16480) +* feat: builder.entireApp, remove --environment ([1925eeb](https://github.com/vitejs/vite/commit/1925eeb)) +* feat: opt-in shared plugins during build ([2866d4f](https://github.com/vitejs/vite/commit/2866d4f)) +* feat: use environment.logger in buildEnvironment ([8843221](https://github.com/vitejs/vite/commit/8843221)) +* refactor: align createBuilder params with createServer ([62921e6](https://github.com/vitejs/vite/commit/62921e6)) +* refactor: build --app ([29dd26e](https://github.com/vitejs/vite/commit/29dd26e)) +* refactor: remove server from createServerModuleRunner ([9d6a152](https://github.com/vitejs/vite/commit/9d6a152)) +* refactor: rename createViteBuilder to createBuilder, align with createServer ([4dc2a75](https://github.com/vitejs/vite/commit/4dc2a75)) +* chore: remove the deprecation notice from ssrLoadModule for now ([bf65476](https://github.com/vitejs/vite/commit/bf65476)) + + + +## 6.0.0-alpha.3 (2024-04-20) + +* release: v6.0.0-alpha.3 ([635aad5](https://github.com/vitejs/vite/commit/635aad5)) +* fix: export missing types ([431cd4b](https://github.com/vitejs/vite/commit/431cd4b)) +* chore: remove configureServer from more plugins ([ad2b0bf](https://github.com/vitejs/vite/commit/ad2b0bf)) +* chore: rename plugin.split to plugin.create ([5f6b62f](https://github.com/vitejs/vite/commit/5f6b62f)) +* feat: environment api (#16129) ([f684d4c](https://github.com/vitejs/vite/commit/f684d4c)), closes [#16129](https://github.com/vitejs/vite/issues/16129) + + + ## 6.0.0-alpha.10 (2024-05-02) * feat: sharedDuringBuild for bounded plugins ([d213865](https://github.com/vitejs/vite/commit/d213865)) diff --git a/packages/vite/package.json b/packages/vite/package.json index 7e759b6fa830f9..f8b33c743a2001 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -1,6 +1,6 @@ { "name": "vite", - "version": "6.0.0-alpha.10", + "version": "6.0.0-alpha.11", "type": "module", "license": "MIT", "author": "Evan You", From ffb4aa7885c9905fa593c7aa75d43879ff8d82cd Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 9 May 2024 14:31:11 +0200 Subject: [PATCH 053/123] docs: add a note about the "run" method --- docs/guide/api-vite-environment.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/guide/api-vite-environment.md b/docs/guide/api-vite-environment.md index 319541c2f5f51b..88278ed492e234 100644 --- a/docs/guide/api-vite-environment.md +++ b/docs/guide/api-vite-environment.md @@ -64,12 +64,6 @@ class DevEnvironment { * environment plugins pipeline */ pluginContainer: IsolatedPluginContatiner - /** - * TBD: This abstraction isn't yet clear - * Trigger the execution of a module using the associated module runner - * in the target runtime. - */ - run: ModuleRunFunction /** * Resolved config options for this environment. Options at the server * global scope are taken as defaults for all environments, and can @@ -77,7 +71,7 @@ class DevEnvironment { */ config: ResolvedDevEnvironmentConfig - constructor(name, config, { hot, run, options }: DevEnvironmentOptions) + constructor(name, config, { hot, options }: DevEnvironmentOptions) /** * Resolve the URL to an id, load it, and process the code using the @@ -121,6 +115,10 @@ But the environment instance can't execute the code itself, as the runtime where We are using `transformRequest(url)` and `warmupRequest(url)` in the current version of this proposal so it is easier to discuss and understand for users used to Vite's current API. Before releasing, we can take the opportunity to review these names too. For example, it could be named `environment.processModule(url)` or `environment.loadModule(url)` taking a page from Rollup's `context.load(id)` in plugin hooks. For the moment, we think keeping the current names and delaying this discussion is better. ::: +:::info Running a module +The initial proposal had a `run` method that would allow consumers to invoke an import on the runner side by using the `transport` option. During our testing we found out that the API was not unversal enough to start recommending it. We are open to implement a built-in layer for remote SSR implementation based on the frameworks feedback. In the meantime, Vite still exposes a [`RunnerTransport` API](#runnertransport) to hide the complexity of the runner RPC. +::: + For the default Node environment, Vite creates a module runner that implements evaluation using `new AsyncFunction` running in the same runtime as the server. This runner is an instance of `ModuleRunner` that exposes: ```ts From 0b3cf69b2ea1fc1230486b7e5610da4a8a642ac6 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 20 May 2024 15:04:20 +0200 Subject: [PATCH 054/123] chore: export ssrTransform --- packages/vite/src/node/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index ce66f87ee9c7c1..d8358109611461 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -27,6 +27,7 @@ export { BuildEnvironment } from './build' export { fetchModule, type FetchModuleOptions } from './ssr/fetchModule' export { createServerModuleRunner } from './ssr/runtime/serverModuleRunner' export { ServerHMRConnector } from './ssr/runtime/serverHmrConnector' +export { ssrTransform as moduleRunnerTransform } from './ssr/ssrTransform' export * from './publicUtils' From a5efd56f0db58a9114f61e3def8a7541d1ef5956 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Mon, 20 May 2024 12:02:24 +0200 Subject: [PATCH 055/123] fix: remove enforce inside IsolatedPlugins --- packages/vite/src/node/build.ts | 22 ++++---- packages/vite/src/node/config.ts | 54 +++++++++---------- packages/vite/src/node/plugin.ts | 11 ++-- packages/vite/src/node/plugins/html.ts | 2 +- packages/vite/src/node/plugins/index.ts | 54 ++++++++----------- packages/vite/src/node/preview.ts | 6 ++- packages/vite/src/node/server/hmr.ts | 14 ++--- .../src/node/server/middlewares/indexHtml.ts | 4 +- 8 files changed, 84 insertions(+), 83 deletions(-) diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 127450fe815802..c4aef25912d0bf 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -534,7 +534,9 @@ export async function build( function resolveConfigToBuild( inlineConfig: InlineConfig = {}, patchConfig?: (config: ResolvedConfig) => void, - patchPlugins?: (rawPlugins: (Plugin | IsolatedPluginConstructor)[]) => void, + patchPlugins?: ( + resolvedPlugins: (Plugin | IsolatedPluginConstructor)[], + ) => void, ) { return resolveConfig( inlineConfig, @@ -548,7 +550,7 @@ function resolveConfigToBuild( } /** - * Build an App environment, or a App library (if librayOptions is provided) + * Build an App environment, or a App library (if libraryOptions is provided) **/ export async function buildEnvironment( config: ResolvedConfig, @@ -608,9 +610,11 @@ export async function buildEnvironment( // inject environment and ssr arg to plugin load/transform hooks // TODO: rework lib mode - const plugins = (libOptions ? config : environment).plugins.map((p) => - injectEnvironmentToHooks(p, environment), - ) + const plugins = ( + libOptions + ? (config.plugins.filter((p) => typeof p !== 'function') as Plugin[]) + : environment.plugins + ).map((p) => injectEnvironmentToHooks(p, environment)) const rollupOptions: RollupOptions = { preserveEntrySignatures: ssr @@ -1526,14 +1530,14 @@ export async function createBuilder( } } const patchPlugins = ( - rawPlugins: (Plugin | IsolatedPluginConstructor)[], + resolvedPlugins: (Plugin | IsolatedPluginConstructor)[], ) => { // Force opt-in shared plugins - const environmentPlugins = [...rawPlugins] + const environmentPlugins = [...resolvedPlugins] let validMixedPlugins = true for (let i = 0; i < environmentPlugins.length; i++) { const environmentPlugin = environmentPlugins[i] - const sharedPlugin = config.rawPlugins[i] + const sharedPlugin = config.plugins[i] if ( config.builder.sharedPlugins || environmentPlugin.sharedDuringBuild @@ -1547,7 +1551,7 @@ export async function createBuilder( } if (validMixedPlugins) { for (let i = 0; i < environmentPlugins.length; i++) { - rawPlugins[i] = environmentPlugins[i] + resolvedPlugins[i] = environmentPlugins[i] } } } diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 5e64c90efae636..a2a429d7406081 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -72,9 +72,9 @@ import { import { getFsUtils } from './fsUtils' import { createPluginHookUtils, - createResolvePlugins, getHookHandler, getSortedPluginsByHook, + resolvePlugins, } from './plugins' import type { ESBuildOptions } from './plugins/esbuild' import type { InternalResolveOptions, ResolveOptions } from './plugins/resolve' @@ -538,14 +538,7 @@ export type ResolvedConfig = Readonly< resolve: Required & { alias: Alias[] } - plugins: readonly Plugin[] - rawPlugins: readonly (Plugin | IsolatedPluginConstructor)[] - /** @internal inject user plugins into the shared vite pipeline */ - resolvePlugins: ( - prePlugins: Plugin[], - normalPlugins: Plugin[], - postPlugins: Plugin[], - ) => Plugin[] + plugins: readonly (Plugin | IsolatedPluginConstructor)[] css: ResolvedCSSOptions esbuild: ESBuildOptions | false server: ResolvedServerOptions @@ -770,7 +763,7 @@ export async function resolveConfig( isPreview = false, patchConfig: ((config: ResolvedConfig) => void) | undefined = undefined, patchPlugins: - | ((plugins: (Plugin | IsolatedPluginConstructor)[]) => void) + | ((resolvedPlugins: (Plugin | IsolatedPluginConstructor)[]) => void) | undefined = undefined, ): Promise { let config = inlineConfig @@ -832,9 +825,6 @@ export async function resolveConfig( )[] ).filter(filterPlugin) - // Backward compatibility hook used in builder, opt-in to shared plugins during build - patchPlugins?.(rawPlugins) - const sharedPlugins = rawPlugins.filter( (plugin) => typeof plugin !== 'function', ) as Plugin[] @@ -855,7 +845,7 @@ export async function resolveConfig( // 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 + // through the deprecated ssr top level options or if it is explicitly defined // in the environments config config.environments = { ssr: {}, ...config.environments } } @@ -1139,12 +1129,12 @@ export async function resolveConfig( mainConfig: resolved, bundleChain, } - const resolveWorkerPlugins = await createResolvePlugins(workerResolved) - const resolvedWorkerPlugins = resolveWorkerPlugins( + const resolvedWorkerPlugins = (await resolvePlugins( + workerResolved, workerPrePlugins, workerNormalPlugins, workerPostPlugins, - ) + )) as Plugin[] // TODO: worker plugins and isolated constructor // run configResolved hooks await Promise.all( @@ -1180,8 +1170,6 @@ export async function resolveConfig( bundleChain: [], isProduction, plugins: userPlugins, // placeholder to be replaced - rawPlugins, - resolvePlugins: null as any, // placeholder to be replaced css: resolveCSSOptions(config.css), esbuild: config.esbuild === false @@ -1312,21 +1300,33 @@ export async function resolveConfig( } // Backward compatibility hook, modify the resolved config before it is used - // to create inernal plugins. For example, `config.build.ssr`. Once we rework + // to create internal plugins. For example, `config.build.ssr`. Once we rework // internal plugins to use environment.options, we can remove the dual // patchConfig/patchPlugins and have a single patchConfig before configResolved // gets called patchConfig?.(resolved) - const resolvePlugins = await createResolvePlugins(resolved) - - ;(resolved.resolvePlugins as any) = resolvePlugins - - const resolvedPlugins = resolvePlugins(prePlugins, normalPlugins, postPlugins) + const resolvedPlugins = await resolvePlugins( + resolved, + prePlugins, + normalPlugins, + postPlugins, + ) - ;(resolved.plugins as Plugin[]) = resolvedPlugins + // Backward compatibility hook used in builder, opt-in to shared plugins during build + patchPlugins?.(resolvedPlugins) + ;(resolved.plugins as (Plugin | IsolatedPluginConstructor)[]) = + resolvedPlugins - Object.assign(resolved, createPluginHookUtils(resolved.plugins)) + // TODO: Deprecate config.getSortedPlugins and config.getSortedPluginHooks + Object.assign( + resolved, + createPluginHookUtils( + resolved.plugins.filter( + (plugin) => typeof plugin !== 'function', + ) as Plugin[], + ), + ) // call configResolved hooks await Promise.all( diff --git a/packages/vite/src/node/plugin.ts b/packages/vite/src/node/plugin.ts index 5417dd89b40ad2..4684f3291cb90b 100644 --- a/packages/vite/src/node/plugin.ts +++ b/packages/vite/src/node/plugin.ts @@ -14,7 +14,6 @@ import type { ResolvedConfig, UserConfig, } from './config' -import { sortUserPlugins } from './config' import type { ServerHook, ViteDevServer } from './server' import type { IndexHtmlTransform } from './plugins/html' import type { EnvironmentModuleNode } from './server/moduleGraph' @@ -355,8 +354,8 @@ export type PluginOption = export async function resolveIsolatedPlugins( environment: PluginEnvironment, ): Promise { - const userPlugins: IsolatedPlugin[] = [] - for (const plugin of environment.config.rawPlugins) { + const resolvedPlugins: IsolatedPlugin[] = [] + for (const plugin of environment.config.plugins) { if (typeof plugin === 'function') { const isolatedPlugin = await plugin(environment) if (isolatedPlugin) { @@ -364,13 +363,13 @@ export async function resolveIsolatedPlugins( environment, isolatedPlugin, ) - userPlugins.push(...flatPlugins) + resolvedPlugins.push(...flatPlugins) } } else { - userPlugins.push(plugin) + resolvedPlugins.push(plugin) } } - return environment.config.resolvePlugins(...sortUserPlugins(userPlugins)) + return resolvedPlugins } async function asyncFlattenIsolatedPlugin( diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index 46088a802eed48..0ac974a9a175fe 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -307,7 +307,7 @@ function handleParseError( */ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { const [preHooks, normalHooks, postHooks] = resolveHtmlTransforms( - config.plugins, + config.plugins.filter((plugin) => typeof plugin !== 'function') as Plugin[], config.logger, ) preHooks.unshift(injectCspNonceMetaTagHook(config)) diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index 0cfb7091182606..36d9aff996e06d 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -2,7 +2,12 @@ import aliasPlugin, { type ResolverFunction } from '@rollup/plugin-alias' import type { ObjectHook } from 'rollup' import type { PluginHookUtils, ResolvedConfig } from '../config' import { isDepsOptimizerEnabled } from '../config' -import type { HookHandler, Plugin, PluginWithRequiredHook } from '../plugin' +import type { + HookHandler, + IsolatedPluginConstructor, + Plugin, + PluginWithRequiredHook, +} from '../plugin' import { watchPackageDataPlugin } from '../packages' import { getFsUtils } from '../fsUtils' import { jsonPlugin } from './json' @@ -26,15 +31,12 @@ import { dynamicImportVarsPlugin } from './dynamicImportVars' import { importGlobPlugin } from './importMetaGlob' // TODO: import { loadFallbackPlugin } from './loadFallback' -export async function createResolvePlugins( +export async function resolvePlugins( config: ResolvedConfig, -): Promise< - ( - prePlugins: Plugin[], - normalPlugins: Plugin[], - postPlugins: Plugin[], - ) => Plugin[] -> { + prePlugins: Plugin[], + normalPlugins: Plugin[], + postPlugins: Plugin[], +): Promise<(Plugin | IsolatedPluginConstructor)[]> { const isBuild = config.command === 'build' const isWorker = config.isWorker const buildPlugins = isBuild @@ -46,7 +48,7 @@ export async function createResolvePlugins( (isDepsOptimizerEnabled(config, false) || isDepsOptimizerEnabled(config, true)) - const preVitePlugins = [ + return [ depsOptimizerEnabled ? optimizedDepsPlugin(config) : null, isBuild ? metadataPlugin() : null, !isWorker ? watchPackageDataPlugin(config.packageCache) : null, @@ -55,9 +57,9 @@ export async function createResolvePlugins( entries: config.resolve.alias, customResolver: viteAliasCustomResolver, }), - ] - // then ...prePlugins - const normalVitePlugins = [ + + ...prePlugins, + modulePreload !== false && modulePreload.polyfill ? modulePreloadPolyfillPlugin(config) : null, @@ -87,9 +89,9 @@ export async function createResolvePlugins( wasmHelperPlugin(config), webWorkerPlugin(config), assetPlugin(config), - ] - // then ...normalPlugins - const postVitePlugins = [ + + ...normalPlugins, + wasmFallbackPlugin(), definePlugin(config), cssPostPlugin(config), @@ -99,10 +101,11 @@ export async function createResolvePlugins( ...buildPlugins.pre, dynamicImportVarsPlugin(config), importGlobPlugin(config), - ] - // then ...postVitePlugins - const finalVitePlugins = [ + + ...postPlugins, + ...buildPlugins.post, + // internal server-only plugins are always applied after everything else ...(isBuild ? [] @@ -112,18 +115,7 @@ export async function createResolvePlugins( importAnalysisPlugin(config), // TODO: loadFallbackPlugin(config), ]), - ] - return (prePlugins, normalPlugins, postPlugins) => { - return [ - ...preVitePlugins, - ...prePlugins, - ...normalVitePlugins, - ...normalPlugins, - ...postVitePlugins, - ...postPlugins, - ...finalVitePlugins, - ].filter(Boolean) as Plugin[] - } + ].filter(Boolean) as (Plugin | IsolatedPluginConstructor)[] } export function createPluginHookUtils( diff --git a/packages/vite/src/node/preview.ts b/packages/vite/src/node/preview.ts index 566b5d886e790f..dfdae99daf4b53 100644 --- a/packages/vite/src/node/preview.ts +++ b/packages/vite/src/node/preview.ts @@ -26,6 +26,7 @@ import { indexHtmlMiddleware } from './server/middlewares/indexHtml' import { notFoundMiddleware } from './server/middlewares/notFound' import { proxyMiddleware } from './server/middlewares/proxy' import { resolveHostname, resolveServerUrls, shouldServeFile } from './utils' +import type { Plugin } from './plugin' import { printServerUrls } from './logger' import { bindCLIShortcuts } from './shortcuts' import type { BindCLIShortcutsOptions } from './shortcuts' @@ -114,10 +115,13 @@ export async function preview( const clientOutDir = config.environments.client.build.outDir ?? config.build.outDir const distDir = path.resolve(config.root, clientOutDir) + const plugins = config.plugins.filter( + (plugin) => typeof plugin !== 'function', + ) as Plugin[] if ( !fs.existsSync(distDir) && // error if no plugins implement `configurePreviewServer` - config.plugins.every((plugin) => !plugin.configurePreviewServer) && + plugins.every((plugin) => !plugin.configurePreviewServer) && // error if called in CLI only. programmatic usage could access `httpServer` // and affect file serving process.argv[1]?.endsWith(path.normalize('bin/vite.js')) && diff --git a/packages/vite/src/node/server/hmr.ts b/packages/vite/src/node/server/hmr.ts index 7a66166d6f712e..4b0d7246493282 100644 --- a/packages/vite/src/node/server/hmr.ts +++ b/packages/vite/src/node/server/hmr.ts @@ -6,7 +6,7 @@ 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 type { Environment } from '../environment' import { createDebugger, normalizePath } from '../utils' import type { InferCustomEventPayload, ViteDevServer } from '..' import { getHookHandler } from '../plugins' @@ -165,12 +165,12 @@ export function getSortedPluginsByHotUpdateHook( return sortedPlugins } -const sortedHotUpdatePluginsCache = new WeakMap() -function getSortedHotUpdatePlugins(config: ResolvedConfig): Plugin[] { - let sortedPlugins = sortedHotUpdatePluginsCache.get(config) as Plugin[] +const sortedHotUpdatePluginsCache = new WeakMap() +function getSortedHotUpdatePlugins(environment: Environment): Plugin[] { + let sortedPlugins = sortedHotUpdatePluginsCache.get(environment) as Plugin[] if (!sortedPlugins) { - sortedPlugins = getSortedPluginsByHotUpdateHook(config.plugins) - sortedHotUpdatePluginsCache.set(config, sortedPlugins) + sortedPlugins = getSortedPluginsByHotUpdateHook(environment.plugins) + sortedHotUpdatePluginsCache.set(environment, sortedPlugins) } return sortedPlugins } @@ -255,7 +255,7 @@ export async function handleHMRUpdate( let hmrContext - for (const plugin of getSortedHotUpdatePlugins(config)) { + for (const plugin of getSortedHotUpdatePlugins(environment)) { if (plugin.hotUpdate) { const filteredModules = await getHookHandler(plugin.hotUpdate)( hotContext, diff --git a/packages/vite/src/node/server/middlewares/indexHtml.ts b/packages/vite/src/node/server/middlewares/indexHtml.ts index b88a9c4185ff61..f0eeb428a7bdc1 100644 --- a/packages/vite/src/node/server/middlewares/indexHtml.ts +++ b/packages/vite/src/node/server/middlewares/indexHtml.ts @@ -4,6 +4,7 @@ import MagicString from 'magic-string' import type { SourceMapInput } from 'rollup' import type { Connect } from 'dep-types/connect' import type { DefaultTreeAdapterMap, Token } from 'parse5' +import type { Plugin } from '../../plugin' import type { IndexHtmlTransformHook } from '../../plugins/html' import { addToHTMLProxyCache, @@ -66,7 +67,8 @@ export function createDevHtmlTransformFn( originalUrl?: string, ) => Promise { const [preHooks, normalHooks, postHooks] = resolveHtmlTransforms( - config.plugins, + // TODO: interaction between transformIndexHtml and plugin constructors + config.plugins.filter((plugin) => typeof plugin !== 'function') as Plugin[], config.logger, ) const transformHooks = [ From 8c5674e10deb67aaf23b74fddedb5ff750ba3bb7 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Mon, 20 May 2024 15:05:47 +0200 Subject: [PATCH 056/123] chore: respect ssr flag in clientInjectsions transform --- packages/vite/src/node/plugins/clientInjections.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/node/plugins/clientInjections.ts b/packages/vite/src/node/plugins/clientInjections.ts index b874b27d9944dd..f624b4091608f1 100644 --- a/packages/vite/src/node/plugins/clientInjections.ts +++ b/packages/vite/src/node/plugins/clientInjections.ts @@ -89,10 +89,11 @@ export function clientInjectionsPlugin(config: ResolvedConfig): Plugin { .replace(`__HMR_CONFIG_NAME__`, hmrConfigNameReplacement) } }, - async transform(code, id) { + async transform(code, id, options) { if (!this.environment) return // TODO: !environment.options.nodeCompatible ? - const ssr = this.environment.name !== 'client' + // TODO: Remove options?.ssr, Vitest currently hijacks this plugin + const ssr = options?.ssr ?? this.environment.name !== 'client' if (id === normalizedClientEntry || id === normalizedEnvEntry) { return injectConfigValues(code) } else if (!ssr && code.includes('process.env.NODE_ENV')) { From 371f7a02a74a5e43536b2ab5725814b43a4db4e6 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Mon, 20 May 2024 15:07:51 +0200 Subject: [PATCH 057/123] release: v6.0.0-alpha.12 --- packages/vite/CHANGELOG.md | 8 ++++++++ packages/vite/package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/vite/CHANGELOG.md b/packages/vite/CHANGELOG.md index 3d481ac08803bc..9bca2fdc4f523c 100644 --- a/packages/vite/CHANGELOG.md +++ b/packages/vite/CHANGELOG.md @@ -1,3 +1,11 @@ +## 6.0.0-alpha.12 (2024-05-20) + +* chore: export ssrTransform ([0b3cf69](https://github.com/vitejs/vite/commit/0b3cf69)) +* chore: respect ssr flag in clientInjectsions transform ([8c5674e](https://github.com/vitejs/vite/commit/8c5674e)) +* fix: remove enforce inside IsolatedPlugins ([a5efd56](https://github.com/vitejs/vite/commit/a5efd56)) + + + ## 6.0.0-alpha.11 (2024-05-04) * refactor: BoundedPlugin -> IsolatedPlugin ([1ec07a4](https://github.com/vitejs/vite/commit/1ec07a4)) diff --git a/packages/vite/package.json b/packages/vite/package.json index f8b33c743a2001..cb7748173ca16d 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -1,6 +1,6 @@ { "name": "vite", - "version": "6.0.0-alpha.11", + "version": "6.0.0-alpha.12", "type": "module", "license": "MIT", "author": "Evan You", From 82111bfb4134646f5ea03a6bead16ebdf67bbe15 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Mon, 20 May 2024 15:23:58 +0200 Subject: [PATCH 058/123] fix: types --- packages/vite/src/node/index.ts | 1 + packages/vite/src/node/ssr/ssrTransform.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index d8358109611461..92b6a0acd6e7db 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -28,6 +28,7 @@ export { fetchModule, type FetchModuleOptions } from './ssr/fetchModule' export { createServerModuleRunner } from './ssr/runtime/serverModuleRunner' export { ServerHMRConnector } from './ssr/runtime/serverHmrConnector' export { ssrTransform as moduleRunnerTransform } from './ssr/ssrTransform' +export type { ModuleRunnerTransformOptions } from './ssr/ssrTransform' export * from './publicUtils' diff --git a/packages/vite/src/node/ssr/ssrTransform.ts b/packages/vite/src/node/ssr/ssrTransform.ts index ab49bdbcd18c85..0ba9dd09ed00b7 100644 --- a/packages/vite/src/node/ssr/ssrTransform.ts +++ b/packages/vite/src/node/ssr/ssrTransform.ts @@ -26,7 +26,7 @@ type Node = _Node & { end: number } -interface TransformOptions { +export interface ModuleRunnerTransformOptions { json?: { stringify?: boolean } @@ -47,7 +47,7 @@ export async function ssrTransform( inMap: SourceMap | { mappings: '' } | null, url: string, originalCode: string, - options?: TransformOptions, + options?: ModuleRunnerTransformOptions, // TODO: Should we export two functions instead of using options here? ): Promise { if (options?.json?.stringify && isJSONRequest(url)) { return ssrTransformJSON(code, inMap) From 2ae2fca9db1c6834a0f8156ccbf28551215ee0e9 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Mon, 20 May 2024 15:24:13 +0200 Subject: [PATCH 059/123] release: v6.0.0-alpha.13 --- packages/vite/CHANGELOG.md | 6 ++++++ packages/vite/package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/vite/CHANGELOG.md b/packages/vite/CHANGELOG.md index 9bca2fdc4f523c..6e0a6ad86974f8 100644 --- a/packages/vite/CHANGELOG.md +++ b/packages/vite/CHANGELOG.md @@ -1,3 +1,9 @@ +## 6.0.0-alpha.13 (2024-05-20) + +* fix: types ([82111bf](https://github.com/vitejs/vite/commit/82111bf)) + + + ## 6.0.0-alpha.12 (2024-05-20) * chore: export ssrTransform ([0b3cf69](https://github.com/vitejs/vite/commit/0b3cf69)) diff --git a/packages/vite/package.json b/packages/vite/package.json index cb7748173ca16d..6cc9bca455b08e 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -1,6 +1,6 @@ { "name": "vite", - "version": "6.0.0-alpha.12", + "version": "6.0.0-alpha.13", "type": "module", "license": "MIT", "author": "Evan You", From 3546d1e3cdf2e2d97055251ba65386e3a29e606b Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 20 May 2024 15:48:43 +0200 Subject: [PATCH 060/123] fix: revert resolveId importer require --- packages/vite/src/node/server/pluginContainer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite/src/node/server/pluginContainer.ts b/packages/vite/src/node/server/pluginContainer.ts index 8639fc0ee7053d..fd798319ae7571 100644 --- a/packages/vite/src/node/server/pluginContainer.ts +++ b/packages/vite/src/node/server/pluginContainer.ts @@ -844,7 +844,7 @@ export interface PluginContainer { buildStart(options: InputOptions): Promise resolveId( id: string, - importer: string | undefined, + importer?: string, options?: { attributes?: Record custom?: CustomPluginOptions From 34d851874a092da187b0cadc3450df40caeb5cbd Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 20 May 2024 15:52:28 +0200 Subject: [PATCH 061/123] fix: export isFileLoadingAllowed --- packages/vite/src/node/publicUtils.ts | 6 ++++-- packages/vite/src/node/server/middlewares/static.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/vite/src/node/publicUtils.ts b/packages/vite/src/node/publicUtils.ts index 5c8c0ca99cdbbf..a9cad7a5106db6 100644 --- a/packages/vite/src/node/publicUtils.ts +++ b/packages/vite/src/node/publicUtils.ts @@ -21,6 +21,8 @@ export { send } from './server/send' export { createLogger } from './logger' export { searchForWorkspaceRoot } from './server/searchRoot' -// TODO: export isFileLoadingAllowed? -export { isFileServingAllowed } from './server/middlewares/static' +export { + isFileServingAllowed, + isFileLoadingAllowed, +} from './server/middlewares/static' export { loadEnv, resolveEnvPrefix } from './env' diff --git a/packages/vite/src/node/server/middlewares/static.ts b/packages/vite/src/node/server/middlewares/static.ts index 3795e10a440849..6924989ab538ab 100644 --- a/packages/vite/src/node/server/middlewares/static.ts +++ b/packages/vite/src/node/server/middlewares/static.ts @@ -252,7 +252,7 @@ function fsDenyGlob(config: ResolvedConfig, filePath: string): boolean { /** * Check if the url is allowed to be served, via the `server.fs` config. - * @deprecate use isFileLoadingAllowed + * @deprecated use isFileLoadingAllowed */ export function isFileServingAllowed( url: string, From 8ee7fcdd9e2c848358a94c2e41fb5cfbf03f8547 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 20 May 2024 17:42:39 +0200 Subject: [PATCH 062/123] fix: rely on the current environment in resolveId, if known --- packages/vite/src/node/config.ts | 13 ++++++----- packages/vite/src/node/idResolver.ts | 27 ++++++++++------------- packages/vite/src/node/plugins/resolve.ts | 11 ++++----- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index a2a429d7406081..5c7b6bbd4a92de 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -1216,11 +1216,14 @@ export async function resolveConfig( 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 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 + * @deprecated + */ createResolver(options) { const alias: { client?: IsolatedPluginContainer diff --git a/packages/vite/src/node/idResolver.ts b/packages/vite/src/node/idResolver.ts index 5d60b6b0821c51..8f0b4d1a30d3d6 100644 --- a/packages/vite/src/node/idResolver.ts +++ b/packages/vite/src/node/idResolver.ts @@ -36,21 +36,18 @@ export function createIdResolver( if (!pluginContainer) { pluginContainer = await createIsolatedPluginContainer(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, - ), + 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, + }), ]) pluginContainerMap.set(environment, pluginContainer) } diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index aa486f0aee425d..dfaa03604ddfe9 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -188,7 +188,7 @@ export function resolvePlugin( * 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, + environmentsOptions?: Record, ): Plugin { const { root, isProduction, asSrc, preferRelative = false } = resolveOptions @@ -230,8 +230,9 @@ export function resolvePlugin( resolveOpts?.custom?.['node-resolve']?.isRequire ?? false const environmentName = this.environment?.name ?? (ssr ? 'ssr' : 'client') - const environmentResolveOptions = - environmentsOptions[environmentName].resolve + const currentEnvironmentOptions = + this.environment?.options || environmentsOptions?.[environmentName] + const environmentResolveOptions = currentEnvironmentOptions?.resolve if (!environmentResolveOptions) { throw new Error( `Missing ResolveOptions for ${environmentName} environment`, @@ -240,8 +241,8 @@ export function resolvePlugin( const options: InternalResolveOptions = { isRequire, ...environmentResolveOptions, - nodeCompatible: environmentsOptions[environmentName].nodeCompatible, - webCompatible: environmentsOptions[environmentName].webCompatible, + nodeCompatible: currentEnvironmentOptions.nodeCompatible, + webCompatible: currentEnvironmentOptions.webCompatible, ...resolveOptions, // plugin options + resolve options overrides scan: resolveOpts?.scan ?? resolveOptions.scan, } From 54c219eeb0f81181665121cff7475650acf969a5 Mon Sep 17 00:00:00 2001 From: patak <583075+patak-dev@users.noreply.github.com> Date: Mon, 20 May 2024 22:34:52 +0200 Subject: [PATCH 063/123] feat: back to environmentPlugins hook (#16732) --- docs/guide/api-vite-environment.md | 28 ++--- packages/vite/src/node/build.ts | 22 ++-- packages/vite/src/node/config.ts | 49 +++----- packages/vite/src/node/environment.ts | 6 +- packages/vite/src/node/idResolver.ts | 12 +- packages/vite/src/node/index.ts | 7 +- packages/vite/src/node/optimizer/scan.ts | 16 +-- packages/vite/src/node/plugin.ts | 108 ++++++++---------- packages/vite/src/node/plugins/html.ts | 2 +- packages/vite/src/node/plugins/index.ts | 11 +- packages/vite/src/node/preview.ts | 6 +- packages/vite/src/node/server/environment.ts | 14 +-- .../src/node/server/middlewares/indexHtml.ts | 4 +- .../vite/src/node/server/pluginContainer.ts | 10 +- 14 files changed, 121 insertions(+), 174 deletions(-) diff --git a/docs/guide/api-vite-environment.md b/docs/guide/api-vite-environment.md index 88278ed492e234..fac53cbc67b397 100644 --- a/docs/guide/api-vite-environment.md +++ b/docs/guide/api-vite-environment.md @@ -58,12 +58,12 @@ class DevEnvironment { * Resolved plugins for this environment, including the ones * created using the per-environment `create` hook */ - plugins: IsolatedPlugin[] + plugins: EnvironmentPlugin[] /** * Allows to resolve, load, and transform code through the * environment plugins pipeline */ - pluginContainer: IsolatedPluginContatiner + pluginContainer: EnvironmentPluginContainer /** * Resolved config options for this environment. Options at the server * global scope are taken as defaults for all environments, and can @@ -888,19 +888,19 @@ function myPlugin() { // Share state among all environments in dev and build const sharedState = ... - const plugin = (environment) => { - // Isolated state for each environment during dev and build - const isolatedState = ... - - return { - name: 'isolated-plugin', - transform(code, id) { ... } - } + return { + name: 'with-environment-plugins', + environmentPlugins(environment) { + // Isolated state for each environment during dev and build + const isolatedState = ... + return { + name: 'per-environment-plugin', + transform(code, id) { ... }, + } + }, + // Opt-in into a single instance for all environments + sharedDuringBuild: true } - - // Opt-in into a single instance for all environments - plugin.sharedDuringBuild = true - return plugin } ``` diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index c4aef25912d0bf..6c310528a3e4e7 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -47,7 +47,7 @@ import { partialEncodeURIPath, requireResolveFromRootWithFallback, } from './utils' -import { resolveIsolatedPlugins } from './plugin' +import { resolveEnvironmentPlugins } from './plugin' import { manifestPlugin } from './plugins/manifest' import type { Logger } from './logger' import { dataURIPlugin } from './plugins/dataUri' @@ -66,7 +66,7 @@ import { mergeConfig } from './publicUtils' import { webWorkerPostPlugin } from './plugins/worker' import { getHookHandler } from './plugins' import { Environment } from './environment' -import type { IsolatedPluginConstructor, Plugin, PluginContext } from './plugin' +import type { Plugin, PluginContext } from './plugin' export interface BuildEnvironmentOptions { /** @@ -534,9 +534,7 @@ export async function build( function resolveConfigToBuild( inlineConfig: InlineConfig = {}, patchConfig?: (config: ResolvedConfig) => void, - patchPlugins?: ( - resolvedPlugins: (Plugin | IsolatedPluginConstructor)[], - ) => void, + patchPlugins?: (resolvedPlugins: Plugin[]) => void, ) { return resolveConfig( inlineConfig, @@ -610,11 +608,9 @@ export async function buildEnvironment( // inject environment and ssr arg to plugin load/transform hooks // TODO: rework lib mode - const plugins = ( - libOptions - ? (config.plugins.filter((p) => typeof p !== 'function') as Plugin[]) - : environment.plugins - ).map((p) => injectEnvironmentToHooks(p, environment)) + const plugins = (libOptions ? config : environment).plugins.map((p) => + injectEnvironmentToHooks(p, environment), + ) const rollupOptions: RollupOptions = { preserveEntrySignatures: ssr @@ -1443,7 +1439,7 @@ export class BuildEnvironment extends Environment { return } this._inited = true - this._plugins = await resolveIsolatedPlugins(this) + this._plugins = await resolveEnvironmentPlugins(this) } } @@ -1529,9 +1525,7 @@ export async function createBuilder( lib: false, } } - const patchPlugins = ( - resolvedPlugins: (Plugin | IsolatedPluginConstructor)[], - ) => { + const patchPlugins = (resolvedPlugins: Plugin[]) => { // Force opt-in shared plugins const environmentPlugins = [...resolvedPlugins] let validMixedPlugins = true diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 5c7b6bbd4a92de..ef75e4b1536f55 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -22,7 +22,6 @@ import { } from './constants' import type { HookHandler, - IsolatedPluginConstructor, Plugin, PluginEnvironment, PluginOption, @@ -83,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 { IsolatedPluginContainer } from './server/pluginContainer' -import { createIsolatedPluginContainer } from './server/pluginContainer' +import type { EnvironmentPluginContainer } from './server/pluginContainer' +import { createEnvironmentPluginContainer } from './server/pluginContainer' import type { PackageCache } from './packages' import { findNearestPackageData } from './packages' import { loadEnv, resolveEnvPrefix } from './env' @@ -538,7 +537,7 @@ export type ResolvedConfig = Readonly< resolve: Required & { alias: Alias[] } - plugins: readonly (Plugin | IsolatedPluginConstructor)[] + plugins: readonly Plugin[] css: ResolvedCSSOptions esbuild: ESBuildOptions | false server: ResolvedServerOptions @@ -762,9 +761,7 @@ export async function resolveConfig( defaultNodeEnv = 'development', isPreview = false, patchConfig: ((config: ResolvedConfig) => void) | undefined = undefined, - patchPlugins: - | ((resolvedPlugins: (Plugin | IsolatedPluginConstructor)[]) => void) - | undefined = undefined, + patchPlugins: ((resolvedPlugins: Plugin[]) => void) | undefined = undefined, ): Promise { let config = inlineConfig let configFileDependencies: string[] = [] @@ -805,7 +802,7 @@ export async function resolveConfig( mode = inlineConfig.mode || config.mode || mode configEnv.mode = mode - const filterPlugin = (p: Plugin | IsolatedPluginConstructor) => { + const filterPlugin = (p: Plugin) => { if (!p) { return false } else if (typeof p === 'function' || !p.apply) { @@ -819,18 +816,10 @@ export async function resolveConfig( // resolve plugins const rawPlugins = ( - (await asyncFlatten(config.plugins || [])) as ( - | Plugin - | IsolatedPluginConstructor - )[] + (await asyncFlatten(config.plugins || [])) as Plugin[] ).filter(filterPlugin) - const sharedPlugins = rawPlugins.filter( - (plugin) => typeof plugin !== 'function', - ) as Plugin[] - - const [prePlugins, normalPlugins, postPlugins] = - sortUserPlugins(sharedPlugins) + const [prePlugins, normalPlugins, postPlugins] = sortUserPlugins(rawPlugins) const isBuild = command === 'build' @@ -1226,12 +1215,12 @@ export async function resolveConfig( */ createResolver(options) { const alias: { - client?: IsolatedPluginContainer - ssr?: IsolatedPluginContainer + client?: EnvironmentPluginContainer + ssr?: EnvironmentPluginContainer } = {} const resolver: { - client?: IsolatedPluginContainer - ssr?: IsolatedPluginContainer + client?: EnvironmentPluginContainer + ssr?: EnvironmentPluginContainer } = {} const environments = this.environments ?? resolvedEnvironments const createPluginContainer = async ( @@ -1242,7 +1231,7 @@ export async function resolveConfig( // environment so we can safely cast to a base Environment instance to a // PluginEnvironment here const environment = new Environment(environmentName, this) - const pluginContainer = await createIsolatedPluginContainer( + const pluginContainer = await createEnvironmentPluginContainer( environment as PluginEnvironment, plugins, ) @@ -1256,7 +1245,7 @@ export async function resolveConfig( ssr?: boolean, ): Promise { const environmentName = ssr ? 'ssr' : 'client' - let container: IsolatedPluginContainer + let container: EnvironmentPluginContainer if (aliasOnly) { let aliasContainer = alias[environmentName] if (!aliasContainer) { @@ -1318,18 +1307,10 @@ export async function resolveConfig( // Backward compatibility hook used in builder, opt-in to shared plugins during build patchPlugins?.(resolvedPlugins) - ;(resolved.plugins as (Plugin | IsolatedPluginConstructor)[]) = - resolvedPlugins + ;(resolved.plugins as Plugin[]) = resolvedPlugins // TODO: Deprecate config.getSortedPlugins and config.getSortedPluginHooks - Object.assign( - resolved, - createPluginHookUtils( - resolved.plugins.filter( - (plugin) => typeof plugin !== 'function', - ) as Plugin[], - ), - ) + Object.assign(resolved, createPluginHookUtils(resolved.plugins)) // call configResolved hooks await Promise.all( diff --git a/packages/vite/src/node/environment.ts b/packages/vite/src/node/environment.ts index 0148f1874e0d3e..8df034a721760a 100644 --- a/packages/vite/src/node/environment.ts +++ b/packages/vite/src/node/environment.ts @@ -1,7 +1,7 @@ import colors from 'picocolors' import type { Logger } from './logger' import type { ResolvedConfig, ResolvedEnvironmentOptions } from './config' -import type { IsolatedPlugin } from './plugin' +import type { EnvironmentPlugin } from './plugin' export class Environment { name: string @@ -9,7 +9,7 @@ export class Environment { config: ResolvedConfig options: ResolvedEnvironmentOptions - get plugins(): IsolatedPlugin[] { + get plugins(): EnvironmentPlugin[] { if (!this._plugins) throw new Error( `${this.name} environment.plugins called before initialized`, @@ -19,7 +19,7 @@ export class Environment { /** * @internal */ - _plugins: IsolatedPlugin[] | undefined + _plugins: EnvironmentPlugin[] | undefined /** * @internal */ diff --git a/packages/vite/src/node/idResolver.ts b/packages/vite/src/node/idResolver.ts index 8f0b4d1a30d3d6..bcf0f124a2677d 100644 --- a/packages/vite/src/node/idResolver.ts +++ b/packages/vite/src/node/idResolver.ts @@ -3,8 +3,8 @@ import aliasPlugin from '@rollup/plugin-alias' import type { ResolvedConfig } from './config' import type { Environment } from './environment' import type { PluginEnvironment } from './plugin' -import type { IsolatedPluginContainer } from './server/pluginContainer' -import { createIsolatedPluginContainer } from './server/pluginContainer' +import type { EnvironmentPluginContainer } from './server/pluginContainer' +import { createEnvironmentPluginContainer } from './server/pluginContainer' import { resolvePlugin } from './plugins/resolve' import type { InternalResolveOptions } from './plugins/resolve' import { getFsUtils } from './fsUtils' @@ -26,7 +26,7 @@ export function createIdResolver( ): ResolveIdFn { const scan = options?.scan - const pluginContainerMap = new Map() + const pluginContainerMap = new Map() async function resolve( environment: PluginEnvironment, id: string, @@ -34,7 +34,7 @@ export function createIdResolver( ): Promise { let pluginContainer = pluginContainerMap.get(environment) if (!pluginContainer) { - pluginContainer = await createIsolatedPluginContainer(environment, [ + pluginContainer = await createEnvironmentPluginContainer(environment, [ aliasPlugin({ entries: config.resolve.alias }), // TODO: resolve.alias per environment? resolvePlugin({ root: config.root, @@ -56,7 +56,7 @@ export function createIdResolver( const aliasOnlyPluginContainerMap = new Map< Environment, - IsolatedPluginContainer + EnvironmentPluginContainer >() async function resolveAlias( environment: PluginEnvironment, @@ -65,7 +65,7 @@ export function createIdResolver( ): Promise { let pluginContainer = aliasOnlyPluginContainerMap.get(environment) if (!pluginContainer) { - pluginContainer = await createIsolatedPluginContainer(environment, [ + pluginContainer = await createEnvironmentPluginContainer(environment, [ aliasPlugin({ entries: config.resolve.alias }), // TODO: resolve.alias per environment? ]) aliasOnlyPluginContainerMap.set(environment, pluginContainer) diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index 92b6a0acd6e7db..a1a652f33ca3c3 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -96,12 +96,7 @@ export type { SSROptions, SSRTarget, } from './ssr' -export type { - IsolatedPlugin, - IsolatedPluginConstructor, - Plugin, - HookHandler, -} from './plugin' +export type { EnvironmentPlugin, Plugin, HookHandler } from './plugin' export type { Logger, LogOptions, diff --git a/packages/vite/src/node/optimizer/scan.ts b/packages/vite/src/node/optimizer/scan.ts index 5691a34726285d..18fc7497b6dcbe 100644 --- a/packages/vite/src/node/optimizer/scan.ts +++ b/packages/vite/src/node/optimizer/scan.ts @@ -35,9 +35,9 @@ import { virtualModulePrefix, virtualModuleRE, } from '../utils' -import { resolveIsolatedPlugins } from '../plugin' -import type { IsolatedPluginContainer } from '../server/pluginContainer' -import { createIsolatedPluginContainer } from '../server/pluginContainer' +import { resolveEnvironmentPlugins } from '../plugin' +import type { EnvironmentPluginContainer } from '../server/pluginContainer' +import { createEnvironmentPluginContainer } from '../server/pluginContainer' import { Environment } from '../environment' import type { DevEnvironment } from '../server/environment' import { transformGlobImport } from '../plugins/importMetaGlob' @@ -47,7 +47,7 @@ import { loadTsconfigJsonForFile } from '../plugins/esbuild' export class ScanEnvironment extends Environment { mode = 'scan' as const - get pluginContainer(): IsolatedPluginContainer { + get pluginContainer(): EnvironmentPluginContainer { if (!this._pluginContainer) throw new Error( `${this.name} environment.pluginContainer called before initialized`, @@ -57,15 +57,15 @@ export class ScanEnvironment extends Environment { /** * @internal */ - _pluginContainer: IsolatedPluginContainer | undefined + _pluginContainer: EnvironmentPluginContainer | undefined async init(): Promise { if (this._inited) { return } this._inited = true - this._plugins = await resolveIsolatedPlugins(this) - this._pluginContainer = await createIsolatedPluginContainer( + this._plugins = await resolveEnvironmentPlugins(this) + this._pluginContainer = await createEnvironmentPluginContainer( this, this.plugins, ) @@ -101,7 +101,7 @@ export function devToScanEnvironment( } type ResolveIdOptions = Omit< - Parameters[2], + Parameters[2], 'environment' > diff --git a/packages/vite/src/node/plugin.ts b/packages/vite/src/node/plugin.ts index 4684f3291cb90b..6d6edd63cea265 100644 --- a/packages/vite/src/node/plugin.ts +++ b/packages/vite/src/node/plugin.ts @@ -77,7 +77,7 @@ export interface TransformPluginContext extends RollupTransformPluginContext { * 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. + * management for for environment specific plugins. * Environment Plugins are closer to regular rollup plugins. They can't define * app level hooks (like config, configResolved, configureServer, etc). */ @@ -103,21 +103,7 @@ type ModifyHookContext = Hook extends { ? ModifyObjectHookContext : ModifyFunctionContext -export interface BasePlugin extends RollupPlugin { - /** - * Enforce plugin invocation tier similar to webpack loaders. Hooks ordering - * is still subject to the `order` property in the hook object. - * - * Plugin invocation order: - * - alias resolution - * - `enforce: 'pre'` plugins - * - vite core plugins - * - normal plugins - * - vite build plugins - * - `enforce: 'post'` plugins - * - vite build post plugins - */ - enforce?: 'pre' | 'post' +export interface EnvironmentPlugin extends RollupPlugin { /** * Perform custom handling of HMR updates. * The handler receives a context containing changed filename, timestamp, a @@ -205,9 +191,7 @@ export interface BasePlugin extends RollupPlugin { renderChunk?: ModifyHookContext['renderChunk'], PluginContext> } -export type IsolatedPlugin = BasePlugin - -export interface Plugin extends BasePlugin { +export interface Plugin extends EnvironmentPlugin { /** * Opt-in this plugin into the shared plugins pipeline. * For backward-compatibility, plugins are re-recreated for each environment @@ -217,6 +201,20 @@ export interface Plugin extends BasePlugin { * @experimental */ sharedDuringBuild?: boolean + /** + * Enforce plugin invocation tier similar to webpack loaders. Hooks ordering + * is still subject to the `order` property in the hook object. + * + * Plugin invocation order: + * - alias resolution + * - `enforce: 'pre'` plugins + * - vite core plugins + * - normal plugins + * - vite build plugins + * - `enforce: 'post'` plugins + * - vite build post plugins + */ + enforce?: 'pre' | 'post' /** * Apply the plugin only for serve or build, or on certain conditions. */ @@ -307,6 +305,12 @@ export interface Plugin extends BasePlugin { * `{ order: 'pre', handler: hook }` */ transformIndexHtml?: IndexHtmlTransform + /** + * Inject per environment plugins after the shared plugin + */ + environmentPlugins?: ( + environment: PluginEnvironment, + ) => PluginOption[] | undefined /** * @deprecated @@ -327,43 +331,35 @@ export type PluginWithRequiredHook = Plugin & { [P in K]: NonNullable } -export type IsolatedPluginConstructor = { - (environment: PluginEnvironment): IsolatedPluginOption - sharedDuringBuild?: boolean -} - -export type MaybeIsolatedPlugin = IsolatedPlugin | false | null | undefined - -export type IsolatedPluginOption = - | MaybeIsolatedPlugin - | IsolatedPluginOption[] - | Promise - -export type MaybePlugin = - | Plugin - | IsolatedPluginConstructor +export type MaybeEnvironmentPlugin = + | EnvironmentPlugin | false | null | undefined +export type EnvironmentPluginOption = + | MaybeEnvironmentPlugin + | EnvironmentPluginOption[] + | Promise + +export type MaybePlugin = Plugin | false | null | undefined + export type PluginOption = | MaybePlugin | PluginOption[] | Promise -export async function resolveIsolatedPlugins( +export async function resolveEnvironmentPlugins( environment: PluginEnvironment, -): Promise { - const resolvedPlugins: IsolatedPlugin[] = [] +): Promise { + const resolvedPlugins: EnvironmentPlugin[] = [] for (const plugin of environment.config.plugins) { - if (typeof plugin === 'function') { - const isolatedPlugin = await plugin(environment) - if (isolatedPlugin) { - const flatPlugins = await asyncFlattenIsolatedPlugin( - environment, - isolatedPlugin, - ) - resolvedPlugins.push(...flatPlugins) + if (plugin.environmentPlugins) { + const environmentPlugins = await plugin.environmentPlugins(environment) + if (environmentPlugins) { + const newPlugins = + await asyncFlattenEnvironmentPlugins(environmentPlugins) + resolvedPlugins.push(...newPlugins) } } else { resolvedPlugins.push(plugin) @@ -372,21 +368,13 @@ export async function resolveIsolatedPlugins( return resolvedPlugins } -async function asyncFlattenIsolatedPlugin( - environment: PluginEnvironment, - plugins: IsolatedPluginOption, -): Promise { - if (!Array.isArray(plugins)) { - plugins = [plugins] - } +async function asyncFlattenEnvironmentPlugins( + plugins: EnvironmentPluginOption[], +): Promise { do { - plugins = ( - await Promise.all( - plugins.map((p: any) => (typeof p === 'function' ? p(environment) : p)), - ) - ) + plugins = ((await Promise.all(plugins)) as any[]) .flat(Infinity) - .filter(Boolean) as IsolatedPluginOption[] - } while (plugins.some((v: any) => v?.then || v?.split)) - return plugins as IsolatedPlugin[] + .filter(Boolean) as EnvironmentPluginOption[] + } while (plugins.some((v: any) => v?.then)) + return plugins as EnvironmentPlugin[] } diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index 0ac974a9a175fe..46088a802eed48 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -307,7 +307,7 @@ function handleParseError( */ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { const [preHooks, normalHooks, postHooks] = resolveHtmlTransforms( - config.plugins.filter((plugin) => typeof plugin !== 'function') as Plugin[], + config.plugins, config.logger, ) preHooks.unshift(injectCspNonceMetaTagHook(config)) diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index 36d9aff996e06d..6e325118133164 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -2,12 +2,7 @@ import aliasPlugin, { type ResolverFunction } from '@rollup/plugin-alias' import type { ObjectHook } from 'rollup' import type { PluginHookUtils, ResolvedConfig } from '../config' import { isDepsOptimizerEnabled } from '../config' -import type { - HookHandler, - IsolatedPluginConstructor, - Plugin, - PluginWithRequiredHook, -} from '../plugin' +import type { HookHandler, Plugin, PluginWithRequiredHook } from '../plugin' import { watchPackageDataPlugin } from '../packages' import { getFsUtils } from '../fsUtils' import { jsonPlugin } from './json' @@ -36,7 +31,7 @@ export async function resolvePlugins( prePlugins: Plugin[], normalPlugins: Plugin[], postPlugins: Plugin[], -): Promise<(Plugin | IsolatedPluginConstructor)[]> { +): Promise { const isBuild = config.command === 'build' const isWorker = config.isWorker const buildPlugins = isBuild @@ -115,7 +110,7 @@ export async function resolvePlugins( importAnalysisPlugin(config), // TODO: loadFallbackPlugin(config), ]), - ].filter(Boolean) as (Plugin | IsolatedPluginConstructor)[] + ].filter(Boolean) as Plugin[] } export function createPluginHookUtils( diff --git a/packages/vite/src/node/preview.ts b/packages/vite/src/node/preview.ts index dfdae99daf4b53..566b5d886e790f 100644 --- a/packages/vite/src/node/preview.ts +++ b/packages/vite/src/node/preview.ts @@ -26,7 +26,6 @@ import { indexHtmlMiddleware } from './server/middlewares/indexHtml' import { notFoundMiddleware } from './server/middlewares/notFound' import { proxyMiddleware } from './server/middlewares/proxy' import { resolveHostname, resolveServerUrls, shouldServeFile } from './utils' -import type { Plugin } from './plugin' import { printServerUrls } from './logger' import { bindCLIShortcuts } from './shortcuts' import type { BindCLIShortcutsOptions } from './shortcuts' @@ -115,13 +114,10 @@ export async function preview( const clientOutDir = config.environments.client.build.outDir ?? config.build.outDir const distDir = path.resolve(config.root, clientOutDir) - const plugins = config.plugins.filter( - (plugin) => typeof plugin !== 'function', - ) as Plugin[] if ( !fs.existsSync(distDir) && // error if no plugins implement `configurePreviewServer` - plugins.every((plugin) => !plugin.configurePreviewServer) && + config.plugins.every((plugin) => !plugin.configurePreviewServer) && // error if called in CLI only. programmatic usage could access `httpServer` // and affect file serving process.argv[1]?.endsWith(path.normalize('bin/vite.js')) && diff --git a/packages/vite/src/node/server/environment.ts b/packages/vite/src/node/server/environment.ts index 253c380b6d8296..60e7b507ca557c 100644 --- a/packages/vite/src/node/server/environment.ts +++ b/packages/vite/src/node/server/environment.ts @@ -16,7 +16,7 @@ import { createDepsOptimizer, createExplicitDepsOptimizer, } from '../optimizer/optimizer' -import { resolveIsolatedPlugins } from '../plugin' +import { resolveEnvironmentPlugins } from '../plugin' import type { DepsOptimizer } from '../optimizer' import { EnvironmentModuleGraph } from './moduleGraph' import type { HMRChannel } from './hmr' @@ -25,10 +25,10 @@ import { transformRequest } from './transformRequest' import type { TransformResult } from './transformRequest' import { ERR_CLOSED_SERVER, - createIsolatedPluginContainer, + createEnvironmentPluginContainer, } from './pluginContainer' import type { RemoteEnvironmentTransport } from './environmentTransport' -import type { IsolatedPluginContainer } from './pluginContainer' +import type { EnvironmentPluginContainer } from './pluginContainer' export interface DevEnvironmentSetup { hot?: false | HMRChannel @@ -52,7 +52,7 @@ export class DevEnvironment extends Environment { */ _ssrRunnerOptions: FetchModuleOptions | undefined - get pluginContainer(): IsolatedPluginContainer { + get pluginContainer(): EnvironmentPluginContainer { if (!this._pluginContainer) throw new Error( `${this.name} environment.pluginContainer called before initialized`, @@ -62,7 +62,7 @@ export class DevEnvironment extends Environment { /** * @internal */ - _pluginContainer: IsolatedPluginContainer | undefined + _pluginContainer: EnvironmentPluginContainer | undefined /** * TODO: should this be public? @@ -165,8 +165,8 @@ export class DevEnvironment extends Environment { return } this._inited = true - this._plugins = await resolveIsolatedPlugins(this) - this._pluginContainer = await createIsolatedPluginContainer( + this._plugins = await resolveEnvironmentPlugins(this) + this._pluginContainer = await createEnvironmentPluginContainer( this, this._plugins, ) diff --git a/packages/vite/src/node/server/middlewares/indexHtml.ts b/packages/vite/src/node/server/middlewares/indexHtml.ts index f0eeb428a7bdc1..b88a9c4185ff61 100644 --- a/packages/vite/src/node/server/middlewares/indexHtml.ts +++ b/packages/vite/src/node/server/middlewares/indexHtml.ts @@ -4,7 +4,6 @@ import MagicString from 'magic-string' import type { SourceMapInput } from 'rollup' import type { Connect } from 'dep-types/connect' import type { DefaultTreeAdapterMap, Token } from 'parse5' -import type { Plugin } from '../../plugin' import type { IndexHtmlTransformHook } from '../../plugins/html' import { addToHTMLProxyCache, @@ -67,8 +66,7 @@ export function createDevHtmlTransformFn( originalUrl?: string, ) => Promise { const [preHooks, normalHooks, postHooks] = resolveHtmlTransforms( - // TODO: interaction between transformIndexHtml and plugin constructors - config.plugins.filter((plugin) => typeof plugin !== 'function') as Plugin[], + config.plugins, config.logger, ) const transformHooks = [ diff --git a/packages/vite/src/node/server/pluginContainer.ts b/packages/vite/src/node/server/pluginContainer.ts index fd798319ae7571..001034d3a094cf 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 { IsolatedPlugin, Plugin, PluginEnvironment } from '../plugin' +import type { EnvironmentPlugin, Plugin, PluginEnvironment } from '../plugin' import { combineSourcemaps, createDebugger, @@ -103,7 +103,7 @@ export interface PluginContainerOptions { writeFile?: (name: string, source: string | Uint8Array) => void } -export interface IsolatedPluginContainer { +export interface EnvironmentPluginContainer { options: InputOptions buildStart(options: InputOptions): Promise resolveId( @@ -146,11 +146,11 @@ type PluginContext = Omit< * instead of using environment.plugins to allow the creation of different * pipelines working with the same environment (used for createIdResolver). */ -export async function createIsolatedPluginContainer( +export async function createEnvironmentPluginContainer( environment: PluginEnvironment, - plugins: IsolatedPlugin[], + plugins: EnvironmentPlugin[], watcher?: FSWatcher, -): Promise { +): Promise { const { config, logger, From 2b2d010e8674d0f45216f60f01c0fee248ad33d7 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Mon, 20 May 2024 22:52:59 +0200 Subject: [PATCH 064/123] release: v6.0.0-alpha.14 --- packages/vite/CHANGELOG.md | 21 +++++++++++++++++++++ packages/vite/package.json | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/vite/CHANGELOG.md b/packages/vite/CHANGELOG.md index 6e0a6ad86974f8..3d28752596357f 100644 --- a/packages/vite/CHANGELOG.md +++ b/packages/vite/CHANGELOG.md @@ -1,3 +1,24 @@ +## 6.0.0-alpha.14 (2024-05-20) + +* feat: back to environmentPlugins hook (#16732) ([54c219e](https://github.com/vitejs/vite/commit/54c219e)), closes [#16732](https://github.com/vitejs/vite/issues/16732) +* fix: export isFileLoadingAllowed ([34d8518](https://github.com/vitejs/vite/commit/34d8518)) +* fix: mention `build.rollupOptions.output.manualChunks` instead of `build.rollupOutput.manualChunks` ([89378c0](https://github.com/vitejs/vite/commit/89378c0)), closes [#16721](https://github.com/vitejs/vite/issues/16721) +* fix: rely on the current environment in resolveId, if known ([8ee7fcd](https://github.com/vitejs/vite/commit/8ee7fcd)) +* fix: revert resolveId importer require ([3546d1e](https://github.com/vitejs/vite/commit/3546d1e)) +* fix(build): make SystemJSWrapRE match lazy (#16633) ([6583ad2](https://github.com/vitejs/vite/commit/6583ad2)), closes [#16633](https://github.com/vitejs/vite/issues/16633) +* fix(css): avoid generating empty JS files when JS files becomes empty but has CSS files imported (#1 ([95fe5a7](https://github.com/vitejs/vite/commit/95fe5a7)), closes [#16078](https://github.com/vitejs/vite/issues/16078) +* fix(css): page reload was not happening with .css?raw (#16455) ([8041846](https://github.com/vitejs/vite/commit/8041846)), closes [#16455](https://github.com/vitejs/vite/issues/16455) +* fix(deps): update all non-major dependencies (#16603) ([6711553](https://github.com/vitejs/vite/commit/6711553)), closes [#16603](https://github.com/vitejs/vite/issues/16603) +* fix(deps): update all non-major dependencies (#16660) ([bf2f014](https://github.com/vitejs/vite/commit/bf2f014)), closes [#16660](https://github.com/vitejs/vite/issues/16660) +* fix(error-logging): rollup errors weren't displaying id and codeframe (#16540) ([22dc196](https://github.com/vitejs/vite/commit/22dc196)), closes [#16540](https://github.com/vitejs/vite/issues/16540) +* fix(hmr): trigger page reload when calling invalidate on root module (#16636) ([2b61cc3](https://github.com/vitejs/vite/commit/2b61cc3)), closes [#16636](https://github.com/vitejs/vite/issues/16636) +* fix(logger): truncate log over 5000 characters long (#16581) ([b0b839a](https://github.com/vitejs/vite/commit/b0b839a)), closes [#16581](https://github.com/vitejs/vite/issues/16581) +* fix(sourcemap): improve sourcemap compatibility for vue2 (#16594) ([913c040](https://github.com/vitejs/vite/commit/913c040)), closes [#16594](https://github.com/vitejs/vite/issues/16594) +* chore(deps): update all non-major dependencies (#16722) ([b45922a](https://github.com/vitejs/vite/commit/b45922a)), closes [#16722](https://github.com/vitejs/vite/issues/16722) +* docs: correct proxy shorthand example (#15938) ([abf766e](https://github.com/vitejs/vite/commit/abf766e)), closes [#15938](https://github.com/vitejs/vite/issues/15938) + + + ## 6.0.0-alpha.13 (2024-05-20) * fix: types ([82111bf](https://github.com/vitejs/vite/commit/82111bf)) diff --git a/packages/vite/package.json b/packages/vite/package.json index 3141a6472e9cdf..7ab7bd59bb686e 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -1,6 +1,6 @@ { "name": "vite", - "version": "6.0.0-alpha.13", + "version": "6.0.0-alpha.14", "type": "module", "license": "MIT", "author": "Evan You", From acb208ed985dc487d9163c3d7b98370cb6c7aedc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arnaud=20Barr=C3=A9?= Date: Mon, 20 May 2024 23:46:04 +0200 Subject: [PATCH 065/123] fix hmr connected message on client --- packages/vite/src/client/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index 6beae7ff9ad9ed..9253802db6decd 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -175,7 +175,7 @@ const hmrClient = new HMRClient( async function handleMessage(payload: HMRPayload) { switch (payload.type) { case 'connected': - console.debug(`connected.`) + console.debug(`[vite] connected.`) hmrClient.messenger.flush() // proxy(nginx, docker) hmr ws maybe caused timeout, // so send ping package let ws keep alive. From 7e9dd25fe2cd8ee55330bd8eaa78ee624a69862a Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Mon, 20 May 2024 23:54:38 +0200 Subject: [PATCH 066/123] fix: only join base url in none ssr env --- packages/vite/src/node/plugins/importAnalysis.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index ff27fce4ca41bd..3da93418a2e3ed 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -390,7 +390,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { } // prepend base - url = joinUrlSegments(base, url) + if (!ssr) url = joinUrlSegments(base, url) } return [url, resolved.id] From 0166bfe90846df7f99c05f26f674627d1ee8a0b8 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Tue, 21 May 2024 00:05:04 +0200 Subject: [PATCH 067/123] fix: svelte back compat --- packages/vite/src/node/config.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index ef75e4b1536f55..64d8884186e9d1 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -823,6 +823,10 @@ export async function resolveConfig( const isBuild = command === 'build' + // run config hooks + const userPlugins = [...prePlugins, ...normalPlugins, ...postPlugins] + config = await runConfigHook(config, userPlugins, configEnv) + // Ensure default client and ssr environments // If there are present, ensure order { client, ssr, ...custom } config.environments ??= {} @@ -842,10 +846,6 @@ export async function resolveConfig( config.environments = { client: {}, ...config.environments } } - // run config hooks - const userPlugins = [...prePlugins, ...normalPlugins, ...postPlugins] - config = await runConfigHook(config, userPlugins, configEnv) - // Define logger const logger = createLogger(config.logLevel, { allowClearScreen: config.clearScreen, From d231966f03ecc31bab3282e663ab22b0519583af Mon Sep 17 00:00:00 2001 From: patak-dev Date: Tue, 21 May 2024 00:59:03 +0200 Subject: [PATCH 068/123] release: v6.0.0-alpha.15 --- packages/vite/CHANGELOG.md | 8 ++++++++ packages/vite/package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/vite/CHANGELOG.md b/packages/vite/CHANGELOG.md index 3d28752596357f..daacaacb504539 100644 --- a/packages/vite/CHANGELOG.md +++ b/packages/vite/CHANGELOG.md @@ -1,3 +1,11 @@ +## 6.0.0-alpha.15 (2024-05-20) + +* fix: only join base url in none ssr env ([7e9dd25](https://github.com/vitejs/vite/commit/7e9dd25)) +* fix: svelte back compat ([0166bfe](https://github.com/vitejs/vite/commit/0166bfe)) +* fix hmr connected message on client ([acb208e](https://github.com/vitejs/vite/commit/acb208e)) + + + ## 6.0.0-alpha.14 (2024-05-20) * feat: back to environmentPlugins hook (#16732) ([54c219e](https://github.com/vitejs/vite/commit/54c219e)), closes [#16732](https://github.com/vitejs/vite/issues/16732) diff --git a/packages/vite/package.json b/packages/vite/package.json index 7ab7bd59bb686e..623e67221869c4 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -1,6 +1,6 @@ { "name": "vite", - "version": "6.0.0-alpha.14", + "version": "6.0.0-alpha.15", "type": "module", "license": "MIT", "author": "Evan You", From 46f21b8754ad2c0758e6ec60ffc97fefdf075fb7 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Tue, 21 May 2024 01:49:47 +0200 Subject: [PATCH 069/123] fix: back compat for server.pluginContainer.buildStart --- packages/vite/src/node/server/pluginContainer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite/src/node/server/pluginContainer.ts b/packages/vite/src/node/server/pluginContainer.ts index 001034d3a094cf..f7d00cf4a4651e 100644 --- a/packages/vite/src/node/server/pluginContainer.ts +++ b/packages/vite/src/node/server/pluginContainer.ts @@ -910,7 +910,7 @@ export function createPluginContainer( }, async buildStart() { - // noop, buildStart will be called for each environment + ;(environments.client as DevEnvironment).pluginContainer!.buildStart({}) }, async resolveId(rawId, importer, options) { From 431455f44815273aa2d1824e6ff92cd376f8379d Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 21 May 2024 10:03:02 +0200 Subject: [PATCH 070/123] chore: remove extra symbol --- packages/vite/src/node/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 64d8884186e9d1..061b6fc757124a 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -139,7 +139,7 @@ export function defineConfig(config: UserConfigExport): UserConfigExport { export interface DevEnvironmentOptions { /** - * Files tßo be pre-transformed. Supports glob patterns. + * Files to be pre-transformed. Supports glob patterns. */ warmup?: string[] /** From 63de43a5cda8fbab73e3487a168161d4172ef31e Mon Sep 17 00:00:00 2001 From: patak-dev Date: Tue, 21 May 2024 10:18:49 +0200 Subject: [PATCH 071/123] fix: keep plugin with environmentPlugins hook --- packages/vite/src/node/plugin.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/vite/src/node/plugin.ts b/packages/vite/src/node/plugin.ts index 6d6edd63cea265..8ac758731ff32a 100644 --- a/packages/vite/src/node/plugin.ts +++ b/packages/vite/src/node/plugin.ts @@ -354,6 +354,7 @@ export async function resolveEnvironmentPlugins( ): Promise { const resolvedPlugins: EnvironmentPlugin[] = [] for (const plugin of environment.config.plugins) { + resolvedPlugins.push(plugin) if (plugin.environmentPlugins) { const environmentPlugins = await plugin.environmentPlugins(environment) if (environmentPlugins) { @@ -361,8 +362,6 @@ export async function resolveEnvironmentPlugins( await asyncFlattenEnvironmentPlugins(environmentPlugins) resolvedPlugins.push(...newPlugins) } - } else { - resolvedPlugins.push(plugin) } } return resolvedPlugins From 21225c9fdf6047e863d5c31a8445900916d3d754 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Tue, 21 May 2024 10:39:01 +0200 Subject: [PATCH 072/123] feat: improve Plugin option types --- packages/vite/src/node/index.ts | 9 +++++++-- packages/vite/src/node/plugin.ts | 25 ++++++++++--------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index a1a652f33ca3c3..58e19934c253dc 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -52,7 +52,13 @@ export type { DevEnvironmentOptions, ResolvedDevEnvironmentOptions, } from './config' -export type { PluginOption } from './plugin' +export type { + EnvironmentPlugin, + Plugin, + EnvironmentPluginOptionArray, + PluginOption, + HookHandler, +} from './plugin' export type { FilterPattern } from './utils' export type { CorsOptions, CorsOrigin, CommonServerOptions } from './http' export type { @@ -96,7 +102,6 @@ export type { SSROptions, SSRTarget, } from './ssr' -export type { EnvironmentPlugin, Plugin, HookHandler } from './plugin' export type { Logger, LogOptions, diff --git a/packages/vite/src/node/plugin.ts b/packages/vite/src/node/plugin.ts index 8ac758731ff32a..0327889d4d89ea 100644 --- a/packages/vite/src/node/plugin.ts +++ b/packages/vite/src/node/plugin.ts @@ -310,7 +310,7 @@ export interface Plugin extends EnvironmentPlugin { */ environmentPlugins?: ( environment: PluginEnvironment, - ) => PluginOption[] | undefined + ) => EnvironmentPluginOptionArray /** * @deprecated @@ -331,23 +331,18 @@ export type PluginWithRequiredHook = Plugin & { [P in K]: NonNullable } -export type MaybeEnvironmentPlugin = - | EnvironmentPlugin - | false - | null - | undefined +type Thenable = T | Promise +type FalsyPlugin = false | null | undefined -export type EnvironmentPluginOption = - | MaybeEnvironmentPlugin - | EnvironmentPluginOption[] - | Promise +export type EnvironmentPluginOption = Thenable< + EnvironmentPlugin | FalsyPlugin | EnvironmentPluginOption[] +> -export type MaybePlugin = Plugin | false | null | undefined +export type EnvironmentPluginOptionArray = Thenable< + EnvironmentPluginOption[] | FalsyPlugin +> -export type PluginOption = - | MaybePlugin - | PluginOption[] - | Promise +export type PluginOption = Thenable export async function resolveEnvironmentPlugins( environment: PluginEnvironment, From 53734a8faee7ed6e2323c9fbbfbebd30acd40acc Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Tue, 21 May 2024 12:24:37 +0200 Subject: [PATCH 073/123] feat: provide `environment` in every hook context --- package.json | 1 + packages/vite/package.json | 2 +- .../vite/src/node/__tests_dts__/plugin.ts | 38 ++++++++++ packages/vite/src/node/build.ts | 53 +++++++------- packages/vite/src/node/constants.ts | 29 ++++++++ packages/vite/src/node/plugin.ts | 69 ++++++++++++++++--- packages/vite/src/node/tsconfig.json | 3 +- packages/vite/src/node/typeUtils.ts | 22 ++++++ pnpm-lock.yaml | 7 ++ 9 files changed, 189 insertions(+), 35 deletions(-) create mode 100644 packages/vite/src/node/__tests_dts__/plugin.ts create mode 100644 packages/vite/src/node/typeUtils.ts diff --git a/package.json b/package.json index d9095b142e8f50..e23155fc698bee 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@babel/types": "^7.24.5", "@eslint-types/typescript-eslint": "^7.5.0", "@rollup/plugin-typescript": "^11.1.6", + "@type-challenges/utils": "^0.1.1", "@types/babel__core": "^7.20.5", "@types/babel__preset-env": "^7.9.6", "@types/convert-source-map": "^2.0.3", diff --git a/packages/vite/package.json b/packages/vite/package.json index 623e67221869c4..a486b0d1e32c76 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -78,7 +78,7 @@ "build-types-temp": "tsc --emitDeclarationOnly --outDir temp -p src/node", "build-types-roll": "rollup --config rollup.dts.config.ts --configPlugin typescript && rimraf temp", "build-types-check": "tsc --project tsconfig.check.json", - "typecheck": "tsc --noEmit", + "typecheck": "tsc --noEmit && tsc --noEmit -p src/node", "lint": "eslint --cache --ext .ts src/**", "format": "prettier --write --cache --parser typescript \"src/**/*.ts\"", "prepublishOnly": "npm run build" diff --git a/packages/vite/src/node/__tests_dts__/plugin.ts b/packages/vite/src/node/__tests_dts__/plugin.ts new file mode 100644 index 00000000000000..6844672accfa15 --- /dev/null +++ b/packages/vite/src/node/__tests_dts__/plugin.ts @@ -0,0 +1,38 @@ +/** + * This is a developement only file for testing types. + */ +import type { Plugin as RollupPlugin } from 'rollup' +import type { Equal, ExpectExtends, ExpectTrue } from '@type-challenges/utils' +import type { EnvironmentPlugin, PluginContextExtension } from '../plugin' +import type { ROLLUP_HOOKS } from '../constants' +import type { + GetHookContextMap, + NonNeverKeys, + RollupPluginHooks, +} from '../typeUtils' + +type EnvironmentPluginHooksContext = GetHookContextMap +type EnvironmentPluginHooksContextMatched = { + [K in keyof EnvironmentPluginHooksContext]: EnvironmentPluginHooksContext[K] extends PluginContextExtension + ? never + : false +} + +type HooksMissingExtension = NonNeverKeys +type HooksMissingInConstans = Exclude< + RollupPluginHooks, + (typeof ROLLUP_HOOKS)[number] +> + +export type cases = [ + // Ensure environment plugin hooks are superset of rollup plugin hooks + ExpectTrue>, + + // Ensure all Rollup hooks have Vite's plugin context extension + ExpectTrue>, + + // Ensure the `ROLLUP_HOOKS` constant is up-to-date + ExpectTrue>, +] + +export {} diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 8f83c3ce3ea132..1c93528fc8f60c 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -24,6 +24,7 @@ import { withTrailingSlash } from '../shared/utils' import { DEFAULT_ASSETS_INLINE_LIMIT, ESBUILD_MODULES_TARGET, + ROLLUP_HOOKS, VERSION, } from './constants' import type { @@ -67,6 +68,7 @@ import { webWorkerPostPlugin } from './plugins/worker' import { getHookHandler } from './plugins' import { Environment } from './environment' import type { Plugin, PluginContext } from './plugin' +import type { RollupPluginHooks } from './typeUtils' export interface BuildEnvironmentOptions { /** @@ -1124,23 +1126,30 @@ export function injectEnvironmentToHooks( plugin: Plugin, environment?: BuildEnvironment, ): Plugin { - const { - buildStart, - resolveId, - load, - transform, - generateBundle, - renderChunk, - } = plugin - return { - ...plugin, - resolveId: wrapEnvironmentResolveId(resolveId, environment), - load: wrapEnvironmentLoad(load, environment), - transform: wrapEnvironmentTransform(transform, environment), - buildStart: wrapEnvironmentHook(buildStart, environment), - generateBundle: wrapEnvironmentHook(generateBundle, environment), - renderChunk: wrapEnvironmentHook(renderChunk, environment), + const { resolveId, load, transform } = plugin + + const clone = { ...plugin } + + for (const hook of Object.keys(clone) as RollupPluginHooks[]) { + switch (hook) { + case 'resolveId': + clone[hook] = wrapEnvironmentResolveId(resolveId, environment) + break + case 'load': + clone[hook] = wrapEnvironmentLoad(load, environment) + break + case 'transform': + clone[hook] = wrapEnvironmentTransform(transform, environment) + break + default: + if (ROLLUP_HOOKS.includes(hook)) { + ;(clone as any)[hook] = wrapEnvironmentHook(clone[hook], environment) + } + break + } } + + return clone } function wrapEnvironmentResolveId( @@ -1227,6 +1236,8 @@ function wrapEnvironmentHook( if (!hook) return const fn = getHookHandler(hook) + if (typeof fn !== 'function') return hook + const handler: Plugin[HookName] = function ( this: PluginContext, ...args: any[] @@ -1248,14 +1259,8 @@ function injectEnvironmentInContext( context: Context, environment?: BuildEnvironment, ) { - return new Proxy(context, { - get(target, prop, receiver) { - if (prop === 'environment') { - return environment - } - return Reflect.get(target, prop, receiver) - }, - }) + context.environment ??= environment + return context } function injectSsrFlag>( diff --git a/packages/vite/src/node/constants.ts b/packages/vite/src/node/constants.ts index f729f9d6162298..1af75b0ea601e6 100644 --- a/packages/vite/src/node/constants.ts +++ b/packages/vite/src/node/constants.ts @@ -1,11 +1,40 @@ import path, { resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { readFileSync } from 'node:fs' +import type { RollupPluginHooks } from './typeUtils' const { version } = JSON.parse( readFileSync(new URL('../../package.json', import.meta.url)).toString(), ) +export const ROLLUP_HOOKS = [ + 'buildStart', + 'buildEnd', + 'renderStart', + 'renderError', + 'renderChunk', + 'writeBundle', + 'generateBundle', + 'banner', + 'footer', + 'augmentChunkHash', + 'outputOptions', + 'renderDynamicImport', + 'resolveFileUrl', + 'resolveImportMeta', + 'intro', + 'outro', + 'closeBundle', + 'closeWatcher', + 'load', + 'moduleParsed', + 'watchChange', + 'resolveDynamicImport', + 'resolveId', + 'shouldTransformCachedModule', + 'transform', +] satisfies RollupPluginHooks[] + export const VERSION = version as string export const DEFAULT_MAIN_FIELDS = [ diff --git a/packages/vite/src/node/plugin.ts b/packages/vite/src/node/plugin.ts index 0327889d4d89ea..a26f27de82ae6a 100644 --- a/packages/vite/src/node/plugin.ts +++ b/packages/vite/src/node/plugin.ts @@ -59,17 +59,21 @@ export type PluginEnvironment = | BuildEnvironment | ScanEnvironment -export interface PluginContext extends RollupPluginContext { +export interface PluginContextExtension { environment?: PluginEnvironment } -export interface ResolveIdPluginContext extends RollupPluginContext { - environment?: PluginEnvironment -} +export interface PluginContext + extends RollupPluginContext, + PluginContextExtension {} -export interface TransformPluginContext extends RollupTransformPluginContext { - environment?: PluginEnvironment -} +export interface ResolveIdPluginContext + extends RollupPluginContext, + PluginContextExtension {} + +export interface TransformPluginContext + extends RollupTransformPluginContext, + PluginContextExtension {} /** * There are two types of plugins in Vite. App plugins and environment plugins. @@ -87,7 +91,7 @@ type ModifyFunctionContext = Function_ extends ( ...parameters: infer Arguments ) => infer Return ? (this: NewContext, ...parameters: Arguments) => Return - : never + : Function_ type ModifyObjectHookContext< Handler, @@ -182,13 +186,60 @@ export interface EnvironmentPlugin extends RollupPlugin { ) => Promise | TransformResult > - // TODO: abstract to every hook in RollupPlugin? + // Extends rollup hooks + // ./__tests_dts__/plugin.ts will guard this to ensure we have all hooks + augmentChunkHash?: ModifyHookContext< + RollupPlugin['augmentChunkHash'], + PluginContext + > + banner?: ModifyHookContext['banner'], PluginContext> + buildEnd?: ModifyHookContext['buildEnd'], PluginContext> buildStart?: ModifyHookContext['buildStart'], PluginContext> + closeBundle?: ModifyHookContext['closeBundle'], PluginContext> + closeWatcher?: ModifyHookContext< + RollupPlugin['closeWatcher'], + PluginContext + > + footer?: ModifyHookContext['footer'], PluginContext> generateBundle?: ModifyHookContext< RollupPlugin['generateBundle'], PluginContext > + intro?: ModifyHookContext['intro'], PluginContext> + moduleParsed?: ModifyHookContext< + RollupPlugin['moduleParsed'], + PluginContext + > + outputOptions?: ModifyHookContext< + RollupPlugin['outputOptions'], + PluginContext + > + outro?: ModifyHookContext['outro'], PluginContext> renderChunk?: ModifyHookContext['renderChunk'], PluginContext> + renderDynamicImport?: ModifyHookContext< + RollupPlugin['renderDynamicImport'], + PluginContext + > + renderError?: ModifyHookContext['renderError'], PluginContext> + renderStart?: ModifyHookContext['renderStart'], PluginContext> + resolveDynamicImport?: ModifyHookContext< + RollupPlugin['resolveDynamicImport'], + PluginContext + > + resolveFileUrl?: ModifyHookContext< + RollupPlugin['resolveFileUrl'], + PluginContext + > + resolveImportMeta?: ModifyHookContext< + RollupPlugin['resolveImportMeta'], + PluginContext + > + watchChange?: ModifyHookContext['watchChange'], PluginContext> + writeBundle?: ModifyHookContext['writeBundle'], PluginContext> + shouldTransformCachedModule?: ModifyHookContext< + RollupPlugin['shouldTransformCachedModule'], + PluginContext + > } export interface Plugin extends EnvironmentPlugin { diff --git a/packages/vite/src/node/tsconfig.json b/packages/vite/src/node/tsconfig.json index a6108501ed36a4..dfbb49c465ee1d 100644 --- a/packages/vite/src/node/tsconfig.json +++ b/packages/vite/src/node/tsconfig.json @@ -4,10 +4,11 @@ "./", "../module-runner", "../dep-types", + "./__tests_dts__", "../types", "constants.ts" ], - "exclude": ["../**/__tests__"], + "exclude": ["../**/__tests__/"], "compilerOptions": { "lib": ["ESNext", "DOM"], "stripInternal": true, diff --git a/packages/vite/src/node/typeUtils.ts b/packages/vite/src/node/typeUtils.ts new file mode 100644 index 00000000000000..83acb5f5089357 --- /dev/null +++ b/packages/vite/src/node/typeUtils.ts @@ -0,0 +1,22 @@ +import type { + ObjectHook, + Plugin as RollupPlugin, + PluginContext as RollupPluginContext, +} from 'rollup' + +export type NonNeverKeys = { + [K in keyof T]: T[K] extends never ? never : K +}[keyof T] + +export type GetHookContextMap = { + [K in keyof Plugin]-?: Plugin[K] extends ObjectHook + ? T extends (this: infer This, ...args: any[]) => any + ? This extends RollupPluginContext + ? This + : never + : never + : never +} + +type RollupPluginHooksContext = GetHookContextMap +export type RollupPluginHooks = NonNeverKeys diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4a652f3e94cb77..7b95b9ebde5be2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,6 +33,9 @@ importers: '@rollup/plugin-typescript': specifier: ^11.1.6 version: 11.1.6(rollup@4.13.0)(tslib@2.6.2)(typescript@5.2.2) + '@type-challenges/utils': + specifier: ^0.1.1 + version: 0.1.1 '@types/babel__core': specifier: ^7.20.5 version: 7.20.5 @@ -4447,6 +4450,10 @@ packages: /@tsconfig/node16@1.0.2: resolution: {integrity: sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==} + /@type-challenges/utils@0.1.1: + resolution: {integrity: sha512-A7ljYfBM+FLw+NDyuYvGBJiCEV9c0lPWEAdzfOAkb3JFqfLl0Iv/WhWMMARHiRKlmmiD1g8gz/507yVvHdQUYA==} + dev: true + /@types/babel__core@7.20.5: resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} dependencies: From 89ec69c63f1e7dba4bb515447ad3f02707f89300 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Tue, 21 May 2024 12:54:50 +0200 Subject: [PATCH 074/123] fix: argument rollup types for plugin context instead of wrapping --- packages/vite/src/node/plugin.ts | 82 ++------------------------------ 1 file changed, 5 insertions(+), 77 deletions(-) diff --git a/packages/vite/src/node/plugin.ts b/packages/vite/src/node/plugin.ts index a26f27de82ae6a..83ba552dd97b74 100644 --- a/packages/vite/src/node/plugin.ts +++ b/packages/vite/src/node/plugin.ts @@ -75,6 +75,11 @@ export interface TransformPluginContext extends RollupTransformPluginContext, PluginContextExtension {} +// Argument Rollup types to have the PluginContextExtension +declare module 'rollup' { + export interface PluginContext extends PluginContextExtension {} +} + /** * 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 @@ -85,28 +90,6 @@ export interface TransformPluginContext * Environment Plugins are closer to regular rollup plugins. They can't define * app level hooks (like config, configResolved, configureServer, etc). */ - -type ModifyFunctionContext = Function_ extends ( - this: infer This, - ...parameters: infer Arguments -) => infer Return - ? (this: NewContext, ...parameters: Arguments) => Return - : Function_ - -type ModifyObjectHookContext< - Handler, - Object_ extends { handler: Handler }, - NewContext, -> = Object_ & { - handler: ModifyFunctionContext -} - -type ModifyHookContext = Hook extends { - handler: infer Handler -} - ? ModifyObjectHookContext - : ModifyFunctionContext - export interface EnvironmentPlugin extends RollupPlugin { /** * Perform custom handling of HMR updates. @@ -185,61 +168,6 @@ export interface EnvironmentPlugin extends RollupPlugin { }, ) => Promise | TransformResult > - - // Extends rollup hooks - // ./__tests_dts__/plugin.ts will guard this to ensure we have all hooks - augmentChunkHash?: ModifyHookContext< - RollupPlugin['augmentChunkHash'], - PluginContext - > - banner?: ModifyHookContext['banner'], PluginContext> - buildEnd?: ModifyHookContext['buildEnd'], PluginContext> - buildStart?: ModifyHookContext['buildStart'], PluginContext> - closeBundle?: ModifyHookContext['closeBundle'], PluginContext> - closeWatcher?: ModifyHookContext< - RollupPlugin['closeWatcher'], - PluginContext - > - footer?: ModifyHookContext['footer'], PluginContext> - generateBundle?: ModifyHookContext< - RollupPlugin['generateBundle'], - PluginContext - > - intro?: ModifyHookContext['intro'], PluginContext> - moduleParsed?: ModifyHookContext< - RollupPlugin['moduleParsed'], - PluginContext - > - outputOptions?: ModifyHookContext< - RollupPlugin['outputOptions'], - PluginContext - > - outro?: ModifyHookContext['outro'], PluginContext> - renderChunk?: ModifyHookContext['renderChunk'], PluginContext> - renderDynamicImport?: ModifyHookContext< - RollupPlugin['renderDynamicImport'], - PluginContext - > - renderError?: ModifyHookContext['renderError'], PluginContext> - renderStart?: ModifyHookContext['renderStart'], PluginContext> - resolveDynamicImport?: ModifyHookContext< - RollupPlugin['resolveDynamicImport'], - PluginContext - > - resolveFileUrl?: ModifyHookContext< - RollupPlugin['resolveFileUrl'], - PluginContext - > - resolveImportMeta?: ModifyHookContext< - RollupPlugin['resolveImportMeta'], - PluginContext - > - watchChange?: ModifyHookContext['watchChange'], PluginContext> - writeBundle?: ModifyHookContext['writeBundle'], PluginContext> - shouldTransformCachedModule?: ModifyHookContext< - RollupPlugin['shouldTransformCachedModule'], - PluginContext - > } export interface Plugin extends EnvironmentPlugin { From a3ee7f54759ea67d591d427c257e3f6f8f2ce754 Mon Sep 17 00:00:00 2001 From: Dominik G Date: Tue, 21 May 2024 16:52:02 +0200 Subject: [PATCH 075/123] fix: avoid duplicating values in shared optimizeDeps config (#16737) --- packages/vite/src/node/config.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 061b6fc757124a..60b4e6a0ea89c5 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -950,10 +950,8 @@ export async function resolveConfig( // Backward compatibility: merge environments.client.dev.optimizeDeps back into optimizeDeps const resolvedConfigEnvironmentsClient = resolvedEnvironments.client - const patchedOptimizeDeps = mergeConfig( - config.optimizeDeps ?? {}, - resolvedConfigEnvironmentsClient.dev?.optimizeDeps ?? {}, - ) + const patchedOptimizeDeps = + resolvedConfigEnvironmentsClient.dev?.optimizeDeps ?? {} const backwardCompatibleOptimizeDeps = { holdUntilCrawlEnd: true, ...patchedOptimizeDeps, @@ -983,10 +981,7 @@ export async function resolveConfig( ...config.ssr, external: resolvedEnvironments.ssr?.resolve.external, noExternal: resolvedEnvironments.ssr?.resolve.noExternal, - optimizeDeps: mergeConfig( - config.ssr?.optimizeDeps ?? {}, - resolvedEnvironments.ssr?.dev?.optimizeDeps ?? {}, - ), + optimizeDeps: resolvedEnvironments.ssr?.dev?.optimizeDeps, resolve: { ...config.ssr?.resolve, conditions: resolvedEnvironments.ssr?.resolve.conditions, From 4d03124bc2cd65eba8ab925989294a2dcef56821 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Wed, 22 May 2024 08:28:14 +0200 Subject: [PATCH 076/123] feat(types): expose PluginEnvironment type --- packages/vite/src/node/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index 58e19934c253dc..a26af09b029e86 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -54,6 +54,7 @@ export type { } from './config' export type { EnvironmentPlugin, + PluginEnvironment, Plugin, EnvironmentPluginOptionArray, PluginOption, From d86553ac51d3cea0c1d1aefc60ebd1e1b8e4b912 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arnaud=20Barr=C3=A9?= Date: Thu, 23 May 2024 16:03:15 +0200 Subject: [PATCH 077/123] chore: rename to BaseEnvironment (#16797) --- .../vite/src/node/__tests__/external.spec.ts | 4 +- .../src/node/__tests__/plugins/css.spec.ts | 5 +- .../src/node/__tests__/plugins/define.spec.ts | 4 +- packages/vite/src/node/baseEnvironment.ts | 103 ++++++++++++++++++ packages/vite/src/node/build.ts | 8 +- packages/vite/src/node/cli.ts | 7 +- packages/vite/src/node/config.ts | 10 +- packages/vite/src/node/environment.ts | 101 ++--------------- packages/vite/src/node/external.ts | 3 +- packages/vite/src/node/idResolver.ts | 65 ++++++----- packages/vite/src/node/index.ts | 1 - packages/vite/src/node/optimizer/scan.ts | 8 +- packages/vite/src/node/plugin.ts | 15 +-- packages/vite/src/node/plugins/asset.ts | 2 +- packages/vite/src/node/plugins/css.ts | 22 ++-- .../src/node/plugins/dynamicImportVars.ts | 3 +- packages/vite/src/node/server/environment.ts | 19 ++-- packages/vite/src/node/server/hmr.ts | 2 +- .../vite/src/node/server/pluginContainer.ts | 15 +-- 19 files changed, 204 insertions(+), 193 deletions(-) create mode 100644 packages/vite/src/node/baseEnvironment.ts diff --git a/packages/vite/src/node/__tests__/external.spec.ts b/packages/vite/src/node/__tests__/external.spec.ts index 59d7ef6d159a4a..a4c78519fd91ef 100644 --- a/packages/vite/src/node/__tests__/external.spec.ts +++ b/packages/vite/src/node/__tests__/external.spec.ts @@ -2,7 +2,7 @@ import { fileURLToPath } from 'node:url' import { describe, expect, test } from 'vitest' import { resolveConfig } from '../config' import { createIsConfiguredAsExternal } from '../external' -import { Environment } from '../environment' +import { PartialEnvironment } from '../baseEnvironment' describe('createIsConfiguredAsExternal', () => { test('default', async () => { @@ -25,6 +25,6 @@ async function createIsExternal(external?: true) { }, 'serve', ) - const environment = new Environment('ssr', resolvedConfig) + const environment = new PartialEnvironment('ssr', resolvedConfig) return createIsConfiguredAsExternal(environment) } diff --git a/packages/vite/src/node/__tests__/plugins/css.spec.ts b/packages/vite/src/node/__tests__/plugins/css.spec.ts index 8a6c6bd5d42737..efa3156a5ddb18 100644 --- a/packages/vite/src/node/__tests__/plugins/css.spec.ts +++ b/packages/vite/src/node/__tests__/plugins/css.spec.ts @@ -2,8 +2,6 @@ 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, @@ -12,6 +10,7 @@ import { getEmptyChunkReplacer, hoistAtRules, } from '../../plugins/css' +import { PartialEnvironment } from '../../baseEnvironment' describe('search css url function', () => { test('some spaces before it', () => { @@ -215,7 +214,7 @@ async function createCssPluginTransform( inlineConfig: InlineConfig = {}, ) { const config = await resolveConfig(inlineConfig, 'serve') - const environment = new Environment('client', config) as PluginEnvironment + const environment = new PartialEnvironment('client', config) const { transform, buildStart } = cssPlugin(config) diff --git a/packages/vite/src/node/__tests__/plugins/define.spec.ts b/packages/vite/src/node/__tests__/plugins/define.spec.ts index 3748746af23430..cd01281229d624 100644 --- a/packages/vite/src/node/__tests__/plugins/define.spec.ts +++ b/packages/vite/src/node/__tests__/plugins/define.spec.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from 'vitest' import { definePlugin } from '../../plugins/define' import { resolveConfig } from '../../config' -import { Environment } from '../../environment' +import { PartialEnvironment } from '../../baseEnvironment' async function createDefinePluginTransform( define: Record = {}, @@ -13,7 +13,7 @@ async function createDefinePluginTransform( build ? 'build' : 'serve', ) const instance = definePlugin(config) - const environment = new Environment(ssr ? 'ssr' : 'client', config) + const environment = new PartialEnvironment(ssr ? 'ssr' : 'client', config) return async (code: string) => { // @ts-expect-error transform should exist diff --git a/packages/vite/src/node/baseEnvironment.ts b/packages/vite/src/node/baseEnvironment.ts new file mode 100644 index 00000000000000..8de290697cc8bb --- /dev/null +++ b/packages/vite/src/node/baseEnvironment.ts @@ -0,0 +1,103 @@ +import colors from 'picocolors' +import type { Logger } from './logger' +import type { ResolvedConfig, ResolvedEnvironmentOptions } from './config' +import type { EnvironmentPlugin } from './plugin' + +export class PartialEnvironment { + name: string + config: ResolvedConfig + options: ResolvedEnvironmentOptions + logger: Logger + + constructor( + name: string, + config: ResolvedConfig, + options: ResolvedEnvironmentOptions = config.environments[name], + ) { + this.name = name + this.config = config + this.options = options + const environment = colors.dim(`(${this.name})`) + const colorIndex = + [...environment].reduce((acc, c) => acc + c.charCodeAt(0), 0) % + environmentColors.length + const infoColor = environmentColors[colorIndex || 0] + this.logger = { + get hasWarned() { + return config.logger.hasWarned + }, + info(msg, opts) { + return config.logger.info(msg, { + ...opts, + environment: infoColor(environment), + }) + }, + warn(msg, opts) { + return config.logger.warn(msg, { + ...opts, + environment: colors.yellow(environment), + }) + }, + warnOnce(msg, opts) { + return config.logger.warnOnce(msg, { + ...opts, + environment: colors.yellow(environment), + }) + }, + error(msg, opts) { + return config.logger.error(msg, { + ...opts, + environment: colors.red(environment), + }) + }, + clearScreen(type) { + return config.logger.clearScreen(type) + }, + hasErrorLogged(error) { + return config.logger.hasErrorLogged(error) + }, + } + } +} + +export class BaseEnvironment extends PartialEnvironment { + get plugins(): EnvironmentPlugin[] { + if (!this._plugins) + throw new Error( + `${this.name} environment.plugins called before initialized`, + ) + return this._plugins + } + + /** + * @internal + */ + _plugins: EnvironmentPlugin[] | undefined + /** + * @internal + */ + _initiated: boolean = false + + constructor( + name: string, + config: ResolvedConfig, + options: ResolvedEnvironmentOptions = config.environments[name], + ) { + super(name, config, options) + } +} + +/** + * This is used both to avoid users to hardcode conditions like + * !scan && !build => dev + */ +export class FutureCompatEnvironment extends BaseEnvironment { + mode = 'futureCompat' as const +} + +const environmentColors = [ + colors.blue, + colors.magenta, + colors.green, + colors.gray, +] diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 1c93528fc8f60c..a8b042c4569bb6 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -66,7 +66,7 @@ import { completeSystemWrapPlugin } from './plugins/completeSystemWrap' import { mergeConfig } from './publicUtils' import { webWorkerPostPlugin } from './plugins/worker' import { getHookHandler } from './plugins' -import { Environment } from './environment' +import { BaseEnvironment } from './baseEnvironment' import type { Plugin, PluginContext } from './plugin' import type { RollupPluginHooks } from './typeUtils' @@ -1458,7 +1458,7 @@ function areSeparateFolders(a: string, b: string) { ) } -export class BuildEnvironment extends Environment { +export class BuildEnvironment extends BaseEnvironment { mode = 'build' as const constructor( @@ -1481,10 +1481,10 @@ export class BuildEnvironment extends Environment { } async init(): Promise { - if (this._inited) { + if (this._initiated) { return } - this._inited = true + this._initiated = true this._plugins = await resolveEnvironmentPlugins(this) } } diff --git a/packages/vite/src/node/cli.ts b/packages/vite/src/node/cli.ts index fa93b125113f3e..290524284b7acd 100644 --- a/packages/vite/src/node/cli.ts +++ b/packages/vite/src/node/cli.ts @@ -9,6 +9,7 @@ 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') @@ -339,8 +340,6 @@ cli ) .action( async (root: string, options: { force?: boolean } & GlobalCLIOptions) => { - /* TODO: do we need this command? - filterDuplicateOptions(options) const { optimizeDeps } = await import('./optimizer') try { @@ -354,8 +353,7 @@ cli }, 'serve', ) - const environment = new Environment('client', config) - await optimizeDeps(environment, options.force, true) + await optimizeDeps(config, options.force, true) } catch (e) { createLogger(options.logLevel).error( colors.red(`error when optimizing deps:\n${e.stack}`), @@ -363,7 +361,6 @@ cli ) process.exit(1) } - */ }, ) diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 60b4e6a0ea89c5..a8f57e331e1ecf 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -23,7 +23,6 @@ import { import type { HookHandler, Plugin, - PluginEnvironment, PluginOption, PluginWithRequiredHook, } from './plugin' @@ -43,7 +42,6 @@ import { } 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' @@ -89,6 +87,7 @@ import { findNearestPackageData } from './packages' import { loadEnv, resolveEnvPrefix } from './env' import type { ResolvedSSROptions, SSROptions } from './ssr' import { resolveSSROptions } from './ssr' +import { FutureCompatEnvironment } from './baseEnvironment' const debug = createDebugger('vite:config') const promisifiedRealpath = promisify(fs.realpath) @@ -1223,11 +1222,10 @@ export async function resolveConfig( 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) + // environment so we can safely just use the FutureCompatEnvironment here + const environment = new FutureCompatEnvironment(environmentName, this) const pluginContainer = await createEnvironmentPluginContainer( - environment as PluginEnvironment, + environment, plugins, ) await pluginContainer.buildStart({}) diff --git a/packages/vite/src/node/environment.ts b/packages/vite/src/node/environment.ts index 8df034a721760a..1d748eedea96d1 100644 --- a/packages/vite/src/node/environment.ts +++ b/packages/vite/src/node/environment.ts @@ -1,96 +1,13 @@ -import colors from 'picocolors' -import type { Logger } from './logger' -import type { ResolvedConfig, ResolvedEnvironmentOptions } from './config' -import type { EnvironmentPlugin } from './plugin' +import type { DevEnvironment } from './server/environment' +import type { BuildEnvironment } from './build' +import type { ScanEnvironment } from './optimizer/scan' +import { type FutureCompatEnvironment } from './baseEnvironment' -export class Environment { - name: string - - config: ResolvedConfig - options: ResolvedEnvironmentOptions - - get plugins(): EnvironmentPlugin[] { - if (!this._plugins) - throw new Error( - `${this.name} environment.plugins called before initialized`, - ) - return this._plugins - } - /** - * @internal - */ - _plugins: EnvironmentPlugin[] | 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, -] +export type Environment = + | DevEnvironment + | BuildEnvironment + | ScanEnvironment + | FutureCompatEnvironment export function cachedByEnvironment( create: (environment: Environment) => Data, diff --git a/packages/vite/src/node/external.ts b/packages/vite/src/node/external.ts index 6d593cbb0d42b5..58d35435a51959 100644 --- a/packages/vite/src/node/external.ts +++ b/packages/vite/src/node/external.ts @@ -9,6 +9,7 @@ import { isBuiltin, } from './utils' import type { Environment } from './environment' +import type { PartialEnvironment } from './baseEnvironment' const debug = createDebugger('vite:external') @@ -49,7 +50,7 @@ export function isConfiguredAsExternal( } export function createIsConfiguredAsExternal( - environment: Environment, + environment: PartialEnvironment, ): (id: string, importer?: string) => boolean { const { config, options } = environment const { root } = config diff --git a/packages/vite/src/node/idResolver.ts b/packages/vite/src/node/idResolver.ts index bcf0f124a2677d..6cd62a93b3dcf6 100644 --- a/packages/vite/src/node/idResolver.ts +++ b/packages/vite/src/node/idResolver.ts @@ -1,16 +1,16 @@ 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 { EnvironmentPluginContainer } from './server/pluginContainer' import { createEnvironmentPluginContainer } from './server/pluginContainer' import { resolvePlugin } from './plugins/resolve' import type { InternalResolveOptions } from './plugins/resolve' import { getFsUtils } from './fsUtils' +import type { Environment } from './environment' +import type { PartialEnvironment } from './baseEnvironment' export type ResolveIdFn = ( - environment: Environment, + environment: PartialEnvironment, id: string, importer?: string, aliasOnly?: boolean, @@ -26,48 +26,57 @@ export function createIdResolver( ): ResolveIdFn { const scan = options?.scan - const pluginContainerMap = new Map() + const pluginContainerMap = new Map< + PartialEnvironment, + EnvironmentPluginContainer + >() async function resolve( - environment: PluginEnvironment, + environment: PartialEnvironment, id: string, importer?: string, ): Promise { let pluginContainer = pluginContainerMap.get(environment) if (!pluginContainer) { - pluginContainer = await createEnvironmentPluginContainer(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, - }), - ]) + pluginContainer = await createEnvironmentPluginContainer( + environment as 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, + }), + ], + ) pluginContainerMap.set(environment, pluginContainer) } return await pluginContainer.resolveId(id, importer, { scan }) } const aliasOnlyPluginContainerMap = new Map< - Environment, + PartialEnvironment, EnvironmentPluginContainer >() async function resolveAlias( - environment: PluginEnvironment, + environment: PartialEnvironment, id: string, importer?: string, ): Promise { let pluginContainer = aliasOnlyPluginContainerMap.get(environment) if (!pluginContainer) { - pluginContainer = await createEnvironmentPluginContainer(environment, [ - aliasPlugin({ entries: config.resolve.alias }), // TODO: resolve.alias per environment? - ]) + pluginContainer = await createEnvironmentPluginContainer( + environment as Environment, + [ + aliasPlugin({ entries: config.resolve.alias }), // TODO: resolve.alias per environment? + ], + ) aliasOnlyPluginContainerMap.set(environment, pluginContainer) } return await pluginContainer.resolveId(id, importer, { scan }) @@ -77,11 +86,7 @@ export function createIdResolver( 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, - ) + const resolved = await resolveFn(environment, id, importer) return resolved?.id } } diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index a26af09b029e86..58e19934c253dc 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -54,7 +54,6 @@ export type { } from './config' export type { EnvironmentPlugin, - PluginEnvironment, Plugin, EnvironmentPluginOptionArray, PluginOption, diff --git a/packages/vite/src/node/optimizer/scan.ts b/packages/vite/src/node/optimizer/scan.ts index 18fc7497b6dcbe..c4164f9cab8ab3 100644 --- a/packages/vite/src/node/optimizer/scan.ts +++ b/packages/vite/src/node/optimizer/scan.ts @@ -38,13 +38,13 @@ import { import { resolveEnvironmentPlugins } from '../plugin' import type { EnvironmentPluginContainer } from '../server/pluginContainer' import { createEnvironmentPluginContainer } from '../server/pluginContainer' -import { Environment } from '../environment' +import { BaseEnvironment } from '../baseEnvironment' import type { DevEnvironment } from '../server/environment' import { transformGlobImport } from '../plugins/importMetaGlob' import { cleanUrl } from '../../shared/utils' import { loadTsconfigJsonForFile } from '../plugins/esbuild' -export class ScanEnvironment extends Environment { +export class ScanEnvironment extends BaseEnvironment { mode = 'scan' as const get pluginContainer(): EnvironmentPluginContainer { @@ -60,10 +60,10 @@ export class ScanEnvironment extends Environment { _pluginContainer: EnvironmentPluginContainer | undefined async init(): Promise { - if (this._inited) { + if (this._initiated) { return } - this._inited = true + this._initiated = true this._plugins = await resolveEnvironmentPlugins(this) this._pluginContainer = await createEnvironmentPluginContainer( this, diff --git a/packages/vite/src/node/plugin.ts b/packages/vite/src/node/plugin.ts index 83ba552dd97b74..0a931bd6114c53 100644 --- a/packages/vite/src/node/plugin.ts +++ b/packages/vite/src/node/plugin.ts @@ -20,9 +20,7 @@ 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' +import type { Environment } from './environment' /** * Vite plugins extends the Rollup plugin interface with a few extra @@ -54,13 +52,8 @@ import type { ScanEnvironment } from './optimizer/scan' * check if they have access to dev specific APIs. */ -export type PluginEnvironment = - | DevEnvironment - | BuildEnvironment - | ScanEnvironment - export interface PluginContextExtension { - environment?: PluginEnvironment + environment?: Environment } export interface PluginContext @@ -288,7 +281,7 @@ export interface Plugin extends EnvironmentPlugin { * Inject per environment plugins after the shared plugin */ environmentPlugins?: ( - environment: PluginEnvironment, + environment: Environment, ) => EnvironmentPluginOptionArray /** @@ -324,7 +317,7 @@ export type EnvironmentPluginOptionArray = Thenable< export type PluginOption = Thenable export async function resolveEnvironmentPlugins( - environment: PluginEnvironment, + environment: Environment, ): Promise { const resolvedPlugins: EnvironmentPlugin[] = [] for (const plugin of environment.config.plugins) { diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index bf36130d8580ca..05862d3e05f967 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -11,7 +11,6 @@ import { toOutputFilePathInJS, } from '../build' import type { Plugin, PluginContext } from '../plugin' -import type { Environment } from '../environment' import type { ResolvedConfig } from '../config' import { checkPublicFile } from '../publicDir' import { @@ -27,6 +26,7 @@ import { } from '../utils' import { DEFAULT_ASSETS_INLINE_LIMIT, FS_PREFIX } from '../constants' import { cleanUrl, withTrailingSlash } from '../../shared/utils' +import type { Environment } from '../environment' // referenceId is base64url but replaces - with $ export const assetUrlRE = /__VITE_ASSET__([\w$]+)__(?:\$_(.*?)__)?/g diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 3234f1929e7c0f..8664ac35c76088 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -41,7 +41,6 @@ import { } from '../constants' import type { ResolvedConfig } from '../config' import type { Plugin } from '../plugin' -import { Environment } from '../environment' import { checkPublicFile } from '../publicDir' import { arraify, @@ -71,6 +70,7 @@ import type { Logger } from '../logger' import { cleanUrl, slash } from '../../shared/utils' import { createIdResolver } from '../idResolver' import type { ResolveIdFn } from '../idResolver' +import { PartialEnvironment } from '../baseEnvironment' import { addToHTMLProxyTransformResult } from './html' import { assetUrlRE, @@ -1089,7 +1089,7 @@ function getCssResolversKeys( } async function compileCSSPreprocessors( - environment: Environment, + environment: PartialEnvironment, id: string, lang: PreprocessLang, code: string, @@ -1171,7 +1171,7 @@ function getAtImportResolvers(config: ResolvedConfig) { } async function compileCSS( - environment: Environment, + environment: PartialEnvironment, id: string, code: string, workerController: PreprocessorWorkerController, @@ -1480,7 +1480,7 @@ export async function preprocessCSS( // 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), + environment: PartialEnvironment = new PartialEnvironment('client', config), ): Promise { let workerController = preprocessorWorkerControllerCache.get(config) @@ -1929,7 +1929,7 @@ type StylusStylePreprocessorOptions = StylePreprocessorOptions & { type StylePreprocessor = { process: ( - environment: Environment, + environment: PartialEnvironment, source: string, root: string, options: StylePreprocessorOptions, @@ -1940,7 +1940,7 @@ type StylePreprocessor = { type SassStylePreprocessor = { process: ( - environment: Environment, + environment: PartialEnvironment, source: string, root: string, options: SassStylePreprocessorOptions, @@ -1951,7 +1951,7 @@ type SassStylePreprocessor = { type StylusStylePreprocessor = { process: ( - environment: Environment, + environment: PartialEnvironment, source: string, root: string, options: StylusStylePreprocessorOptions, @@ -2048,7 +2048,7 @@ function fixScssBugImportValue( // .scss/.sass processor const makeScssWorker = ( - environment: Environment, + environment: PartialEnvironment, resolvers: CSSAtImportResolvers, alias: Alias[], maxWorkers: number | undefined, @@ -2225,7 +2225,7 @@ const scssProcessor = ( * root file as base. */ async function rebaseUrls( - environment: Environment, + environment: PartialEnvironment, file: string, rootFile: string, alias: Alias[], @@ -2292,7 +2292,7 @@ async function rebaseUrls( // .less const makeLessWorker = ( - environment: Environment, + environment: PartialEnvironment, resolvers: CSSAtImportResolvers, alias: Alias[], maxWorkers: number | undefined, @@ -2714,7 +2714,7 @@ const importLightningCSS = createCachedImport(() => import('lightningcss')) async function compileLightningCSS( id: string, src: string, - environment: Environment, + environment: PartialEnvironment, urlReplacer?: CssUrlReplacer, ): ReturnType { const { config } = environment diff --git a/packages/vite/src/node/plugins/dynamicImportVars.ts b/packages/vite/src/node/plugins/dynamicImportVars.ts index 625f8fcc2d199c..534f52ed690121 100644 --- a/packages/vite/src/node/plugins/dynamicImportVars.ts +++ b/packages/vite/src/node/plugins/dynamicImportVars.ts @@ -5,8 +5,6 @@ import type { ImportSpecifier } from 'es-module-lexer' import { parse as parseJS } from 'acorn' import { dynamicImportToGlob } from '@rollup/plugin-dynamic-import-vars' import type { Plugin } from '../plugin' -import type { Environment } from '../environment' -import { cachedByEnvironment } from '../environment' import type { ResolvedConfig } from '../config' import { CLIENT_ENTRY } from '../constants' import { createIdResolver } from '../idResolver' @@ -19,6 +17,7 @@ import { transformStableResult, urlRE, } from '../utils' +import { type Environment, cachedByEnvironment } from '../environment' import { toAbsoluteGlob } from './importMetaGlob' import { hasViteIgnoreRE } from './importAnalysis' import { workerOrSharedWorkerRE } from './worker' diff --git a/packages/vite/src/node/server/environment.ts b/packages/vite/src/node/server/environment.ts index 60e7b507ca557c..a4ddd12f06fb6e 100644 --- a/packages/vite/src/node/server/environment.ts +++ b/packages/vite/src/node/server/environment.ts @@ -1,7 +1,7 @@ import type { FetchResult } from 'vite/module-runner' import type { FSWatcher } from 'dep-types/chokidar' import colors from 'picocolors' -import { Environment } from '../environment' +import { BaseEnvironment } from '../baseEnvironment' import { ERR_OUTDATED_OPTIMIZED_DEP } from '../plugins/optimizedDeps' import type { EnvironmentOptions, @@ -21,14 +21,14 @@ 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 { transformRequest } from './transformRequest' +import type { EnvironmentPluginContainer } from './pluginContainer' import { ERR_CLOSED_SERVER, createEnvironmentPluginContainer, } from './pluginContainer' import type { RemoteEnvironmentTransport } from './environmentTransport' -import type { EnvironmentPluginContainer } from './pluginContainer' export interface DevEnvironmentSetup { hot?: false | HMRChannel @@ -40,8 +40,7 @@ export interface DevEnvironmentSetup { depsOptimizer?: DepsOptimizer } -// Maybe we will rename this to DevEnvironment -export class DevEnvironment extends Environment { +export class DevEnvironment extends BaseEnvironment { mode = 'dev' as const // TODO: should this be 'serve'? moduleGraph: EnvironmentModuleGraph @@ -147,8 +146,8 @@ export class DevEnvironment extends Environment { 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 + // environments `noDiscovery` has no effect and a simpler explicit deps + // optimizer is used that only optimizes explicitly 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 @@ -161,10 +160,10 @@ export class DevEnvironment extends Environment { } async init(): Promise { - if (this._inited) { + if (this._initiated) { return } - this._inited = true + this._initiated = true this._plugins = await resolveEnvironmentPlugins(this) this._pluginContainer = await createEnvironmentPluginContainer( this, @@ -207,9 +206,9 @@ export class DevEnvironment extends Environment { async close(): Promise { this._closing = true + this._crawlEndFinder?.cancel() await Promise.allSettled([ this.pluginContainer.close(), - this._crawlEndFinder?.cancel(), this.depsOptimizer?.close(), (async () => { while (this._pendingRequests.size > 0) { diff --git a/packages/vite/src/node/server/hmr.ts b/packages/vite/src/node/server/hmr.ts index 7c6a64e87769a8..247be05fc0a7d9 100644 --- a/packages/vite/src/node/server/hmr.ts +++ b/packages/vite/src/node/server/hmr.ts @@ -6,13 +6,13 @@ import colors from 'picocolors' import type { CustomPayload, HMRPayload, Update } from 'types/hmrPayload' import type { RollupError } from 'rollup' import { CLIENT_DIR } from '../constants' -import type { Environment } from '../environment' import { createDebugger, normalizePath } from '../utils' import type { InferCustomEventPayload, ViteDevServer } from '..' import { getHookHandler } from '../plugins' import { isCSSRequest } from '../plugins/css' import { isExplicitImportRequired } from '../plugins/importAnalysis' import { getEnvFilesForMode } from '../env' +import type { Environment } from '../environment' import { withTrailingSlash, wrapId } from '../../shared/utils' import type { Plugin } from '../plugin' import type { EnvironmentModuleNode } from './moduleGraph' diff --git a/packages/vite/src/node/server/pluginContainer.ts b/packages/vite/src/node/server/pluginContainer.ts index f7d00cf4a4651e..dbf25c65ecbba4 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 { EnvironmentPlugin, Plugin, PluginEnvironment } from '../plugin' +import type { EnvironmentPlugin, Plugin } from '../plugin' import { combineSourcemaps, createDebugger, @@ -78,6 +78,7 @@ import { import { FS_PREFIX } from '../constants' import { createPluginHookUtils, getHookHandler } from '../plugins' import { cleanUrl, unwrapId } from '../../shared/utils' +import type { Environment } from '../environment' import type { DevEnvironment } from './environment' import { buildErrorMessage } from './middlewares/error' import type { EnvironmentModuleNode } from './moduleGraph' @@ -147,7 +148,7 @@ type PluginContext = Omit< * pipelines working with the same environment (used for createIdResolver). */ export async function createEnvironmentPluginContainer( - environment: PluginEnvironment, + environment: Environment, plugins: EnvironmentPlugin[], watcher?: FSWatcher, ): Promise { @@ -263,7 +264,7 @@ export async function createEnvironmentPluginContainer( // 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 + environment: Environment meta = minimalContext.meta ssr = false _scan = false @@ -850,7 +851,7 @@ export interface PluginContainer { custom?: CustomPluginOptions skip?: Set ssr?: boolean - environment?: PluginEnvironment + environment?: Environment /** * @internal */ @@ -864,14 +865,14 @@ export interface PluginContainer { options?: { inMap?: SourceDescription['map'] ssr?: boolean - environment?: PluginEnvironment + environment?: Environment }, ): Promise<{ code: string; map: SourceMap | { mappings: '' } | null }> load( id: string, options?: { ssr?: boolean - environment?: PluginEnvironment + environment?: Environment }, ): Promise watchChange( @@ -892,7 +893,7 @@ export interface PluginContainer { **/ export function createPluginContainer( - environments: Record, + environments: Record, ): PluginContainer { // Backward compatibility // Users should call pluginContainer.resolveId (and load/transform) passing the environment they want to work with From e36f2f3f417dab6b9ea9c1a457aff55a9849db6a Mon Sep 17 00:00:00 2001 From: patak <583075+patak-dev@users.noreply.github.com> Date: Thu, 23 May 2024 16:21:09 +0200 Subject: [PATCH 078/123] feat: only shared plugins (#17289) --- docs/guide/api-vite-environment.md | 53 ++++++----------- .../vite/src/node/__tests_dts__/plugin.ts | 12 ++-- packages/vite/src/node/baseEnvironment.ts | 6 +- packages/vite/src/node/build.ts | 3 +- packages/vite/src/node/index.ts | 8 +-- packages/vite/src/node/optimizer/scan.ts | 2 +- packages/vite/src/node/plugin.ts | 57 ++++--------------- packages/vite/src/node/server/environment.ts | 2 +- .../vite/src/node/server/pluginContainer.ts | 4 +- 9 files changed, 46 insertions(+), 101 deletions(-) diff --git a/docs/guide/api-vite-environment.md b/docs/guide/api-vite-environment.md index fac53cbc67b397..dcc9bf70b185f2 100644 --- a/docs/guide/api-vite-environment.md +++ b/docs/guide/api-vite-environment.md @@ -58,7 +58,7 @@ class DevEnvironment { * Resolved plugins for this environment, including the ones * created using the per-environment `create` hook */ - plugins: EnvironmentPlugin[] + plugins: Plugin[] /** * Allows to resolve, load, and transform code through the * environment plugins pipeline @@ -579,19 +579,25 @@ The hook can choose to: ### Per-environment Plugins -A plugin can now also be a constructor to lazily create per-environment plugins. +A plugin can define what are the environments it should apply to with the `applyToEnvironment` function. ```js -function perEnvironmentPlugin() { - return (environment: Environment) => { - // Return a plugin, an array, a Promise, or a falsy value for each environment - if (!passesCondition(environment)) { - return undefined - } - return [ - createEnvironmentPlugin(environment), - otherPlugin(environment) - ] +const UnoCssPlugin = () => { + // shared global state + return { + buildStart() { + // init per environment state with WeakMap, this.environment + }, + configureServer() { + // use global hooks normally + }, + applyToEnvironment(environment) { + // return true if this plugin should be active in this environment + // if the function isn't provided, the plugin is active in all environments + }, + resolveId(id, importer) { + // only called for environments this plugin apply to + }, } } ``` @@ -881,29 +887,6 @@ function myPlugin() { } ``` -And for per-environment plugins: - -```js -function myPlugin() { - // Share state among all environments in dev and build - const sharedState = ... - - return { - name: 'with-environment-plugins', - environmentPlugins(environment) { - // Isolated state for each environment during dev and build - const isolatedState = ... - return { - name: 'per-environment-plugin', - transform(code, id) { ... }, - } - }, - // Opt-in into a single instance for all environments - sharedDuringBuild: true - } -} -``` - ## Backward Compatibility The current Vite server API will be deprecated but keep working during the next major. diff --git a/packages/vite/src/node/__tests_dts__/plugin.ts b/packages/vite/src/node/__tests_dts__/plugin.ts index 6844672accfa15..5b4ebeb82895c8 100644 --- a/packages/vite/src/node/__tests_dts__/plugin.ts +++ b/packages/vite/src/node/__tests_dts__/plugin.ts @@ -1,9 +1,9 @@ /** - * This is a developement only file for testing types. + * This is a development only file for testing types. */ import type { Plugin as RollupPlugin } from 'rollup' import type { Equal, ExpectExtends, ExpectTrue } from '@type-challenges/utils' -import type { EnvironmentPlugin, PluginContextExtension } from '../plugin' +import type { Plugin, PluginContextExtension } from '../plugin' import type { ROLLUP_HOOKS } from '../constants' import type { GetHookContextMap, @@ -11,7 +11,7 @@ import type { RollupPluginHooks, } from '../typeUtils' -type EnvironmentPluginHooksContext = GetHookContextMap +type EnvironmentPluginHooksContext = GetHookContextMap type EnvironmentPluginHooksContextMatched = { [K in keyof EnvironmentPluginHooksContext]: EnvironmentPluginHooksContext[K] extends PluginContextExtension ? never @@ -19,20 +19,20 @@ type EnvironmentPluginHooksContextMatched = { } type HooksMissingExtension = NonNeverKeys -type HooksMissingInConstans = Exclude< +type HooksMissingInConstants = Exclude< RollupPluginHooks, (typeof ROLLUP_HOOKS)[number] > export type cases = [ // Ensure environment plugin hooks are superset of rollup plugin hooks - ExpectTrue>, + ExpectTrue>, // Ensure all Rollup hooks have Vite's plugin context extension ExpectTrue>, // Ensure the `ROLLUP_HOOKS` constant is up-to-date - ExpectTrue>, + ExpectTrue>, ] export {} diff --git a/packages/vite/src/node/baseEnvironment.ts b/packages/vite/src/node/baseEnvironment.ts index 8de290697cc8bb..b03d8164b08974 100644 --- a/packages/vite/src/node/baseEnvironment.ts +++ b/packages/vite/src/node/baseEnvironment.ts @@ -1,7 +1,7 @@ import colors from 'picocolors' import type { Logger } from './logger' import type { ResolvedConfig, ResolvedEnvironmentOptions } from './config' -import type { EnvironmentPlugin } from './plugin' +import type { Plugin } from './plugin' export class PartialEnvironment { name: string @@ -61,7 +61,7 @@ export class PartialEnvironment { } export class BaseEnvironment extends PartialEnvironment { - get plugins(): EnvironmentPlugin[] { + get plugins(): Plugin[] { if (!this._plugins) throw new Error( `${this.name} environment.plugins called before initialized`, @@ -72,7 +72,7 @@ export class BaseEnvironment extends PartialEnvironment { /** * @internal */ - _plugins: EnvironmentPlugin[] | undefined + _plugins: Plugin[] | undefined /** * @internal */ diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index a8b042c4569bb6..0742a90da7c6bf 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -1480,12 +1480,13 @@ export class BuildEnvironment extends BaseEnvironment { super(name, config, options) } + // TODO: This could be sync, discuss if applyToEnvironment should support async async init(): Promise { if (this._initiated) { return } this._initiated = true - this._plugins = await resolveEnvironmentPlugins(this) + this._plugins = resolveEnvironmentPlugins(this) } } diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index 58e19934c253dc..ce5fec1b02e969 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -52,13 +52,7 @@ export type { DevEnvironmentOptions, ResolvedDevEnvironmentOptions, } from './config' -export type { - EnvironmentPlugin, - Plugin, - EnvironmentPluginOptionArray, - PluginOption, - HookHandler, -} from './plugin' +export type { Plugin, PluginOption, HookHandler } from './plugin' export type { FilterPattern } from './utils' export type { CorsOptions, CorsOrigin, CommonServerOptions } from './http' export type { diff --git a/packages/vite/src/node/optimizer/scan.ts b/packages/vite/src/node/optimizer/scan.ts index c4164f9cab8ab3..02f547790f339f 100644 --- a/packages/vite/src/node/optimizer/scan.ts +++ b/packages/vite/src/node/optimizer/scan.ts @@ -64,7 +64,7 @@ export class ScanEnvironment extends BaseEnvironment { return } this._initiated = true - this._plugins = await resolveEnvironmentPlugins(this) + this._plugins = resolveEnvironmentPlugins(this) this._pluginContainer = await createEnvironmentPluginContainer( this, this.plugins, diff --git a/packages/vite/src/node/plugin.ts b/packages/vite/src/node/plugin.ts index 0a931bd6114c53..9799dca74cbb95 100644 --- a/packages/vite/src/node/plugin.ts +++ b/packages/vite/src/node/plugin.ts @@ -83,7 +83,7 @@ declare module 'rollup' { * Environment Plugins are closer to regular rollup plugins. They can't define * app level hooks (like config, configResolved, configureServer, etc). */ -export interface EnvironmentPlugin extends RollupPlugin { +export interface Plugin extends RollupPlugin { /** * Perform custom handling of HMR updates. * The handler receives a context containing changed filename, timestamp, a @@ -161,9 +161,6 @@ export interface EnvironmentPlugin extends RollupPlugin { }, ) => Promise | TransformResult > -} - -export interface Plugin extends EnvironmentPlugin { /** * Opt-in this plugin into the shared plugins pipeline. * For backward-compatibility, plugins are re-recreated for each environment @@ -194,6 +191,11 @@ export interface Plugin extends EnvironmentPlugin { | 'serve' | 'build' | ((this: void, config: UserConfig, env: ConfigEnv) => boolean) + /** + * Define environments where this plugin should be active + * By default, the plugin is active in all environments + */ + applyToEnvironment?: (environment: Environment) => boolean /** * Modify vite config before it's resolved. The hook can either mutate the * passed-in config directly, or return a partial config object that will be @@ -277,12 +279,6 @@ export interface Plugin extends EnvironmentPlugin { * `{ order: 'pre', handler: hook }` */ transformIndexHtml?: IndexHtmlTransform - /** - * Inject per environment plugins after the shared plugin - */ - environmentPlugins?: ( - environment: Environment, - ) => EnvironmentPluginOptionArray /** * @deprecated @@ -304,43 +300,14 @@ export type PluginWithRequiredHook = Plugin & { } type Thenable = T | Promise -type FalsyPlugin = false | null | undefined - -export type EnvironmentPluginOption = Thenable< - EnvironmentPlugin | FalsyPlugin | EnvironmentPluginOption[] -> -export type EnvironmentPluginOptionArray = Thenable< - EnvironmentPluginOption[] | FalsyPlugin -> +type FalsyPlugin = false | null | undefined export type PluginOption = Thenable -export async function resolveEnvironmentPlugins( - environment: Environment, -): Promise { - const resolvedPlugins: EnvironmentPlugin[] = [] - for (const plugin of environment.config.plugins) { - resolvedPlugins.push(plugin) - if (plugin.environmentPlugins) { - const environmentPlugins = await plugin.environmentPlugins(environment) - if (environmentPlugins) { - const newPlugins = - await asyncFlattenEnvironmentPlugins(environmentPlugins) - resolvedPlugins.push(...newPlugins) - } - } - } - return resolvedPlugins -} - -async function asyncFlattenEnvironmentPlugins( - plugins: EnvironmentPluginOption[], -): Promise { - do { - plugins = ((await Promise.all(plugins)) as any[]) - .flat(Infinity) - .filter(Boolean) as EnvironmentPluginOption[] - } while (plugins.some((v: any) => v?.then)) - return plugins as EnvironmentPlugin[] +export function resolveEnvironmentPlugins(environment: Environment): Plugin[] { + return environment.config.plugins.filter( + (plugin) => + !plugin.applyToEnvironment || plugin.applyToEnvironment(environment), + ) } diff --git a/packages/vite/src/node/server/environment.ts b/packages/vite/src/node/server/environment.ts index a4ddd12f06fb6e..742186e9a3ae1b 100644 --- a/packages/vite/src/node/server/environment.ts +++ b/packages/vite/src/node/server/environment.ts @@ -164,7 +164,7 @@ export class DevEnvironment extends BaseEnvironment { return } this._initiated = true - this._plugins = await resolveEnvironmentPlugins(this) + this._plugins = resolveEnvironmentPlugins(this) this._pluginContainer = await createEnvironmentPluginContainer( this, this._plugins, diff --git a/packages/vite/src/node/server/pluginContainer.ts b/packages/vite/src/node/server/pluginContainer.ts index dbf25c65ecbba4..07557b9b102c6e 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 { EnvironmentPlugin, Plugin } from '../plugin' +import type { Plugin } from '../plugin' import { combineSourcemaps, createDebugger, @@ -149,7 +149,7 @@ type PluginContext = Omit< */ export async function createEnvironmentPluginContainer( environment: Environment, - plugins: EnvironmentPlugin[], + plugins: Plugin[], watcher?: FSWatcher, ): Promise { const { From 778e39d2f2e388e1b9d3cfefd12ee2a2cd7bc309 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Thu, 23 May 2024 17:38:26 +0200 Subject: [PATCH 079/123] fix: backcompat with config.optimizeDeps --- packages/vite/src/node/config.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index a8f57e331e1ecf..7fec1dab805102 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -949,8 +949,10 @@ export async function resolveConfig( // Backward compatibility: merge environments.client.dev.optimizeDeps back into optimizeDeps const resolvedConfigEnvironmentsClient = resolvedEnvironments.client - const patchedOptimizeDeps = - resolvedConfigEnvironmentsClient.dev?.optimizeDeps ?? {} + const patchedOptimizeDeps = mergeConfig( + config.optimizeDeps ?? {}, + resolvedConfigEnvironmentsClient.dev?.optimizeDeps ?? {}, + ) const backwardCompatibleOptimizeDeps = { holdUntilCrawlEnd: true, ...patchedOptimizeDeps, From bcb19591adbfbc78c66190fd2a3a4c105755a21f Mon Sep 17 00:00:00 2001 From: patak-dev Date: Thu, 23 May 2024 17:51:02 +0200 Subject: [PATCH 080/123] feat: createWeakData --- packages/vite/src/node/environment.ts | 14 -------------- packages/vite/src/node/index.ts | 4 ++-- .../src/node/plugins/dynamicImportVars.ts | 5 +++-- packages/vite/src/node/publicUtils.ts | 1 + packages/vite/src/node/utils.ts | 19 +++++++++++++++++++ 5 files changed, 25 insertions(+), 18 deletions(-) diff --git a/packages/vite/src/node/environment.ts b/packages/vite/src/node/environment.ts index 1d748eedea96d1..da8fe81b9c62de 100644 --- a/packages/vite/src/node/environment.ts +++ b/packages/vite/src/node/environment.ts @@ -8,17 +8,3 @@ export type Environment = | BuildEnvironment | ScanEnvironment | FutureCompatEnvironment - -export function cachedByEnvironment( - create: (environment: Environment) => Data, -): (environment: Environment) => Data { - const cache = new WeakMap() - return function (environment: Environment) { - let data = cache.get(environment) - if (!data) { - data = create(environment) - cache.set(environment, data) - } - return data - } -} diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index ce5fec1b02e969..1d88e1cd04da10 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -12,8 +12,7 @@ export { createServer } from './server' export { preview } from './preview' export { build, createBuilder } from './build' -// TODO: Can we remove this? -// export { optimizeDeps } from './optimizer' +export { optimizeDeps } from './optimizer' export { formatPostcssSourceMap, preprocessCSS } from './plugins/css' export { transformWithEsbuild } from './plugins/esbuild' @@ -53,6 +52,7 @@ export type { ResolvedDevEnvironmentOptions, } from './config' export type { Plugin, PluginOption, HookHandler } from './plugin' +export type { Environment } from './environment' export type { FilterPattern } from './utils' export type { CorsOptions, CorsOrigin, CommonServerOptions } from './http' export type { diff --git a/packages/vite/src/node/plugins/dynamicImportVars.ts b/packages/vite/src/node/plugins/dynamicImportVars.ts index 534f52ed690121..04f1ef215754f8 100644 --- a/packages/vite/src/node/plugins/dynamicImportVars.ts +++ b/packages/vite/src/node/plugins/dynamicImportVars.ts @@ -10,6 +10,7 @@ import { CLIENT_ENTRY } from '../constants' import { createIdResolver } from '../idResolver' import { createFilter, + createWeakData, normalizePath, rawRE, requestQueryMaybeEscapedSplitRE, @@ -17,7 +18,7 @@ import { transformStableResult, urlRE, } from '../utils' -import { type Environment, cachedByEnvironment } from '../environment' +import type { Environment } from '../environment' import { toAbsoluteGlob } from './importMetaGlob' import { hasViteIgnoreRE } from './importAnalysis' import { workerOrSharedWorkerRE } from './worker' @@ -174,7 +175,7 @@ export function dynamicImportVarsPlugin(config: ResolvedConfig): Plugin { extensions: [], }) - const getFilter = cachedByEnvironment((environment: Environment) => { + const getFilter = createWeakData((environment: Environment) => { const { include, exclude } = environment.options.build.dynamicImportVarsOptions return createFilter(include, exclude) diff --git a/packages/vite/src/node/publicUtils.ts b/packages/vite/src/node/publicUtils.ts index a9cad7a5106db6..7415f5792e8fc7 100644 --- a/packages/vite/src/node/publicUtils.ts +++ b/packages/vite/src/node/publicUtils.ts @@ -15,6 +15,7 @@ export { mergeConfig, mergeAlias, createFilter, + createWeakData, rollupVersion, } from './utils' export { send } from './server/send' diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index b398ee9d74e969..5bfab27883d621 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -1438,3 +1438,22 @@ export function partialEncodeURIPath(uri: string): string { const postfix = filePath !== uri ? uri.slice(filePath.length) : '' return filePath.replaceAll('%', '%25') + postfix } + +/** + * Creates a function that hides the complexities of a WeakMap with an initial value + * to implement object metadata. Used by plugins to implement cross hooks per + * environment metadata + */ +export function createWeakData( + initial: (key: Key) => Data, +): (key: Key) => Data { + const cache = new WeakMap() + return function (key: Key) { + let data = cache.get(key) + if (!data) { + data = initial(key) + cache.set(key, data) + } + return data + } +} From c7d02ac6511074ad83d8357e64c0c2a4fdd79237 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Thu, 23 May 2024 18:00:07 +0200 Subject: [PATCH 081/123] release: v6.0.0-alpha.16 --- packages/vite/CHANGELOG.md | 17 +++++++++++++++++ packages/vite/package.json | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/vite/CHANGELOG.md b/packages/vite/CHANGELOG.md index daacaacb504539..8f49083f3039e0 100644 --- a/packages/vite/CHANGELOG.md +++ b/packages/vite/CHANGELOG.md @@ -1,3 +1,20 @@ +## 6.0.0-alpha.16 (2024-05-23) + +* feat: createWeakData ([bcb1959](https://github.com/vitejs/vite/commit/bcb1959)) +* feat: improve Plugin option types ([21225c9](https://github.com/vitejs/vite/commit/21225c9)) +* feat: only shared plugins (#17289) ([e36f2f3](https://github.com/vitejs/vite/commit/e36f2f3)), closes [#17289](https://github.com/vitejs/vite/issues/17289) +* feat: provide `environment` in every hook context ([53734a8](https://github.com/vitejs/vite/commit/53734a8)) +* feat(types): expose PluginEnvironment type ([4d03124](https://github.com/vitejs/vite/commit/4d03124)) +* fix: argument rollup types for plugin context instead of wrapping ([89ec69c](https://github.com/vitejs/vite/commit/89ec69c)) +* fix: avoid duplicating values in shared optimizeDeps config (#16737) ([a3ee7f5](https://github.com/vitejs/vite/commit/a3ee7f5)), closes [#16737](https://github.com/vitejs/vite/issues/16737) +* fix: back compat for server.pluginContainer.buildStart ([46f21b8](https://github.com/vitejs/vite/commit/46f21b8)) +* fix: backcompat with config.optimizeDeps ([778e39d](https://github.com/vitejs/vite/commit/778e39d)) +* fix: keep plugin with environmentPlugins hook ([63de43a](https://github.com/vitejs/vite/commit/63de43a)) +* chore: remove extra symbol ([431455f](https://github.com/vitejs/vite/commit/431455f)) +* chore: rename to BaseEnvironment (#16797) ([d86553a](https://github.com/vitejs/vite/commit/d86553a)), closes [#16797](https://github.com/vitejs/vite/issues/16797) + + + ## 6.0.0-alpha.15 (2024-05-20) * fix: only join base url in none ssr env ([7e9dd25](https://github.com/vitejs/vite/commit/7e9dd25)) diff --git a/packages/vite/package.json b/packages/vite/package.json index a486b0d1e32c76..f13736e4b9b855 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -1,6 +1,6 @@ { "name": "vite", - "version": "6.0.0-alpha.15", + "version": "6.0.0-alpha.16", "type": "module", "license": "MIT", "author": "Evan You", From 478a9aa7bd3317a7e00ec45d2dfb0e7b040222f2 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Thu, 23 May 2024 21:59:24 +0200 Subject: [PATCH 082/123] fix: optimizeDeps back compat --- packages/vite/src/node/config.ts | 6 ++---- packages/vite/src/node/server/environment.ts | 5 +++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 7fec1dab805102..a8f57e331e1ecf 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -949,10 +949,8 @@ export async function resolveConfig( // Backward compatibility: merge environments.client.dev.optimizeDeps back into optimizeDeps const resolvedConfigEnvironmentsClient = resolvedEnvironments.client - const patchedOptimizeDeps = mergeConfig( - config.optimizeDeps ?? {}, - resolvedConfigEnvironmentsClient.dev?.optimizeDeps ?? {}, - ) + const patchedOptimizeDeps = + resolvedConfigEnvironmentsClient.dev?.optimizeDeps ?? {} const backwardCompatibleOptimizeDeps = { holdUntilCrawlEnd: true, ...patchedOptimizeDeps, diff --git a/packages/vite/src/node/server/environment.ts b/packages/vite/src/node/server/environment.ts index 742186e9a3ae1b..e853644bd50e0f 100644 --- a/packages/vite/src/node/server/environment.ts +++ b/packages/vite/src/node/server/environment.ts @@ -140,8 +140,9 @@ export class DevEnvironment extends BaseEnvironment { if (setup?.depsOptimizer) { this.depsOptimizer = setup?.depsOptimizer } else if ( - optimizeDeps?.noDiscovery && - optimizeDeps?.include?.length === 0 + optimizeDeps?.disabled === true || + optimizeDeps?.disabled === 'build' || + (optimizeDeps?.noDiscovery && optimizeDeps?.include?.length === 0) ) { this.depsOptimizer = undefined } else { From 006cfb7362c8ce6215e98ec8524686f69b8b6818 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Thu, 23 May 2024 22:22:53 +0200 Subject: [PATCH 083/123] feat: per-environment optimizeDeps entries and force --- packages/vite/src/node/config.ts | 28 ++++++++++--------- packages/vite/src/node/index.ts | 1 - packages/vite/src/node/optimizer/index.ts | 9 ++---- .../vite/src/node/plugins/importAnalysis.ts | 4 +-- packages/vite/src/node/plugins/resolve.ts | 6 ++-- packages/vite/src/node/ssr/index.ts | 4 +-- packages/vite/src/node/utils.ts | 4 +-- 7 files changed, 27 insertions(+), 29 deletions(-) diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index a8f57e331e1ecf..17f7ba11557fe9 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -78,7 +78,7 @@ import type { InternalResolveOptions, ResolveOptions } from './plugins/resolve' import { resolvePlugin, tryNodeResolve } from './plugins/resolve' import type { LogLevel, Logger } from './logger' import { createLogger } from './logger' -import type { DepOptimizationConfig, DepOptimizationOptions } from './optimizer' +import type { DepOptimizationOptions } from './optimizer' import type { JsonOptions } from './plugins/json' import type { EnvironmentPluginContainer } from './server/pluginContainer' import { createEnvironmentPluginContainer } from './server/pluginContainer' @@ -167,7 +167,7 @@ export interface DevEnvironmentOptions { /** * Optimize deps config */ - optimizeDeps?: DepOptimizationConfig + optimizeDeps?: DepOptimizationOptions /** * create the Dev Environment instance @@ -574,7 +574,7 @@ export function resolveDevEnvironmentOptions( preTransformRequests: dev?.preTransformRequests ?? environmentName === 'client', warmup: dev?.warmup ?? [], - optimizeDeps: resolveOptimizeDepsConfig( + optimizeDeps: resolveDepOptimizationOptions( dev?.optimizeDeps, preserverSymlinks, ), @@ -732,11 +732,11 @@ function resolveEnvironmentResolveOptions( return resolvedResolve } -// TODO: Introduce ResolvedDepOptimizationConfig -function resolveOptimizeDepsConfig( - optimizeDeps: DepOptimizationConfig | undefined, +// TODO: Introduce ResolvedDepOptimizationOptions +function resolveDepOptimizationOptions( + optimizeDeps: DepOptimizationOptions | undefined, preserveSymlinks: boolean, -): DepOptimizationConfig { +): DepOptimizationOptions { optimizeDeps ??= {} return { include: optimizeDeps.include ?? [], @@ -750,6 +750,8 @@ function resolveOptimizeDepsConfig( ...optimizeDeps.esbuildOptions, }, disabled: optimizeDeps.disabled, + entries: optimizeDeps.entries, + force: optimizeDeps.force ?? false, } } @@ -949,8 +951,8 @@ export async function resolveConfig( // Backward compatibility: merge environments.client.dev.optimizeDeps back into optimizeDeps const resolvedConfigEnvironmentsClient = resolvedEnvironments.client - const patchedOptimizeDeps = - resolvedConfigEnvironmentsClient.dev?.optimizeDeps ?? {} + const patchedOptimizeDeps = resolvedConfigEnvironmentsClient.dev?.optimizeDeps + const backwardCompatibleOptimizeDeps = { holdUntilCrawlEnd: true, ...patchedOptimizeDeps, @@ -1760,23 +1762,23 @@ async function runConfigEnvironmentHook( } } -export function getDepOptimizationConfig( +export function getDepOptimizationOptions( config: ResolvedConfig, ssr: boolean, -): DepOptimizationConfig { +): DepOptimizationOptions { return ssr ? config.ssr.optimizeDeps : config.optimizeDeps } export function isDepsOptimizerEnabled( config: ResolvedConfig, ssr: boolean, ): boolean { - const optimizeDeps = getDepOptimizationConfig(config, ssr) + const optimizeDeps = getDepOptimizationOptions(config, ssr) return !(optimizeDeps.noDiscovery && !optimizeDeps.include?.length) } function optimizeDepsDisabledBackwardCompatibility( resolved: ResolvedConfig, - optimizeDeps: DepOptimizationConfig, + optimizeDeps: DepOptimizationOptions, optimizeDepsPath: string = '', ) { const optimizeDepsDisabled = optimizeDeps.disabled diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index 1d88e1cd04da10..6477e33799a316 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -86,7 +86,6 @@ export type { export type { DepOptimizationMetadata, DepOptimizationOptions, - DepOptimizationConfig, OptimizedDepInfo, ExportsData, } from './optimizer' diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index a339e88eefa6ac..ac9a34fb2ef56d 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -62,7 +62,7 @@ export interface DepsOptimizer { options: DepOptimizationOptions } -export interface DepOptimizationConfig { +export interface DepOptimizationOptions { /** * Force optimize listed dependencies (must be resolvable import paths, * cannot be globs). @@ -135,16 +135,13 @@ export interface DepOptimizationConfig { * When enabled, it will hold the first optimized deps results until all static * imports are crawled on cold start. This avoids the need for full-page reloads * when new dependencies are discovered and they trigger the generation of new - * common chunks. If all dependencies are found by the scanner plus the explicitely + * common chunks. If all dependencies are found by the scanner plus the explicitly * defined ones in `include`, it is better to disable this option to let the * browser process more requests in parallel. * @default true * @experimental */ holdUntilCrawlEnd?: boolean -} - -export type DepOptimizationOptions = DepOptimizationConfig & { /** * By default, Vite will crawl your `index.html` to detect dependencies that * need to be pre-bundled. If `build.rollupOptions.input` is specified, Vite @@ -164,7 +161,7 @@ export type DepOptimizationOptions = DepOptimizationConfig & { } // TODO: We first need to define if entries and force should be per-environment -// export type ResolvedDepOptimizationConfig = Required +// export type ResolvedDepOptimizationOptions = Required export interface DepOptimizationResult { metadata: DepOptimizationMetadata diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index 3da93418a2e3ed..e897c719573772 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -51,7 +51,7 @@ import { } from '../utils' import { getFsUtils } from '../fsUtils' import { checkPublicFile } from '../publicDir' -import { getDepOptimizationConfig } from '../config' +import { getDepOptimizationOptions } from '../config' import type { ResolvedConfig } from '../config' import type { Plugin } from '../plugin' import type { DevEnvironment } from '../server/environment' @@ -277,7 +277,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { let importerFile = importer - const optimizeDeps = getDepOptimizationConfig(config, ssr) + const optimizeDeps = getDepOptimizationOptions(config, ssr) if (moduleListContains(optimizeDeps?.exclude, url)) { if (depsOptimizer) { await depsOptimizer.scanProcessing diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index dfaa03604ddfe9..1c6edfb7e08a35 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -38,7 +38,7 @@ import { import type { ResolvedEnvironmentOptions } from '../config' import { optimizedDepInfoFromFile, optimizedDepInfoFromId } from '../optimizer' import type { DepsOptimizer } from '../optimizer' -import type { DepOptimizationConfig, SSROptions } from '..' +import type { DepOptimizationOptions, SSROptions } from '..' import type { PackageCache, PackageData } from '../packages' import type { FsUtils } from '../fsUtils' import { commonFsUtils } from '../fsUtils' @@ -772,7 +772,7 @@ export function tryNodeResolve( ssr: boolean = false, externalize?: boolean, allowLinkedExternal: boolean = true, - depsOptimizerOptions?: DepOptimizationConfig, + depsOptimizerOptions?: DepOptimizationOptions, ): PartialResolvedId | undefined { const { root, dedupe, isBuild, preserveSymlinks, packageCache } = options @@ -1248,7 +1248,7 @@ function tryResolveBrowserMapping( options: InternalResolveOptions, isFilePath: boolean, externalize?: boolean, - depsOptimizerOptions?: DepOptimizationConfig, + depsOptimizerOptions?: DepOptimizationOptions, ) { let res: string | undefined const pkg = diff --git a/packages/vite/src/node/ssr/index.ts b/packages/vite/src/node/ssr/index.ts index 4b0626c58d76b9..0f17a347d0e185 100644 --- a/packages/vite/src/node/ssr/index.ts +++ b/packages/vite/src/node/ssr/index.ts @@ -1,8 +1,8 @@ -import type { DepOptimizationConfig } from '../optimizer' +import type { DepOptimizationOptions } from '../optimizer' export type SSRTarget = 'node' | 'webworker' -export type SsrDepOptimizationOptions = DepOptimizationConfig +export type SsrDepOptimizationOptions = DepOptimizationOptions /** * @deprecated use environments.ssr diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index 5bfab27883d621..074303cb9cc35d 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -30,7 +30,7 @@ import { loopbackHosts, wildcardHosts, } from './constants' -import type { DepOptimizationConfig } from './optimizer' +import type { DepOptimizationOptions } from './optimizer' import type { ResolvedConfig } from './config' import type { ResolvedServerUrls, ViteDevServer } from './server' import type { PreviewServer } from './preview' @@ -122,7 +122,7 @@ export function moduleListContains( export function isOptimizable( id: string, - optimizeDeps: DepOptimizationConfig, + optimizeDeps: DepOptimizationOptions, ): boolean { const { extensions } = optimizeDeps return ( From 5f36aa6ebb72a37db6f059d364949fa7cddc6b5a Mon Sep 17 00:00:00 2001 From: patak-dev Date: Fri, 24 May 2024 00:30:45 +0200 Subject: [PATCH 084/123] chore: refactor isDepsOptimizerEnabled --- packages/vite/src/node/config.ts | 14 --------- packages/vite/src/node/optimizer/index.ts | 9 ++++++ .../vite/src/node/plugins/importAnalysis.ts | 29 +++++++++---------- packages/vite/src/node/plugins/index.ts | 11 +++---- packages/vite/src/node/server/environment.ts | 9 ++---- 5 files changed, 32 insertions(+), 40 deletions(-) diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 17f7ba11557fe9..8bd49eef80b130 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -1762,20 +1762,6 @@ async function runConfigEnvironmentHook( } } -export function getDepOptimizationOptions( - config: ResolvedConfig, - ssr: boolean, -): DepOptimizationOptions { - return ssr ? config.ssr.optimizeDeps : config.optimizeDeps -} -export function isDepsOptimizerEnabled( - config: ResolvedConfig, - ssr: boolean, -): boolean { - const optimizeDeps = getDepOptimizationOptions(config, ssr) - return !(optimizeDeps.noDiscovery && !optimizeDeps.include?.length) -} - function optimizeDepsDisabledBackwardCompatibility( resolved: ResolvedConfig, optimizeDeps: DepOptimizationOptions, diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index ac9a34fb2ef56d..9af9ba72445f4b 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -160,6 +160,15 @@ export interface DepOptimizationOptions { force?: boolean } +export function isDepOptimizationEnabled( + optimizeDeps: DepOptimizationOptions, +): boolean { + return ( + !(optimizeDeps.disabled === true || optimizeDeps.disabled === 'dev') && + !(optimizeDeps.noDiscovery && !optimizeDeps.include?.length) + ) +} + // TODO: We first need to define if entries and force should be per-environment // export type ResolvedDepOptimizationOptions = Required diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index e897c719573772..19ee2d352252b7 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -51,7 +51,6 @@ import { } from '../utils' import { getFsUtils } from '../fsUtils' import { checkPublicFile } from '../publicDir' -import { getDepOptimizationOptions } from '../config' import type { ResolvedConfig } from '../config' import type { Plugin } from '../plugin' import type { DevEnvironment } from '../server/environment' @@ -277,20 +276,20 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { let importerFile = importer - const optimizeDeps = getDepOptimizationOptions(config, ssr) - if (moduleListContains(optimizeDeps?.exclude, url)) { - if (depsOptimizer) { - await depsOptimizer.scanProcessing - - // if the dependency encountered in the optimized file was excluded from the optimization - // the dependency needs to be resolved starting from the original source location of the optimized file - // because starting from node_modules/.vite will not find the dependency if it was not hoisted - // (that is, if it is under node_modules directory in the package source of the optimized file) - for (const optimizedModule of depsOptimizer.metadata.depInfoList) { - if (!optimizedModule.src) continue // Ignore chunks - if (optimizedModule.file === importerModule.file) { - importerFile = optimizedModule.src - } + if ( + depsOptimizer && + moduleListContains(depsOptimizer.options.exclude, url) + ) { + await depsOptimizer.scanProcessing + + // if the dependency encountered in the optimized file was excluded from the optimization + // the dependency needs to be resolved starting from the original source location of the optimized file + // because starting from node_modules/.vite will not find the dependency if it was not hoisted + // (that is, if it is under node_modules directory in the package source of the optimized file) + for (const optimizedModule of depsOptimizer.metadata.depInfoList) { + if (!optimizedModule.src) continue // Ignore chunks + if (optimizedModule.file === importerModule.file) { + importerFile = optimizedModule.src } } } diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index 6e325118133164..1bd609d0a75839 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -1,7 +1,7 @@ import aliasPlugin, { type ResolverFunction } from '@rollup/plugin-alias' import type { ObjectHook } from 'rollup' import type { PluginHookUtils, ResolvedConfig } from '../config' -import { isDepsOptimizerEnabled } from '../config' +import { isDepOptimizationEnabled } from '../optimizer' import type { HookHandler, Plugin, PluginWithRequiredHook } from '../plugin' import { watchPackageDataPlugin } from '../packages' import { getFsUtils } from '../fsUtils' @@ -38,13 +38,14 @@ export async function resolvePlugins( ? await (await import('../build')).resolveBuildPlugins(config) : { pre: [], post: [] } const { modulePreload } = config.build - const depsOptimizerEnabled = + const depOptimizationEnabled = !isBuild && - (isDepsOptimizerEnabled(config, false) || - isDepsOptimizerEnabled(config, true)) + Object.values(config.environments).some((environment) => + isDepOptimizationEnabled(environment.dev.optimizeDeps), + ) return [ - depsOptimizerEnabled ? optimizedDepsPlugin(config) : null, + depOptimizationEnabled ? optimizedDepsPlugin(config) : null, isBuild ? metadataPlugin() : null, !isWorker ? watchPackageDataPlugin(config.packageCache) : null, preAliasPlugin(config), diff --git a/packages/vite/src/node/server/environment.ts b/packages/vite/src/node/server/environment.ts index e853644bd50e0f..3aa773e8db47d3 100644 --- a/packages/vite/src/node/server/environment.ts +++ b/packages/vite/src/node/server/environment.ts @@ -12,12 +12,13 @@ import { getDefaultResolvedEnvironmentOptions } from '../config' import { mergeConfig, promiseWithResolvers } from '../utils' import type { FetchModuleOptions } from '../ssr/fetchModule' import { fetchModule } from '../ssr/fetchModule' +import type { DepsOptimizer } from '../optimizer' +import { isDepOptimizationEnabled } from '../optimizer' import { createDepsOptimizer, createExplicitDepsOptimizer, } from '../optimizer/optimizer' import { resolveEnvironmentPlugins } from '../plugin' -import type { DepsOptimizer } from '../optimizer' import { EnvironmentModuleGraph } from './moduleGraph' import type { HMRChannel } from './hmr' import { createNoopHMRChannel, getShortName, updateModules } from './hmr' @@ -139,11 +140,7 @@ export class DevEnvironment extends BaseEnvironment { const { optimizeDeps } = this.options.dev if (setup?.depsOptimizer) { this.depsOptimizer = setup?.depsOptimizer - } else if ( - optimizeDeps?.disabled === true || - optimizeDeps?.disabled === 'build' || - (optimizeDeps?.noDiscovery && optimizeDeps?.include?.length === 0) - ) { + } else if (!isDepOptimizationEnabled(optimizeDeps)) { this.depsOptimizer = undefined } else { // We only support auto-discovery for the client environment, for all other From 2b69389ba4f24064513331cdeca9b3d7af9d36fc Mon Sep 17 00:00:00 2001 From: patak <583075+patak-dev@users.noreply.github.com> Date: Fri, 24 May 2024 00:43:55 +0200 Subject: [PATCH 085/123] feat: reporter as shared plugin using createWeakData (#17293) --- packages/vite/src/node/plugins/reporter.ts | 109 +++++++++++++-------- 1 file changed, 68 insertions(+), 41 deletions(-) diff --git a/packages/vite/src/node/plugins/reporter.ts b/packages/vite/src/node/plugins/reporter.ts index 285d9baa7daba2..ab3f6f26d90dc4 100644 --- a/packages/vite/src/node/plugins/reporter.ts +++ b/packages/vite/src/node/plugins/reporter.ts @@ -2,9 +2,15 @@ import path from 'node:path' import { gzip } from 'node:zlib' import { promisify } from 'node:util' import colors from 'picocolors' -import type { Plugin } from 'rollup' +import type { Plugin } from '../plugin' import type { ResolvedConfig } from '../config' -import { isDefined, isInNodeModules, normalizePath } from '../utils' +import type { Environment } from '../environment' +import { + createWeakData, + isDefined, + isInNodeModules, + normalizePath, +} from '../utils' import { LogLevels } from '../logger' import { withTrailingSlash } from '../../shared/utils' @@ -25,7 +31,6 @@ const COMPRESSIBLE_ASSETS_RE = /\.(?:html|json|svg|txt|xml|xhtml)$/ export function buildReporterPlugin(config: ResolvedConfig): Plugin { const compress = promisify(gzip) - const chunkLimit = config.build.chunkSizeWarningLimit const numberFormatter = new Intl.NumberFormat('en', { maximumFractionDigits: 2, @@ -37,66 +42,78 @@ export function buildReporterPlugin(config: ResolvedConfig): Plugin { const tty = process.stdout.isTTY && !process.env.CI const shouldLogInfo = LogLevels[config.logLevel || 'info'] >= LogLevels.info - let hasTransformed = false - let hasRenderedChunk = false - let hasCompressChunk = false - let transformedCount = 0 - let chunkCount = 0 - let compressedCount = 0 + const getData = createWeakData((environment: Environment) => { + const data = { + hasTransformed: false, + hasRenderedChunk: false, + hasCompressChunk: false, + transformedCount: 0, + chunkCount: 0, + compressedCount: 0, + logTransform: throttle((id: string) => { + writeLine( + `transforming (${data.transformedCount}) ${colors.dim( + path.relative(config.root, id), + )}`, + ) + }), + } + return data + }) async function getCompressedSize( + environment: Environment, code: string | Uint8Array, ): Promise { - if (config.build.ssr || !config.build.reportCompressedSize) { + if ( + environment.options.build.ssr || + !environment.options.build.reportCompressedSize + ) { return null } - if (shouldLogInfo && !hasCompressChunk) { + const data = getData(environment) + if (shouldLogInfo && !data.hasCompressChunk) { if (!tty) { config.logger.info('computing gzip size...') } else { writeLine('computing gzip size (0)...') } - hasCompressChunk = true + data.hasCompressChunk = true } const compressed = await compress( typeof code === 'string' ? code : Buffer.from(code), ) - compressedCount++ + data.compressedCount++ if (shouldLogInfo && tty) { - writeLine(`computing gzip size (${compressedCount})...`) + writeLine(`computing gzip size (${data.compressedCount})...`) } return compressed.length } - const logTransform = throttle((id: string) => { - writeLine( - `transforming (${transformedCount}) ${colors.dim( - path.relative(config.root, id), - )}`, - ) - }) - return { name: 'vite:reporter', + sharedDuringBuild: true, transform(_, id) { - transformedCount++ + const data = getData(this.environment!) + + data.transformedCount++ if (shouldLogInfo) { if (!tty) { - if (!hasTransformed) { + if (!data.hasTransformed) { config.logger.info(`transforming...`) } } else { if (id.includes(`?`)) return - logTransform(id) + data.logTransform(id) } - hasTransformed = true + data.hasTransformed = true } return null }, buildStart() { - transformedCount = 0 + getData(this.environment!).transformedCount = 0 }, buildEnd() { @@ -105,14 +122,15 @@ export function buildReporterPlugin(config: ResolvedConfig): Plugin { clearLine() } config.logger.info( - `${colors.green(`✓`)} ${transformedCount} modules transformed.`, + `${colors.green(`✓`)} ${getData(this.environment!).transformedCount} modules transformed.`, ) } }, renderStart() { - chunkCount = 0 - compressedCount = 0 + const data = getData(this.environment!) + data.chunkCount = 0 + data.compressedCount = 0 }, renderChunk(code, chunk, options) { @@ -145,17 +163,17 @@ export function buildReporterPlugin(config: ResolvedConfig): Plugin { } } } - - chunkCount++ + const data = getData(this.environment!) + data.chunkCount++ if (shouldLogInfo) { if (!tty) { - if (!hasRenderedChunk) { + if (!data.hasRenderedChunk) { config.logger.info('rendering chunks...') } } else { - writeLine(`rendering chunks (${chunkCount})...`) + writeLine(`rendering chunks (${data.chunkCount})...`) } - hasRenderedChunk = true + data.hasRenderedChunk = true } return null }, @@ -165,6 +183,9 @@ export function buildReporterPlugin(config: ResolvedConfig): Plugin { }, async writeBundle({ dir: outDir }, output) { + const environment = this.environment! + const chunkLimit = environment.options.build.chunkSizeWarningLimit + let hasLargeChunks = false if (shouldLogInfo) { @@ -177,7 +198,10 @@ export function buildReporterPlugin(config: ResolvedConfig): Plugin { name: chunk.fileName, group: 'JS', size: chunk.code.length, - compressedSize: await getCompressedSize(chunk.code), + compressedSize: await getCompressedSize( + environment, + chunk.code, + ), mapSize: chunk.map ? chunk.map.toString().length : null, } } else { @@ -191,7 +215,7 @@ export function buildReporterPlugin(config: ResolvedConfig): Plugin { size: chunk.source.length, mapSize: null, // Rollup doesn't support CSS maps? compressedSize: isCompressible - ? await getCompressedSize(chunk.source) + ? await getCompressedSize(environment, chunk.source) : null, } } @@ -226,10 +250,13 @@ export function buildReporterPlugin(config: ResolvedConfig): Plugin { const relativeOutDir = normalizePath( path.relative( config.root, - path.resolve(config.root, outDir ?? config.build.outDir), + path.resolve( + config.root, + outDir ?? environment.options.build.outDir, + ), ), ) - const assetsDir = path.join(config.build.assetsDir, '/') + const assetsDir = path.join(environment.options.build.assetsDir, '/') for (const group of groups) { const filtered = entries.filter((e) => e.group === group.name) @@ -276,9 +303,9 @@ export function buildReporterPlugin(config: ResolvedConfig): Plugin { if ( hasLargeChunks && - config.build.minify && + environment.options.build.minify && !config.build.lib && - !config.build.ssr + !environment.options.build.ssr ) { config.logger.warn( colors.yellow( From e33995902885e6bad9e89aa968dd8cf98726f940 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Fri, 24 May 2024 02:53:12 +0200 Subject: [PATCH 086/123] fix(types): avoid referencing `WeakKey` type --- packages/vite/src/node/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index 074303cb9cc35d..d90c0c778c0c73 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -1444,7 +1444,7 @@ export function partialEncodeURIPath(uri: string): string { * to implement object metadata. Used by plugins to implement cross hooks per * environment metadata */ -export function createWeakData( +export function createWeakData( initial: (key: Key) => Data, ): (key: Key) => Data { const cache = new WeakMap() From 11bddb0503fd5522bce8c8cd8396ec4eb6825c4d Mon Sep 17 00:00:00 2001 From: patak <583075+patak-dev@users.noreply.github.com> Date: Fri, 24 May 2024 02:55:31 +0200 Subject: [PATCH 087/123] fix: handleHotUpdate compat (#17295) --- packages/vite/src/node/server/hmr.ts | 206 ++++++++++++++++++--------- 1 file changed, 137 insertions(+), 69 deletions(-) diff --git a/packages/vite/src/node/server/hmr.ts b/packages/vite/src/node/server/hmr.ts index 247be05fc0a7d9..7f8f8e32206dc8 100644 --- a/packages/vite/src/node/server/hmr.ts +++ b/packages/vite/src/node/server/hmr.ts @@ -223,78 +223,146 @@ export async function handleHMRUpdate( return } - // 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. - - 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) - } + const timestamp = Date.now() + const contextMeta = { + type, + file, + timestamp, + read: () => readModifiedFile(file), + server, + } + const hotMap = new Map< + Environment, + { context: HotUpdateContext; error?: Error } + >() + + for (const environment of Object.values(server.environments)) { + const mods = new Set(environment.moduleGraph.getModulesByFile(file)) + if (type === 'create') { + for (const mod of environment.moduleGraph._hasResolveFailedErrorModules) { + mods.add(mod) } + } + const context = { + ...contextMeta, + modules: [...mods], + // later on hotUpdate will be called for each runtime with a new hotContext + environment, + } + hotMap.set(environment, { context }) + } - // 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, - } + const mixedMods = new Set(server.moduleGraph.getModulesByFile(file)) - let hmrContext + const mixedHmrContext: HmrContext = { + ...contextMeta, + modules: [...mixedMods], + } + const clientHotContext = hotMap.get(server.environments.client)!.context + const ssrHotContext = hotMap.get(server.environments.ssr)?.context + try { + for (const plugin of getSortedHotUpdatePlugins( + server.environments.client, + )) { + if (plugin.hotUpdate) { + const filteredModules = await getHookHandler(plugin.hotUpdate)( + clientHotContext, + ) + if (filteredModules) { + clientHotContext.modules = filteredModules + // Invalidate the hmrContext to force compat modules to be updated + mixedHmrContext.modules = mixedHmrContext.modules.filter( + (mixedMod) => + filteredModules.find((mod) => mixedMod.id === mod.id) || + ssrHotContext?.modules.find( + (ssrMod) => ssrMod.id === mixedMod.id, + ), + ) + mixedHmrContext.modules.push( + ...filteredModules + .filter( + (mod) => + !mixedHmrContext.modules.find( + (mixedMod) => mixedMod.id === mod.id, + ), + ) + .map((mod) => + server.moduleGraph.getBackwardCompatibleModuleNode(mod), + ), + ) + } + } else if (type === 'update') { + // later on, we'll need: if (runtime === 'client') + // Backward compatibility with mixed client and ssr moduleGraph + const filteredModules = await getHookHandler(plugin.handleHotUpdate!)( + mixedHmrContext, + ) + if (filteredModules) { + mixedHmrContext.modules = filteredModules + clientHotContext.modules = clientHotContext.modules.filter((mod) => + filteredModules.find((mixedMod) => mod.id === mixedMod.id), + ) + clientHotContext.modules.push( + ...(filteredModules + .filter( + (mixedMod) => + !clientHotContext.modules.find( + (mod) => mod.id === mixedMod.id, + ), + ) + .map((mixedMod) => mixedMod._clientModule) + .filter(Boolean) as EnvironmentModuleNode[]), + ) + if (ssrHotContext) { + ssrHotContext.modules = ssrHotContext.modules.filter((mod) => + filteredModules.find((mixedMod) => mod.id === mixedMod.id), + ) + ssrHotContext.modules.push( + ...(filteredModules + .filter( + (mixedMod) => + !ssrHotContext.modules.find( + (mod) => mod.id === mixedMod.id, + ), + ) + .map((mixedMod) => mixedMod._ssrModule) + .filter(Boolean) as EnvironmentModuleNode[]), + ) + } + } + } + } + } catch (error) { + hotMap.get(server.environments.client)!.error = error + } + + for (const environment of Object.values(server.environments)) { + if (environment.name === 'client') continue + const hot = hotMap.get(environment)! + try { for (const plugin of getSortedHotUpdatePlugins(environment)) { if (plugin.hotUpdate) { const filteredModules = await getHookHandler(plugin.hotUpdate)( - hotContext, + hot.context, ) 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[] + hot.context.modules = filteredModules } } } + } catch (error) { + hot.error = error + } + } - if (!hotContext.modules.length) { + async function hmr(environment: DevEnvironment) { + try { + const { context, error } = hotMap.get(environment)! + if (error) { + throw error + } + if (!context.modules.length) { // html file cannot be hot updated if (file.endsWith('.html')) { environment.logger.info( @@ -304,22 +372,22 @@ export async function handleHMRUpdate( timestamp: true, }, ) - environments.forEach(({ hot }) => - hot.send({ - type: 'full-reload', - path: config.server.middlewareMode - ? '*' - : '/' + normalizePath(path.relative(config.root, file)), - }), - ) + environment.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)}`) + debugHmr?.( + `(${environment.name}) [no modules matched] ${colors.dim(shortFile)}`, + ) } return } - updateModules(environment, shortFile, hotContext.modules, timestamp) + updateModules(environment, shortFile, context.modules, timestamp) } catch (err) { environment.hot.send({ type: 'error', From 5c655e65cf070eee7517ef9a0df0ac2ff86bb8a5 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Fri, 24 May 2024 03:00:14 +0200 Subject: [PATCH 088/123] fix: DepOptimizationConfig --- packages/vite/src/node/config.ts | 20 ++++++++++---------- packages/vite/src/node/index.ts | 4 ++-- packages/vite/src/node/optimizer/index.ts | 8 ++++---- packages/vite/src/node/plugins/preAlias.ts | 4 ++-- packages/vite/src/node/plugins/resolve.ts | 6 +++--- packages/vite/src/node/ssr/index.ts | 8 ++++---- packages/vite/src/node/utils.ts | 4 ++-- 7 files changed, 27 insertions(+), 27 deletions(-) diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 8bd49eef80b130..60f010ba17a2cb 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -78,7 +78,7 @@ import type { InternalResolveOptions, ResolveOptions } from './plugins/resolve' import { resolvePlugin, tryNodeResolve } from './plugins/resolve' import type { LogLevel, Logger } from './logger' import { createLogger } from './logger' -import type { DepOptimizationOptions } from './optimizer' +import type { DepOptimizationConfig } from './optimizer' import type { JsonOptions } from './plugins/json' import type { EnvironmentPluginContainer } from './server/pluginContainer' import { createEnvironmentPluginContainer } from './server/pluginContainer' @@ -167,7 +167,7 @@ export interface DevEnvironmentOptions { /** * Optimize deps config */ - optimizeDeps?: DepOptimizationOptions + optimizeDeps?: DepOptimizationConfig /** * create the Dev Environment instance @@ -414,7 +414,7 @@ export interface UserConfig extends DefaultEnvironmentOptions { /** * Dep optimization options */ - optimizeDeps?: DepOptimizationOptions + optimizeDeps?: DepOptimizationConfig /** * SSR specific options * We could make SSROptions be a EnvironmentOptions if we can abstract @@ -548,7 +548,7 @@ export type ResolvedConfig = Readonly< assetsInclude: (file: string) => boolean logger: Logger createResolver: (options?: Partial) => ResolveFn - optimizeDeps: DepOptimizationOptions + optimizeDeps: DepOptimizationConfig /** @internal */ packageCache: PackageCache worker: ResolvedWorkerOptions @@ -574,7 +574,7 @@ export function resolveDevEnvironmentOptions( preTransformRequests: dev?.preTransformRequests ?? environmentName === 'client', warmup: dev?.warmup ?? [], - optimizeDeps: resolveDepOptimizationOptions( + optimizeDeps: resolveDepOptimizationConfig( dev?.optimizeDeps, preserverSymlinks, ), @@ -732,11 +732,11 @@ function resolveEnvironmentResolveOptions( return resolvedResolve } -// TODO: Introduce ResolvedDepOptimizationOptions -function resolveDepOptimizationOptions( - optimizeDeps: DepOptimizationOptions | undefined, +// TODO: Introduce ResolvedDepOptimizationConfig +function resolveDepOptimizationConfig( + optimizeDeps: DepOptimizationConfig | undefined, preserveSymlinks: boolean, -): DepOptimizationOptions { +): DepOptimizationConfig { optimizeDeps ??= {} return { include: optimizeDeps.include ?? [], @@ -1764,7 +1764,7 @@ async function runConfigEnvironmentHook( function optimizeDepsDisabledBackwardCompatibility( resolved: ResolvedConfig, - optimizeDeps: DepOptimizationOptions, + optimizeDeps: DepOptimizationConfig, optimizeDepsPath: string = '', ) { const optimizeDepsDisabled = optimizeDeps.disabled diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index 6477e33799a316..4ff8e73a8f169b 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -85,13 +85,13 @@ export type { } from './preview' export type { DepOptimizationMetadata, - DepOptimizationOptions, + DepOptimizationConfig, OptimizedDepInfo, ExportsData, } from './optimizer' export type { ResolvedSSROptions, - SsrDepOptimizationOptions, + SsrDepOptimizationConfig, SSROptions, SSRTarget, } from './ssr' diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 9af9ba72445f4b..7a593dbe23adc0 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -59,10 +59,10 @@ export interface DepsOptimizer { close: () => Promise - options: DepOptimizationOptions + options: DepOptimizationConfig } -export interface DepOptimizationOptions { +export interface DepOptimizationConfig { /** * Force optimize listed dependencies (must be resolvable import paths, * cannot be globs). @@ -161,7 +161,7 @@ export interface DepOptimizationOptions { } export function isDepOptimizationEnabled( - optimizeDeps: DepOptimizationOptions, + optimizeDeps: DepOptimizationConfig, ): boolean { return ( !(optimizeDeps.disabled === true || optimizeDeps.disabled === 'dev') && @@ -170,7 +170,7 @@ export function isDepOptimizationEnabled( } // TODO: We first need to define if entries and force should be per-environment -// export type ResolvedDepOptimizationOptions = Required +// export type ResolvedDepOptimizationConfig = Required export interface DepOptimizationResult { metadata: DepOptimizationMetadata diff --git a/packages/vite/src/node/plugins/preAlias.ts b/packages/vite/src/node/plugins/preAlias.ts index 1e9edf0991915d..5bd845f2fa2824 100644 --- a/packages/vite/src/node/plugins/preAlias.ts +++ b/packages/vite/src/node/plugins/preAlias.ts @@ -2,7 +2,7 @@ import path from 'node:path' import type { Alias, AliasOptions, - DepOptimizationOptions, + DepOptimizationConfig, ResolvedConfig, } from '..' import type { Plugin } from '../plugin' @@ -95,7 +95,7 @@ export function preAliasPlugin(config: ResolvedConfig): Plugin { // TODO: environment? function optimizeAliasReplacementForSSR( id: string, - optimizeDeps: DepOptimizationOptions, + optimizeDeps: DepOptimizationConfig, ) { if (optimizeDeps.include?.includes(id)) { return true diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index 1c6edfb7e08a35..dfaa03604ddfe9 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -38,7 +38,7 @@ import { import type { ResolvedEnvironmentOptions } from '../config' import { optimizedDepInfoFromFile, optimizedDepInfoFromId } from '../optimizer' import type { DepsOptimizer } from '../optimizer' -import type { DepOptimizationOptions, SSROptions } from '..' +import type { DepOptimizationConfig, SSROptions } from '..' import type { PackageCache, PackageData } from '../packages' import type { FsUtils } from '../fsUtils' import { commonFsUtils } from '../fsUtils' @@ -772,7 +772,7 @@ export function tryNodeResolve( ssr: boolean = false, externalize?: boolean, allowLinkedExternal: boolean = true, - depsOptimizerOptions?: DepOptimizationOptions, + depsOptimizerOptions?: DepOptimizationConfig, ): PartialResolvedId | undefined { const { root, dedupe, isBuild, preserveSymlinks, packageCache } = options @@ -1248,7 +1248,7 @@ function tryResolveBrowserMapping( options: InternalResolveOptions, isFilePath: boolean, externalize?: boolean, - depsOptimizerOptions?: DepOptimizationOptions, + depsOptimizerOptions?: DepOptimizationConfig, ) { let res: string | undefined const pkg = diff --git a/packages/vite/src/node/ssr/index.ts b/packages/vite/src/node/ssr/index.ts index 0f17a347d0e185..aa65807eb13ed3 100644 --- a/packages/vite/src/node/ssr/index.ts +++ b/packages/vite/src/node/ssr/index.ts @@ -1,8 +1,8 @@ -import type { DepOptimizationOptions } from '../optimizer' +import type { DepOptimizationConfig } from '../optimizer' export type SSRTarget = 'node' | 'webworker' -export type SsrDepOptimizationOptions = DepOptimizationOptions +export type SsrDepOptimizationConfig = DepOptimizationConfig /** * @deprecated use environments.ssr @@ -41,7 +41,7 @@ export interface SSROptions { * @experimental * @deprecated */ - optimizeDeps?: SsrDepOptimizationOptions + optimizeDeps?: SsrDepOptimizationConfig /** * @deprecated @@ -69,7 +69,7 @@ export interface SSROptions { export interface ResolvedSSROptions extends SSROptions { target: SSRTarget - optimizeDeps: SsrDepOptimizationOptions + optimizeDeps: SsrDepOptimizationConfig } export function resolveSSROptions( diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index d90c0c778c0c73..b420060c23110f 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -30,7 +30,7 @@ import { loopbackHosts, wildcardHosts, } from './constants' -import type { DepOptimizationOptions } from './optimizer' +import type { DepOptimizationConfig } from './optimizer' import type { ResolvedConfig } from './config' import type { ResolvedServerUrls, ViteDevServer } from './server' import type { PreviewServer } from './preview' @@ -122,7 +122,7 @@ export function moduleListContains( export function isOptimizable( id: string, - optimizeDeps: DepOptimizationOptions, + optimizeDeps: DepOptimizationConfig, ): boolean { const { extensions } = optimizeDeps return ( From 624e751513c70ce788b9f79c469fe9faa95b899c Mon Sep 17 00:00:00 2001 From: patak-dev Date: Fri, 24 May 2024 09:56:10 +0200 Subject: [PATCH 089/123] fix: dep optimization options --- packages/vite/src/node/config.ts | 20 ++++++++++---------- packages/vite/src/node/index.ts | 1 + packages/vite/src/node/optimizer/index.ts | 10 +++++----- packages/vite/src/node/plugins/preAlias.ts | 4 ++-- packages/vite/src/node/plugins/resolve.ts | 6 +++--- packages/vite/src/node/utils.ts | 4 ++-- 6 files changed, 23 insertions(+), 22 deletions(-) diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 60f010ba17a2cb..8bd49eef80b130 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -78,7 +78,7 @@ import type { InternalResolveOptions, ResolveOptions } from './plugins/resolve' import { resolvePlugin, tryNodeResolve } from './plugins/resolve' import type { LogLevel, Logger } from './logger' import { createLogger } from './logger' -import type { DepOptimizationConfig } from './optimizer' +import type { DepOptimizationOptions } from './optimizer' import type { JsonOptions } from './plugins/json' import type { EnvironmentPluginContainer } from './server/pluginContainer' import { createEnvironmentPluginContainer } from './server/pluginContainer' @@ -167,7 +167,7 @@ export interface DevEnvironmentOptions { /** * Optimize deps config */ - optimizeDeps?: DepOptimizationConfig + optimizeDeps?: DepOptimizationOptions /** * create the Dev Environment instance @@ -414,7 +414,7 @@ export interface UserConfig extends DefaultEnvironmentOptions { /** * Dep optimization options */ - optimizeDeps?: DepOptimizationConfig + optimizeDeps?: DepOptimizationOptions /** * SSR specific options * We could make SSROptions be a EnvironmentOptions if we can abstract @@ -548,7 +548,7 @@ export type ResolvedConfig = Readonly< assetsInclude: (file: string) => boolean logger: Logger createResolver: (options?: Partial) => ResolveFn - optimizeDeps: DepOptimizationConfig + optimizeDeps: DepOptimizationOptions /** @internal */ packageCache: PackageCache worker: ResolvedWorkerOptions @@ -574,7 +574,7 @@ export function resolveDevEnvironmentOptions( preTransformRequests: dev?.preTransformRequests ?? environmentName === 'client', warmup: dev?.warmup ?? [], - optimizeDeps: resolveDepOptimizationConfig( + optimizeDeps: resolveDepOptimizationOptions( dev?.optimizeDeps, preserverSymlinks, ), @@ -732,11 +732,11 @@ function resolveEnvironmentResolveOptions( return resolvedResolve } -// TODO: Introduce ResolvedDepOptimizationConfig -function resolveDepOptimizationConfig( - optimizeDeps: DepOptimizationConfig | undefined, +// TODO: Introduce ResolvedDepOptimizationOptions +function resolveDepOptimizationOptions( + optimizeDeps: DepOptimizationOptions | undefined, preserveSymlinks: boolean, -): DepOptimizationConfig { +): DepOptimizationOptions { optimizeDeps ??= {} return { include: optimizeDeps.include ?? [], @@ -1764,7 +1764,7 @@ async function runConfigEnvironmentHook( function optimizeDepsDisabledBackwardCompatibility( resolved: ResolvedConfig, - optimizeDeps: DepOptimizationConfig, + optimizeDeps: DepOptimizationOptions, optimizeDepsPath: string = '', ) { const optimizeDepsDisabled = optimizeDeps.disabled diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index 4ff8e73a8f169b..ab9c606d8ae973 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -85,6 +85,7 @@ export type { } from './preview' export type { DepOptimizationMetadata, + DepOptimizationOptions, DepOptimizationConfig, OptimizedDepInfo, ExportsData, diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 7a593dbe23adc0..9a17f82971abd3 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -59,7 +59,7 @@ export interface DepsOptimizer { close: () => Promise - options: DepOptimizationConfig + options: DepOptimizationOptions } export interface DepOptimizationConfig { @@ -142,6 +142,9 @@ export interface DepOptimizationConfig { * @experimental */ holdUntilCrawlEnd?: boolean +} + +export type DepOptimizationOptions = DepOptimizationConfig & { /** * By default, Vite will crawl your `index.html` to detect dependencies that * need to be pre-bundled. If `build.rollupOptions.input` is specified, Vite @@ -161,7 +164,7 @@ export interface DepOptimizationConfig { } export function isDepOptimizationEnabled( - optimizeDeps: DepOptimizationConfig, + optimizeDeps: DepOptimizationOptions, ): boolean { return ( !(optimizeDeps.disabled === true || optimizeDeps.disabled === 'dev') && @@ -169,9 +172,6 @@ export function isDepOptimizationEnabled( ) } -// TODO: We first need to define if entries and force should be per-environment -// export type ResolvedDepOptimizationConfig = Required - export interface DepOptimizationResult { metadata: DepOptimizationMetadata /** diff --git a/packages/vite/src/node/plugins/preAlias.ts b/packages/vite/src/node/plugins/preAlias.ts index 5bd845f2fa2824..1e9edf0991915d 100644 --- a/packages/vite/src/node/plugins/preAlias.ts +++ b/packages/vite/src/node/plugins/preAlias.ts @@ -2,7 +2,7 @@ import path from 'node:path' import type { Alias, AliasOptions, - DepOptimizationConfig, + DepOptimizationOptions, ResolvedConfig, } from '..' import type { Plugin } from '../plugin' @@ -95,7 +95,7 @@ export function preAliasPlugin(config: ResolvedConfig): Plugin { // TODO: environment? function optimizeAliasReplacementForSSR( id: string, - optimizeDeps: DepOptimizationConfig, + optimizeDeps: DepOptimizationOptions, ) { if (optimizeDeps.include?.includes(id)) { return true diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index dfaa03604ddfe9..1c6edfb7e08a35 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -38,7 +38,7 @@ import { import type { ResolvedEnvironmentOptions } from '../config' import { optimizedDepInfoFromFile, optimizedDepInfoFromId } from '../optimizer' import type { DepsOptimizer } from '../optimizer' -import type { DepOptimizationConfig, SSROptions } from '..' +import type { DepOptimizationOptions, SSROptions } from '..' import type { PackageCache, PackageData } from '../packages' import type { FsUtils } from '../fsUtils' import { commonFsUtils } from '../fsUtils' @@ -772,7 +772,7 @@ export function tryNodeResolve( ssr: boolean = false, externalize?: boolean, allowLinkedExternal: boolean = true, - depsOptimizerOptions?: DepOptimizationConfig, + depsOptimizerOptions?: DepOptimizationOptions, ): PartialResolvedId | undefined { const { root, dedupe, isBuild, preserveSymlinks, packageCache } = options @@ -1248,7 +1248,7 @@ function tryResolveBrowserMapping( options: InternalResolveOptions, isFilePath: boolean, externalize?: boolean, - depsOptimizerOptions?: DepOptimizationConfig, + depsOptimizerOptions?: DepOptimizationOptions, ) { let res: string | undefined const pkg = diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index b420060c23110f..d90c0c778c0c73 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -30,7 +30,7 @@ import { loopbackHosts, wildcardHosts, } from './constants' -import type { DepOptimizationConfig } from './optimizer' +import type { DepOptimizationOptions } from './optimizer' import type { ResolvedConfig } from './config' import type { ResolvedServerUrls, ViteDevServer } from './server' import type { PreviewServer } from './preview' @@ -122,7 +122,7 @@ export function moduleListContains( export function isOptimizable( id: string, - optimizeDeps: DepOptimizationConfig, + optimizeDeps: DepOptimizationOptions, ): boolean { const { extensions } = optimizeDeps return ( From 87bbb040cb5efc848234d9fb79634e15eae15e4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arnaud=20Barr=C3=A9?= Date: Fri, 24 May 2024 10:18:43 +0200 Subject: [PATCH 090/123] refactor!: rename to HMRPayload to HotPayload and remove HMRBroadcaster (#16875) --- docs/guide/api-vite-environment.md | 4 +- packages/vite/src/client/client.ts | 4 +- packages/vite/src/client/overlay.ts | 2 +- packages/vite/src/module-runner/hmrHandler.ts | 10 +- packages/vite/src/module-runner/types.ts | 4 +- packages/vite/src/node/index.ts | 11 +-- packages/vite/src/node/plugins/esbuild.ts | 4 +- .../server/__tests__/pluginContainer.spec.ts | 2 +- packages/vite/src/node/server/environment.ts | 29 +++--- .../server/environments/nodeEnvironment.ts | 2 +- packages/vite/src/node/server/hmr.ts | 91 +++---------------- packages/vite/src/node/server/index.ts | 34 ++----- .../vite/src/node/server/middlewares/error.ts | 2 +- packages/vite/src/node/server/ws.ts | 23 ++--- .../src/node/ssr/runtime/__tests__/utils.ts | 2 +- .../node/ssr/runtime/serverHmrConnector.ts | 26 +++--- .../node/ssr/runtime/serverModuleRunner.ts | 4 +- packages/vite/src/shared/hmr.ts | 2 +- packages/vite/types/customEvent.d.ts | 2 +- .../{hmrPayload.d.ts => hotPayload.d.ts} | 4 +- playground/hmr-ssr/vite.config.ts | 17 ++-- playground/hmr/vite.config.ts | 17 ++-- playground/html/__tests__/html.spec.ts | 2 +- 23 files changed, 104 insertions(+), 194 deletions(-) rename packages/vite/types/{hmrPayload.d.ts => hotPayload.d.ts} (91%) diff --git a/docs/guide/api-vite-environment.md b/docs/guide/api-vite-environment.md index dcc9bf70b185f2..8c550587a02b34 100644 --- a/docs/guide/api-vite-environment.md +++ b/docs/guide/api-vite-environment.md @@ -48,7 +48,7 @@ class DevEnvironment { * Communication channel to send and receive messages from the * associated module runner in the target runtime. */ - hot: HMRChannel | null + hot: HotChannel | null /** * Graph of module nodes, with the imported relationship between * processed modules and the cached result of the processed code. @@ -816,7 +816,7 @@ export interface ModuleRunnerHMRConnection { * Configure how HMR is handled when this connection triggers an update. * This method expects that the connection will start listening for HMR updates and call this callback when it's received. */ - onUpdate(callback: (payload: HMRPayload) => void): void + onUpdate(callback: (payload: HotPayload) => void): void } ``` diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index 9253802db6decd..88e2ad8e39875c 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -1,4 +1,4 @@ -import type { ErrorPayload, HMRPayload } from 'types/hmrPayload' +import type { ErrorPayload, HotPayload } from 'types/hotPayload' import type { ViteHotContext } from 'types/hot' import type { InferCustomEventPayload } from 'types/customEvent' import { HMRClient, HMRContext } from '../shared/hmr' @@ -172,7 +172,7 @@ const hmrClient = new HMRClient( }, ) -async function handleMessage(payload: HMRPayload) { +async function handleMessage(payload: HotPayload) { switch (payload.type) { case 'connected': console.debug(`[vite] connected.`) diff --git a/packages/vite/src/client/overlay.ts b/packages/vite/src/client/overlay.ts index 63f570be488efb..971577236dc3f0 100644 --- a/packages/vite/src/client/overlay.ts +++ b/packages/vite/src/client/overlay.ts @@ -1,4 +1,4 @@ -import type { ErrorPayload } from 'types/hmrPayload' +import type { ErrorPayload } from 'types/hotPayload' // injected by the hmr plugin when served declare const __BASE__: string diff --git a/packages/vite/src/module-runner/hmrHandler.ts b/packages/vite/src/module-runner/hmrHandler.ts index d46b8b9c5581d2..a0965f854354c6 100644 --- a/packages/vite/src/module-runner/hmrHandler.ts +++ b/packages/vite/src/module-runner/hmrHandler.ts @@ -1,18 +1,18 @@ -import type { HMRPayload } from 'types/hmrPayload' +import type { HotPayload } from 'types/hotPayload' 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( runner: ModuleRunner, -): (payload: HMRPayload) => Promise { +): (payload: HotPayload) => Promise { const queue = new Queue() - return (payload) => queue.enqueue(() => handleHMRPayload(runner, payload)) + return (payload) => queue.enqueue(() => handleHotPayload(runner, payload)) } -export async function handleHMRPayload( +export async function handleHotPayload( runner: ModuleRunner, - payload: HMRPayload, + payload: HotPayload, ): Promise { const hmrClient = runner.hmrClient if (!hmrClient || runner.isDestroyed()) return diff --git a/packages/vite/src/module-runner/types.ts b/packages/vite/src/module-runner/types.ts index 165a593fa31654..59c45a57afba3f 100644 --- a/packages/vite/src/module-runner/types.ts +++ b/packages/vite/src/module-runner/types.ts @@ -1,5 +1,5 @@ import type { ViteHotContext } from 'types/hot' -import type { HMRPayload } from 'types/hmrPayload' +import type { HotPayload } from 'types/hotPayload' import type { HMRConnection, HMRLogger } from '../shared/hmr' import type { DefineImportMetadata, @@ -24,7 +24,7 @@ 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. */ - onUpdate(callback: (payload: HMRPayload) => void): void + onUpdate(callback: (payload: HotPayload) => void): void } export interface ModuleRunnerImportMeta extends ImportMeta { diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index ab9c606d8ae973..8f6f55ad26eb0f 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -145,10 +145,9 @@ export type { export type { HmrOptions, HmrContext, HotUpdateContext } from './server/hmr' export type { - HMRBroadcaster, - HMRChannel, - ServerHMRChannel, - HMRBroadcasterClient, + HotChannel, + ServerHotChannel, + HotChannelClient, } from './server/hmr' export type { FetchFunction, FetchResult } from 'vite/module-runner' @@ -157,7 +156,7 @@ export type { ServerModuleRunnerOptions } from './ssr/runtime/serverModuleRunner export type { BindCLIShortcutsOptions, CLIShortcut } from './shortcuts' export type { - HMRPayload, + HotPayload, ConnectedPayload, UpdatePayload, Update, @@ -165,7 +164,7 @@ export type { CustomPayload, PrunePayload, ErrorPayload, -} from 'types/hmrPayload' +} from 'types/hotPayload' export type { CustomEventMap, InferCustomEventPayload, diff --git a/packages/vite/src/node/plugins/esbuild.ts b/packages/vite/src/node/plugins/esbuild.ts index cf368094e4ed01..219a1f624a5df8 100644 --- a/packages/vite/src/node/plugins/esbuild.ts +++ b/packages/vite/src/node/plugins/esbuild.ts @@ -479,7 +479,7 @@ export async function reloadOnTsconfigChange( // any json file in the tsconfig cache could have been used to compile ts if ( path.basename(changedFile) === 'tsconfig.json' || - changedFile.endsWith('.json') /* + changedFile.endsWith('.json') /* TODO: the tsconfckCache?.clear() line will make this fail if there are several servers we may need a cache per server if we don't want all servers to share the reset leaving it commented for now because it should still work @@ -500,7 +500,7 @@ export async function reloadOnTsconfigChange( tsconfckCache?.clear() // force full reload - server.hot.send({ + server.ws.send({ type: 'full-reload', path: '*', }) diff --git a/packages/vite/src/node/server/__tests__/pluginContainer.spec.ts b/packages/vite/src/node/server/__tests__/pluginContainer.spec.ts index a92d38c7f01af9..b492d6d4494856 100644 --- a/packages/vite/src/node/server/__tests__/pluginContainer.spec.ts +++ b/packages/vite/src/node/server/__tests__/pluginContainer.spec.ts @@ -222,7 +222,7 @@ async function getDevEnvironment( // @ts-expect-error This plugin requires a ViteDevServer instance. config.plugins = config.plugins.filter((p) => !p.name.includes('pre-alias')) - const environment = new DevEnvironment('client', config) + const environment = new DevEnvironment('client', config, { hot: false }) await environment.init() return environment diff --git a/packages/vite/src/node/server/environment.ts b/packages/vite/src/node/server/environment.ts index 3aa773e8db47d3..f52b27b335b046 100644 --- a/packages/vite/src/node/server/environment.ts +++ b/packages/vite/src/node/server/environment.ts @@ -20,8 +20,8 @@ import { } from '../optimizer/optimizer' import { resolveEnvironmentPlugins } from '../plugin' import { EnvironmentModuleGraph } from './moduleGraph' -import type { HMRChannel } from './hmr' -import { createNoopHMRChannel, getShortName, updateModules } from './hmr' +import type { HotChannel } from './hmr' +import { createNoopHotChannel, getShortName, updateModules } from './hmr' import type { TransformResult } from './transformRequest' import { transformRequest } from './transformRequest' import type { EnvironmentPluginContainer } from './pluginContainer' @@ -32,7 +32,7 @@ import { import type { RemoteEnvironmentTransport } from './environmentTransport' export interface DevEnvironmentSetup { - hot?: false | HMRChannel + hot: false | HotChannel watcher?: FSWatcher options?: EnvironmentOptions runner?: FetchModuleOptions & { @@ -90,24 +90,24 @@ export class DevEnvironment extends BaseEnvironment { _crawlEndFinder: CrawlEndFinder /** - * HMR channel for this environment. If not provided or disabled, + * Hot 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 + hot: HotChannel constructor( name: string, config: ResolvedConfig, - setup?: DevEnvironmentSetup, + setup: DevEnvironmentSetup, ) { let options = config.environments[name] ?? getDefaultResolvedEnvironmentOptions(config) - if (setup?.options) { + if (setup.options) { options = mergeConfig( options, - setup?.options, + setup.options, ) as ResolvedEnvironmentOptions } super(name, config, options) @@ -118,17 +118,16 @@ export class DevEnvironment extends BaseEnvironment { this.pluginContainer!.resolveId(url, undefined), ) - this.hot = setup?.hot || createNoopHMRChannel() - this.watcher = setup?.watcher + this.hot = setup.hot || createNoopHotChannel() + 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._ssrRunnerOptions = setup.runner || {} + setup.runner?.transport?.register(this) this.hot.on('vite:invalidate', async ({ path, message }) => { invalidateModule(this, { @@ -138,8 +137,8 @@ export class DevEnvironment extends BaseEnvironment { }) const { optimizeDeps } = this.options.dev - if (setup?.depsOptimizer) { - this.depsOptimizer = setup?.depsOptimizer + if (setup.depsOptimizer) { + this.depsOptimizer = setup.depsOptimizer } else if (!isDepOptimizationEnabled(optimizeDeps)) { this.depsOptimizer = undefined } else { diff --git a/packages/vite/src/node/server/environments/nodeEnvironment.ts b/packages/vite/src/node/server/environments/nodeEnvironment.ts index 0e55fb8fe848ac..e4e76a1ba31eb2 100644 --- a/packages/vite/src/node/server/environments/nodeEnvironment.ts +++ b/packages/vite/src/node/server/environments/nodeEnvironment.ts @@ -6,7 +6,7 @@ import { asyncFunctionDeclarationPaddingLineCount } from '../../../shared/utils' export function createNodeDevEnvironment( name: string, config: ResolvedConfig, - options?: DevEnvironmentSetup, + options: DevEnvironmentSetup, ): DevEnvironment { return new DevEnvironment(name, config, { ...options, diff --git a/packages/vite/src/node/server/hmr.ts b/packages/vite/src/node/server/hmr.ts index 7f8f8e32206dc8..7f2fd70b7ad769 100644 --- a/packages/vite/src/node/server/hmr.ts +++ b/packages/vite/src/node/server/hmr.ts @@ -3,7 +3,7 @@ import path from 'node:path' import type { Server } from 'node:http' import { EventEmitter } from 'node:events' import colors from 'picocolors' -import type { CustomPayload, HMRPayload, Update } from 'types/hmrPayload' +import type { CustomPayload, HotPayload, Update } from 'types/hotPayload' import type { RollupError } from 'rollup' import { CLIENT_DIR } from '../constants' import { createDebugger, normalizePath } from '../utils' @@ -36,8 +36,6 @@ export interface HmrOptions { timeout?: number overlay?: boolean server?: Server - /** @internal */ - channels?: HMRChannel[] } export interface HotUpdateContext { @@ -68,26 +66,22 @@ interface PropagationBoundary { isWithinCircularImport: boolean } -export interface HMRBroadcasterClient { +export interface HotChannelClient { /** * Send event to the client */ - send(payload: HMRPayload): void + send(payload: HotPayload): void /** * Send custom event */ send(event: string, payload?: CustomPayload['data']): void } -export interface HMRChannel { - /** - * Unique channel name - */ - name: string +export interface HotChannel { /** * Broadcast events to all clients */ - send(payload: HMRPayload): void + send(payload: HotPayload): void /** * Send custom event */ @@ -99,7 +93,7 @@ export interface HMRChannel { event: T, listener: ( data: InferCustomEventPayload, - client: HMRBroadcasterClient, + client: HotChannelClient, ...args: any[] ) => void, ): void @@ -118,18 +112,6 @@ export interface HMRChannel { close(): void } -export interface HMRBroadcaster extends Omit { - /** - * All registered channels. Always has websocket channel. - */ - readonly channels: HMRChannel[] - /** - * Add a new third-party channel. - */ - addChannel(connection: HMRChannel): HMRBroadcaster - close(): Promise -} - export function getShortName(file: string, root: string): string { return file.startsWith(withTrailingSlash(root)) ? path.posix.relative(root, file) @@ -884,70 +866,20 @@ async function readModifiedFile(file: string): Promise { } } -export function createHMRBroadcaster(): HMRBroadcaster { - const channels: HMRChannel[] = [] - const readyChannels = new WeakSet() - const broadcaster: HMRBroadcaster = { - get channels() { - return [...channels] - }, - addChannel(channel) { - if (channels.some((c) => c.name === channel.name)) { - throw new Error(`HMR channel "${channel.name}" is already defined.`) - } - channels.push(channel) - return broadcaster - }, - on(event: string, listener: (...args: any[]) => any) { - // emit connection event only when all channels are ready - if (event === 'connection') { - // make a copy so we don't wait for channels that might be added after this is triggered - const channels = this.channels - channels.forEach((channel) => - channel.on('connection', () => { - readyChannels.add(channel) - if (channels.every((c) => readyChannels.has(c))) { - listener() - } - }), - ) - return - } - channels.forEach((channel) => channel.on(event, listener)) - return - }, - off(event, listener) { - channels.forEach((channel) => channel.off(event, listener)) - return - }, - send(...args: any[]) { - channels.forEach((channel) => channel.send(...(args as [any]))) - }, - listen() { - channels.forEach((channel) => channel.listen()) - }, - close() { - return Promise.all(channels.map((channel) => channel.close())) - }, - } - return broadcaster -} - -export interface ServerHMRChannel extends HMRChannel { +export interface ServerHotChannel extends HotChannel { api: { innerEmitter: EventEmitter outsideEmitter: EventEmitter } } -export function createServerHMRChannel(): ServerHMRChannel { +export function createServerHotChannel(): ServerHotChannel { const innerEmitter = new EventEmitter() const outsideEmitter = new EventEmitter() return { - name: 'ssr', send(...args: any[]) { - let payload: HMRPayload + let payload: HotPayload if (typeof args[0] === 'string') { payload = { type: 'custom', @@ -964,7 +896,7 @@ export function createServerHMRChannel(): ServerHMRChannel { }, on: ((event: string, listener: () => unknown) => { innerEmitter.on(event, listener) - }) as ServerHMRChannel['on'], + }) as ServerHotChannel['on'], close() { innerEmitter.removeAllListeners() outsideEmitter.removeAllListeners() @@ -979,13 +911,12 @@ export function createServerHMRChannel(): ServerHMRChannel { } } -export function createNoopHMRChannel(): HMRChannel { +export function createNoopHotChannel(): HotChannel { function noop() { // noop } return { - name: 'noop', send: noop, on: noop, off: noop, diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 8f8a66022df113..5b39f884db1f7a 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -79,13 +79,8 @@ import { ModuleGraph } from './mixedModuleGraph' import type { ModuleNode } from './mixedModuleGraph' import { notFoundMiddleware } from './middlewares/notFound' import { errorMiddleware } from './middlewares/error' -import type { HMRBroadcaster, HmrOptions } from './hmr' -import { - createHMRBroadcaster, - createServerHMRChannel, - handleHMRUpdate, - updateModules, -} from './hmr' +import type { HmrOptions } from './hmr' +import { createServerHotChannel, handleHMRUpdate, updateModules } from './hmr' import { openBrowser as _openBrowser } from './openBrowser' import type { TransformOptions, TransformResult } from './transformRequest' import { transformRequest } from './transformRequest' @@ -251,17 +246,9 @@ export interface ViteDevServer { watcher: FSWatcher /** * web socket server with `send(payload)` method - * @deprecated use `hot` instead - */ - ws: WebSocketServer - /** - * HMR broadcaster that can be used to send custom HMR messages to the client - * - * 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 + ws: WebSocketServer /** * Rollup plugin container that can run plugin hooks on a given file * @deprecated use `environment.pluginContainer` instead @@ -455,11 +442,7 @@ export async function _createServer( : await resolveHttpServer(serverConfig, middlewares, httpsOptions) const ws = createWebSocketServer(httpServer, config, httpsOptions) - 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)) - } + const ssrHotChannel = createServerHotChannel() const publicFiles = await initPublicFilesPromise const { publicDir } = config @@ -540,7 +523,6 @@ export async function _createServer( httpServer, watcher, ws, - hot, environments, pluginContainer, @@ -691,7 +673,7 @@ export async function _createServer( await Promise.allSettled([ watcher.close(), - hot.close(), + ws.close(), Promise.allSettled( Object.values(server.environments).map((environment) => environment.close(), @@ -963,7 +945,7 @@ export async function _createServer( httpServer.listen = (async (port: number, ...args: any[]) => { try { // ensure ws server started - hot.listen() + Object.values(environments).forEach((e) => e.hot.listen()) await initServer() } catch (e) { httpServer.emit('error', e) @@ -973,7 +955,7 @@ export async function _createServer( }) as any } else { if (options.hotListen) { - hot.listen() + Object.values(environments).forEach((e) => e.hot.listen()) } await initServer() } @@ -1179,7 +1161,7 @@ async function restartServer(server: ViteDevServer) { if (!middlewareMode) { await server.listen(port, true) } else { - server.hot.listen() + server.ws.listen() } logger.info('server restarted.', { timestamp: true }) diff --git a/packages/vite/src/node/server/middlewares/error.ts b/packages/vite/src/node/server/middlewares/error.ts index a4de10dc8c8d17..d5c326e0185a6d 100644 --- a/packages/vite/src/node/server/middlewares/error.ts +++ b/packages/vite/src/node/server/middlewares/error.ts @@ -2,7 +2,7 @@ import colors from 'picocolors' import type { RollupError } from 'rollup' import type { Connect } from 'dep-types/connect' import strip from 'strip-ansi' -import type { ErrorPayload } from 'types/hmrPayload' +import type { ErrorPayload } from 'types/hotPayload' import { pad } from '../../utils' import type { ViteDevServer } from '../..' diff --git a/packages/vite/src/node/server/ws.ts b/packages/vite/src/node/server/ws.ts index 6b70d1fbea5e77..061ed270d5d337 100644 --- a/packages/vite/src/node/server/ws.ts +++ b/packages/vite/src/node/server/ws.ts @@ -9,11 +9,11 @@ import colors from 'picocolors' import type { WebSocket as WebSocketRaw } from 'ws' import { WebSocketServer as WebSocketServerRaw_ } from 'ws' import type { WebSocket as WebSocketTypes } from 'dep-types/ws' -import type { CustomPayload, ErrorPayload, HMRPayload } from 'types/hmrPayload' +import type { ErrorPayload, HotPayload } from 'types/hotPayload' import type { InferCustomEventPayload } from 'types/customEvent' -import type { ResolvedConfig } from '..' +import type { HotChannelClient, ResolvedConfig } from '..' import { isObject } from '../utils' -import type { HMRChannel } from './hmr' +import type { HotChannel } from './hmr' import type { HttpServer } from '.' /* In Bun, the `ws` module is overridden to hook into the native code. Using the bundled `js` version @@ -31,7 +31,7 @@ export type WebSocketCustomListener = ( client: WebSocketClient, ) => void -export interface WebSocketServer extends HMRChannel { +export interface WebSocketServer extends HotChannel { /** * Listen on port and host */ @@ -61,15 +61,7 @@ export interface WebSocketServer extends HMRChannel { } } -export interface WebSocketClient { - /** - * Send event to the client - */ - send(payload: HMRPayload): void - /** - * Send custom event - */ - send(event: string, payload?: CustomPayload['data']): void +export interface WebSocketClient extends HotChannelClient { /** * The raw WebSocket instance * @advanced @@ -198,7 +190,7 @@ export function createWebSocketServer( if (!clientsMap.has(socket)) { clientsMap.set(socket, { send: (...args) => { - let payload: HMRPayload + let payload: HotPayload if (typeof args[0] === 'string') { payload = { type: 'custom', @@ -223,7 +215,6 @@ export function createWebSocketServer( let bufferedError: ErrorPayload | null = null return { - name: 'ws', listen: () => { wsHttpServer?.listen(port, host) }, @@ -249,7 +240,7 @@ export function createWebSocketServer( }, send(...args: any[]) { - let payload: HMRPayload + let payload: HotPayload if (typeof args[0] === 'string') { payload = { type: 'custom', diff --git a/packages/vite/src/node/ssr/runtime/__tests__/utils.ts b/packages/vite/src/node/ssr/runtime/__tests__/utils.ts index 9d52ffa0c7cbd6..9a91992aa32fa2 100644 --- a/packages/vite/src/node/ssr/runtime/__tests__/utils.ts +++ b/packages/vite/src/node/ssr/runtime/__tests__/utils.ts @@ -76,7 +76,7 @@ export async function createModuleRunnerTester( ...config, }) t.environment = t.server.environments.ssr - t.runner = await createServerModuleRunner(t.environment, { + t.runner = createServerModuleRunner(t.environment, { hmr: { logger: false, }, diff --git a/packages/vite/src/node/ssr/runtime/serverHmrConnector.ts b/packages/vite/src/node/ssr/runtime/serverHmrConnector.ts index 282da14eeca4bc..5a4000794ba15a 100644 --- a/packages/vite/src/node/ssr/runtime/serverHmrConnector.ts +++ b/packages/vite/src/node/ssr/runtime/serverHmrConnector.ts @@ -1,12 +1,12 @@ -import type { CustomPayload, HMRPayload } from 'types/hmrPayload' +import type { CustomPayload, HotPayload } from 'types/hotPayload' import type { ModuleRunnerHMRConnection } from 'vite/module-runner' -import type { HMRBroadcasterClient, ServerHMRChannel } from '../../server/hmr' +import type { HotChannelClient, ServerHotChannel } from '../../server/hmr' -class ServerHMRBroadcasterClient implements HMRBroadcasterClient { - constructor(private readonly hmrChannel: ServerHMRChannel) {} +class ServerHMRBroadcasterClient implements HotChannelClient { + constructor(private readonly hotChannel: ServerHotChannel) {} send(...args: any[]) { - let payload: HMRPayload + let payload: HotPayload if (typeof args[0] === 'string') { payload = { type: 'custom', @@ -21,7 +21,7 @@ class ServerHMRBroadcasterClient implements HMRBroadcasterClient { 'Cannot send non-custom events from the client to the server.', ) } - this.hmrChannel.send(payload) + this.hotChannel.send(payload) } } @@ -30,17 +30,17 @@ class ServerHMRBroadcasterClient implements HMRBroadcasterClient { * @experimental */ export class ServerHMRConnector implements ModuleRunnerHMRConnection { - private handlers: ((payload: HMRPayload) => void)[] = [] + private handlers: ((payload: HotPayload) => void)[] = [] private hmrClient: ServerHMRBroadcasterClient private connected = false - constructor(private hmrChannel: ServerHMRChannel) { - this.hmrClient = new ServerHMRBroadcasterClient(hmrChannel) - hmrChannel.api.outsideEmitter.on('send', (payload: HMRPayload) => { + constructor(private hotChannel: ServerHotChannel) { + this.hmrClient = new ServerHMRBroadcasterClient(hotChannel) + hotChannel.api.outsideEmitter.on('send', (payload: HotPayload) => { this.handlers.forEach((listener) => listener(payload)) }) - this.hmrChannel = hmrChannel + this.hotChannel = hotChannel } isReady(): boolean { @@ -49,14 +49,14 @@ export class ServerHMRConnector implements ModuleRunnerHMRConnection { send(message: string): void { const payload = JSON.parse(message) as CustomPayload - this.hmrChannel.api.innerEmitter.emit( + this.hotChannel.api.innerEmitter.emit( payload.event, payload.data, this.hmrClient, ) } - onUpdate(handler: (payload: HMRPayload) => void): void { + onUpdate(handler: (payload: HotPayload) => void): void { this.handlers.push(handler) handler({ type: 'connected' }) this.connected = true diff --git a/packages/vite/src/node/ssr/runtime/serverModuleRunner.ts b/packages/vite/src/node/ssr/runtime/serverModuleRunner.ts index f24009ad4fd7be..2df093ead35fa7 100644 --- a/packages/vite/src/node/ssr/runtime/serverModuleRunner.ts +++ b/packages/vite/src/node/ssr/runtime/serverModuleRunner.ts @@ -7,7 +7,7 @@ import type { ModuleRunnerOptions, } from 'vite/module-runner' import type { DevEnvironment } from '../../server/environment' -import type { ServerHMRChannel } from '../../server/hmr' +import type { ServerHotChannel } from '../../server/hmr' import { ServerHMRConnector } from './serverHmrConnector' /** @@ -47,7 +47,7 @@ function createHMROptions( } } if (!('api' in environment.hot)) return false - const connection = new ServerHMRConnector(environment.hot as ServerHMRChannel) + const connection = new ServerHMRConnector(environment.hot as ServerHotChannel) return { connection, logger: options.hmr?.logger, diff --git a/packages/vite/src/shared/hmr.ts b/packages/vite/src/shared/hmr.ts index ee4f727158f60e..919d1054b5d9b2 100644 --- a/packages/vite/src/shared/hmr.ts +++ b/packages/vite/src/shared/hmr.ts @@ -1,4 +1,4 @@ -import type { Update } from 'types/hmrPayload' +import type { Update } from 'types/hotPayload' import type { ModuleNamespace, ViteHotContext } from 'types/hot' import type { InferCustomEventPayload } from 'types/customEvent' diff --git a/packages/vite/types/customEvent.d.ts b/packages/vite/types/customEvent.d.ts index b816a4c6c22907..e9d943aab32f64 100644 --- a/packages/vite/types/customEvent.d.ts +++ b/packages/vite/types/customEvent.d.ts @@ -3,7 +3,7 @@ import type { FullReloadPayload, PrunePayload, UpdatePayload, -} from './hmrPayload' +} from './hotPayload' export interface CustomEventMap { 'vite:beforeUpdate': UpdatePayload diff --git a/packages/vite/types/hmrPayload.d.ts b/packages/vite/types/hotPayload.d.ts similarity index 91% rename from packages/vite/types/hmrPayload.d.ts rename to packages/vite/types/hotPayload.d.ts index 2c6a9d0c2d1121..7c7f81f6cc1daa 100644 --- a/packages/vite/types/hmrPayload.d.ts +++ b/packages/vite/types/hotPayload.d.ts @@ -1,4 +1,6 @@ -export type HMRPayload = +/** @deprecated use HotPayload */ +export type HMRPayload = HotPayload +export type HotPayload = | ConnectedPayload | UpdatePayload | FullReloadPayload diff --git a/playground/hmr-ssr/vite.config.ts b/playground/hmr-ssr/vite.config.ts index 2d570d79acf8c4..f03eac818eee1b 100644 --- a/playground/hmr-ssr/vite.config.ts +++ b/playground/hmr-ssr/vite.config.ts @@ -8,18 +8,21 @@ export default defineConfig({ plugins: [ { name: 'mock-custom', - async handleHotUpdate({ file, read, server }) { + async hotUpdate({ environment, file, read, server }) { if (file.endsWith('customFile.js')) { const content = await read() const msg = content.match(/export const msg = '(\w+)'/)[1] - server.hot.send('custom:foo', { msg }) - server.hot.send('custom:remove', { msg }) + environment.hot.send('custom:foo', { msg }) + environment.hot.send('custom:remove', { msg }) } }, configureServer(server) { - server.hot.on('custom:remote-add', ({ a, b }, client) => { - client.send('custom:remote-add-result', { result: a + b }) - }) + server.environments.ssr.hot.on( + 'custom:remote-add', + ({ a, b }, client) => { + client.send('custom:remote-add-result', { result: a + b }) + }, + ) }, }, virtualPlugin(), @@ -45,7 +48,7 @@ export const virtual = _virtual + '${num}';` } }, configureServer(server) { - server.hot.on('virtual:increment', async () => { + server.environments.ssr.hot.on('virtual:increment', async () => { const mod = await server.environments.ssr.moduleGraph.getModuleByUrl( '\0virtual:file', diff --git a/playground/hmr/vite.config.ts b/playground/hmr/vite.config.ts index 1e3507e0e0f998..64d47797a841cd 100644 --- a/playground/hmr/vite.config.ts +++ b/playground/hmr/vite.config.ts @@ -10,18 +10,21 @@ export default defineConfig({ plugins: [ { name: 'mock-custom', - async handleHotUpdate({ file, read, server }) { + async hotUpdate({ environment, file, read }) { if (file.endsWith('customFile.js')) { const content = await read() const msg = content.match(/export const msg = '(\w+)'/)[1] - server.hot.send('custom:foo', { msg }) - server.hot.send('custom:remove', { msg }) + environment.hot.send('custom:foo', { msg }) + environment.hot.send('custom:remove', { msg }) } }, configureServer(server) { - server.hot.on('custom:remote-add', ({ a, b }, client) => { - client.send('custom:remote-add-result', { result: a + b }) - }) + server.environments.client.hot.on( + 'custom:remote-add', + ({ a, b }, client) => { + client.send('custom:remote-add-result', { result: a + b }) + }, + ) }, }, virtualPlugin(), @@ -47,7 +50,7 @@ export const virtual = _virtual + '${num}';` } }, configureServer(server) { - server.hot.on('virtual:increment', async () => { + server.environments.client.hot.on('virtual:increment', async () => { const mod = await server.environments.client.moduleGraph.getModuleByUrl( '\0virtual:file', diff --git a/playground/html/__tests__/html.spec.ts b/playground/html/__tests__/html.spec.ts index 3574412b49a485..896267ea06bae5 100644 --- a/playground/html/__tests__/html.spec.ts +++ b/playground/html/__tests__/html.spec.ts @@ -282,7 +282,7 @@ describe.runIf(isServe)('invalid', () => { await page.keyboard.press('Escape') await hiddenPromise - viteServer.hot.send({ + viteServer.environments.client.hot.send({ type: 'error', err: { message: 'someError', From e45881c8277db1ec3b820f7e39efa2df0a1eea77 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Fri, 24 May 2024 10:26:25 +0200 Subject: [PATCH 091/123] release: v6.0.0-alpha.17 --- packages/vite/CHANGELOG.md | 15 +++++++++++++++ packages/vite/package.json | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/vite/CHANGELOG.md b/packages/vite/CHANGELOG.md index 8f49083f3039e0..1f9fb7825a63bf 100644 --- a/packages/vite/CHANGELOG.md +++ b/packages/vite/CHANGELOG.md @@ -1,3 +1,18 @@ +## 6.0.0-alpha.17 (2024-05-24) + +* refactor!: rename to HMRPayload to HotPayload and remove HMRBroadcaster (#16875) ([87bbb04](https://github.com/vitejs/vite/commit/87bbb04)), closes [#16875](https://github.com/vitejs/vite/issues/16875) +* fix: dep optimization options ([624e751](https://github.com/vitejs/vite/commit/624e751)) +* fix: DepOptimizationConfig ([5c655e6](https://github.com/vitejs/vite/commit/5c655e6)) +* fix: handleHotUpdate compat (#17295) ([11bddb0](https://github.com/vitejs/vite/commit/11bddb0)), closes [#17295](https://github.com/vitejs/vite/issues/17295) +* fix: optimizeDeps back compat ([478a9aa](https://github.com/vitejs/vite/commit/478a9aa)) +* fix(types): avoid referencing `WeakKey` type ([e339959](https://github.com/vitejs/vite/commit/e339959)) +* feat: per-environment optimizeDeps entries and force ([006cfb7](https://github.com/vitejs/vite/commit/006cfb7)) +* feat: reporter as shared plugin using createWeakData (#17293) ([2b69389](https://github.com/vitejs/vite/commit/2b69389)), closes [#17293](https://github.com/vitejs/vite/issues/17293) +* chore: refactor isDepsOptimizerEnabled ([5f36aa6](https://github.com/vitejs/vite/commit/5f36aa6)) +* docs: deprecate server.hot (#16741) ([e7d38ab](https://github.com/vitejs/vite/commit/e7d38ab)), closes [#16741](https://github.com/vitejs/vite/issues/16741) + + + ## 6.0.0-alpha.16 (2024-05-23) * feat: createWeakData ([bcb1959](https://github.com/vitejs/vite/commit/bcb1959)) diff --git a/packages/vite/package.json b/packages/vite/package.json index f13736e4b9b855..0a4dfba4a1e47b 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -1,6 +1,6 @@ { "name": "vite", - "version": "6.0.0-alpha.16", + "version": "6.0.0-alpha.17", "type": "module", "license": "MIT", "author": "Evan You", From c43c9875b79358729c4ad5343ec61015b8fcf0ee Mon Sep 17 00:00:00 2001 From: patak <583075+patak-dev@users.noreply.github.com> Date: Fri, 24 May 2024 12:45:14 +0200 Subject: [PATCH 092/123] refactor: reporter using custom per env api (#17297) --- packages/vite/src/node/environment.ts | 30 +- .../src/node/plugins/dynamicImportVars.ts | 6 +- packages/vite/src/node/plugins/reporter.ts | 458 +++++++++--------- packages/vite/src/node/publicUtils.ts | 1 - packages/vite/src/node/utils.ts | 19 - 5 files changed, 268 insertions(+), 246 deletions(-) diff --git a/packages/vite/src/node/environment.ts b/packages/vite/src/node/environment.ts index da8fe81b9c62de..30a5295bc6495b 100644 --- a/packages/vite/src/node/environment.ts +++ b/packages/vite/src/node/environment.ts @@ -1,10 +1,38 @@ import type { DevEnvironment } from './server/environment' import type { BuildEnvironment } from './build' import type { ScanEnvironment } from './optimizer/scan' -import { type FutureCompatEnvironment } from './baseEnvironment' +import type { FutureCompatEnvironment } from './baseEnvironment' +import type { PluginContext } from './plugin' export type Environment = | DevEnvironment | BuildEnvironment | ScanEnvironment | FutureCompatEnvironment + +/** + * Creates a function that hides the complexities of a WeakMap with an initial value + * to implement object metadata. Used by plugins to implement cross hooks per + * environment metadata + */ +export function usePerEnvironmentState( + initial: (environment: Environment) => State, +): (context: PluginContext) => State { + const stateMap = new WeakMap() + return function (context: PluginContext) { + const { environment } = context + if (!environment) { + context.error( + new Error( + `Per environment state called with undefined environment. You may be using a Vite v6+ plugin in Vite v5 or Rollup.`, + ), + ) + } + let state = stateMap.get(environment) + if (!state) { + state = initial(environment) + stateMap.set(environment, state) + } + return state + } +} diff --git a/packages/vite/src/node/plugins/dynamicImportVars.ts b/packages/vite/src/node/plugins/dynamicImportVars.ts index 04f1ef215754f8..2153bedd12b1c0 100644 --- a/packages/vite/src/node/plugins/dynamicImportVars.ts +++ b/packages/vite/src/node/plugins/dynamicImportVars.ts @@ -10,7 +10,6 @@ import { CLIENT_ENTRY } from '../constants' import { createIdResolver } from '../idResolver' import { createFilter, - createWeakData, normalizePath, rawRE, requestQueryMaybeEscapedSplitRE, @@ -19,6 +18,7 @@ import { urlRE, } from '../utils' import type { Environment } from '../environment' +import { usePerEnvironmentState } from '../environment' import { toAbsoluteGlob } from './importMetaGlob' import { hasViteIgnoreRE } from './importAnalysis' import { workerOrSharedWorkerRE } from './worker' @@ -175,7 +175,7 @@ export function dynamicImportVarsPlugin(config: ResolvedConfig): Plugin { extensions: [], }) - const getFilter = createWeakData((environment: Environment) => { + const getFilter = usePerEnvironmentState((environment: Environment) => { const { include, exclude } = environment.options.build.dynamicImportVarsOptions return createFilter(include, exclude) @@ -200,7 +200,7 @@ export function dynamicImportVarsPlugin(config: ResolvedConfig): Plugin { const { environment } = this if ( !environment || - !getFilter(environment)(importer) || + !getFilter(this)(importer) || importer === CLIENT_ENTRY || !hasDynamicImportRE.test(source) ) { diff --git a/packages/vite/src/node/plugins/reporter.ts b/packages/vite/src/node/plugins/reporter.ts index ab3f6f26d90dc4..77626ef57c1c09 100644 --- a/packages/vite/src/node/plugins/reporter.ts +++ b/packages/vite/src/node/plugins/reporter.ts @@ -2,15 +2,12 @@ import path from 'node:path' import { gzip } from 'node:zlib' import { promisify } from 'node:util' import colors from 'picocolors' +import type { OutputBundle } from 'rollup' import type { Plugin } from '../plugin' import type { ResolvedConfig } from '../config' import type { Environment } from '../environment' -import { - createWeakData, - isDefined, - isInNodeModules, - normalizePath, -} from '../utils' +import { usePerEnvironmentState } from '../environment' +import { isDefined, isInNodeModules, normalizePath } from '../utils' import { LogLevels } from '../logger' import { withTrailingSlash } from '../../shared/utils' @@ -42,98 +39,258 @@ export function buildReporterPlugin(config: ResolvedConfig): Plugin { const tty = process.stdout.isTTY && !process.env.CI const shouldLogInfo = LogLevels[config.logLevel || 'info'] >= LogLevels.info - const getData = createWeakData((environment: Environment) => { - const data = { - hasTransformed: false, - hasRenderedChunk: false, - hasCompressChunk: false, - transformedCount: 0, - chunkCount: 0, - compressedCount: 0, - logTransform: throttle((id: string) => { - writeLine( - `transforming (${data.transformedCount}) ${colors.dim( - path.relative(config.root, id), - )}`, - ) - }), + + const modulesReporter = usePerEnvironmentState((environment: Environment) => { + let hasTransformed = false + let transformedCount = 0 + + const logTransform = throttle((id: string) => { + writeLine( + `transforming (${transformedCount}) ${colors.dim( + path.relative(config.root, id), + )}`, + ) + }) + + return { + reset() { + transformedCount = 0 + }, + register(id: string) { + transformedCount++ + if (shouldLogInfo) { + if (!tty) { + if (!hasTransformed) { + config.logger.info(`transforming...`) + } + } else { + if (id.includes(`?`)) return + logTransform(id) + } + hasTransformed = true + } + }, + log() { + if (shouldLogInfo) { + if (tty) { + clearLine() + } + environment.logger.info( + `${colors.green(`✓`)} ${transformedCount} modules transformed.`, + ) + } + }, } - return data }) - async function getCompressedSize( - environment: Environment, - code: string | Uint8Array, - ): Promise { - if ( - environment.options.build.ssr || - !environment.options.build.reportCompressedSize - ) { - return null - } - const data = getData(environment) - if (shouldLogInfo && !data.hasCompressChunk) { - if (!tty) { - config.logger.info('computing gzip size...') - } else { - writeLine('computing gzip size (0)...') + const chunksReporter = usePerEnvironmentState((environment: Environment) => { + let hasRenderedChunk = false + let hasCompressChunk = false + let chunkCount = 0 + let compressedCount = 0 + + async function getCompressedSize( + code: string | Uint8Array, + ): Promise { + if ( + environment.options.build.ssr || + !environment.options.build.reportCompressedSize + ) { + return null + } + if (shouldLogInfo && !hasCompressChunk) { + if (!tty) { + config.logger.info('computing gzip size...') + } else { + writeLine('computing gzip size (0)...') + } + hasCompressChunk = true + } + const compressed = await compress( + typeof code === 'string' ? code : Buffer.from(code), + ) + compressedCount++ + if (shouldLogInfo && tty) { + writeLine(`computing gzip size (${compressedCount})...`) } - data.hasCompressChunk = true + return compressed.length } - const compressed = await compress( - typeof code === 'string' ? code : Buffer.from(code), - ) - data.compressedCount++ - if (shouldLogInfo && tty) { - writeLine(`computing gzip size (${data.compressedCount})...`) + + return { + reset() { + chunkCount = 0 + compressedCount = 0 + }, + register() { + chunkCount++ + if (shouldLogInfo) { + if (!tty) { + if (!hasRenderedChunk) { + environment.logger.info('rendering chunks...') + } + } else { + writeLine(`rendering chunks (${chunkCount})...`) + } + hasRenderedChunk = true + } + }, + async log(output: OutputBundle, outDir?: string) { + const chunkLimit = environment.options.build.chunkSizeWarningLimit + + let hasLargeChunks = false + + if (shouldLogInfo) { + const entries = ( + await Promise.all( + Object.values(output).map( + async (chunk): Promise => { + if (chunk.type === 'chunk') { + return { + name: chunk.fileName, + group: 'JS', + size: chunk.code.length, + compressedSize: await getCompressedSize(chunk.code), + mapSize: chunk.map ? chunk.map.toString().length : null, + } + } else { + if (chunk.fileName.endsWith('.map')) return null + const isCSS = chunk.fileName.endsWith('.css') + const isCompressible = + isCSS || COMPRESSIBLE_ASSETS_RE.test(chunk.fileName) + return { + name: chunk.fileName, + group: isCSS ? 'CSS' : 'Assets', + size: chunk.source.length, + mapSize: null, // Rollup doesn't support CSS maps? + compressedSize: isCompressible + ? await getCompressedSize(chunk.source) + : null, + } + } + }, + ), + ) + ).filter(isDefined) + if (tty) clearLine() + + let longest = 0 + let biggestSize = 0 + let biggestMap = 0 + let biggestCompressSize = 0 + for (const entry of entries) { + if (entry.name.length > longest) longest = entry.name.length + if (entry.size > biggestSize) biggestSize = entry.size + if (entry.mapSize && entry.mapSize > biggestMap) { + biggestMap = entry.mapSize + } + if ( + entry.compressedSize && + entry.compressedSize > biggestCompressSize + ) { + biggestCompressSize = entry.compressedSize + } + } + + const sizePad = displaySize(biggestSize).length + const mapPad = displaySize(biggestMap).length + const compressPad = displaySize(biggestCompressSize).length + + const relativeOutDir = normalizePath( + path.relative( + config.root, + path.resolve( + config.root, + outDir ?? environment.options.build.outDir, + ), + ), + ) + const assetsDir = path.join(environment.options.build.assetsDir, '/') + + for (const group of groups) { + const filtered = entries.filter((e) => e.group === group.name) + if (!filtered.length) continue + for (const entry of filtered.sort((a, z) => a.size - z.size)) { + const isLarge = + group.name === 'JS' && entry.size / 1000 > chunkLimit + if (isLarge) hasLargeChunks = true + const sizeColor = isLarge ? colors.yellow : colors.dim + let log = colors.dim(withTrailingSlash(relativeOutDir)) + log += + !config.build.lib && + entry.name.startsWith(withTrailingSlash(assetsDir)) + ? colors.dim(assetsDir) + + group.color( + entry.name + .slice(assetsDir.length) + .padEnd(longest + 2 - assetsDir.length), + ) + : group.color(entry.name.padEnd(longest + 2)) + log += colors.bold( + sizeColor(displaySize(entry.size).padStart(sizePad)), + ) + if (entry.compressedSize) { + log += colors.dim( + ` │ gzip: ${displaySize(entry.compressedSize).padStart( + compressPad, + )}`, + ) + } + if (entry.mapSize) { + log += colors.dim( + ` │ map: ${displaySize(entry.mapSize).padStart(mapPad)}`, + ) + } + config.logger.info(log) + } + } + } else { + hasLargeChunks = Object.values(output).some((chunk) => { + return ( + chunk.type === 'chunk' && chunk.code.length / 1000 > chunkLimit + ) + }) + } + + if ( + hasLargeChunks && + environment.options.build.minify && + !config.build.lib && + !environment.options.build.ssr + ) { + environment.logger.warn( + colors.yellow( + `\n(!) Some chunks are larger than ${chunkLimit} kB after minification. Consider:\n` + + `- Using dynamic import() to code-split the application\n` + + `- Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/configuration-options/#output-manualchunks\n` + + `- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.`, + ), + ) + } + }, } - return compressed.length - } + }) return { name: 'vite:reporter', sharedDuringBuild: true, transform(_, id) { - const data = getData(this.environment!) - - data.transformedCount++ - if (shouldLogInfo) { - if (!tty) { - if (!data.hasTransformed) { - config.logger.info(`transforming...`) - } - } else { - if (id.includes(`?`)) return - data.logTransform(id) - } - data.hasTransformed = true - } - return null + modulesReporter(this).register(id) }, buildStart() { - getData(this.environment!).transformedCount = 0 + modulesReporter(this).reset() }, buildEnd() { - if (shouldLogInfo) { - if (tty) { - clearLine() - } - config.logger.info( - `${colors.green(`✓`)} ${getData(this.environment!).transformedCount} modules transformed.`, - ) - } + modulesReporter(this).log() }, renderStart() { - const data = getData(this.environment!) - data.chunkCount = 0 - data.compressedCount = 0 + chunksReporter(this).reset() }, - renderChunk(code, chunk, options) { + renderChunk(_, chunk, options) { if (!options.inlineDynamicImports) { for (const id of chunk.moduleIds) { const module = this.getModuleInfo(id) @@ -163,159 +320,16 @@ export function buildReporterPlugin(config: ResolvedConfig): Plugin { } } } - const data = getData(this.environment!) - data.chunkCount++ - if (shouldLogInfo) { - if (!tty) { - if (!data.hasRenderedChunk) { - config.logger.info('rendering chunks...') - } - } else { - writeLine(`rendering chunks (${data.chunkCount})...`) - } - data.hasRenderedChunk = true - } - return null + + chunksReporter(this).register() }, generateBundle() { if (shouldLogInfo && tty) clearLine() }, - async writeBundle({ dir: outDir }, output) { - const environment = this.environment! - const chunkLimit = environment.options.build.chunkSizeWarningLimit - - let hasLargeChunks = false - - if (shouldLogInfo) { - const entries = ( - await Promise.all( - Object.values(output).map( - async (chunk): Promise => { - if (chunk.type === 'chunk') { - return { - name: chunk.fileName, - group: 'JS', - size: chunk.code.length, - compressedSize: await getCompressedSize( - environment, - chunk.code, - ), - mapSize: chunk.map ? chunk.map.toString().length : null, - } - } else { - if (chunk.fileName.endsWith('.map')) return null - const isCSS = chunk.fileName.endsWith('.css') - const isCompressible = - isCSS || COMPRESSIBLE_ASSETS_RE.test(chunk.fileName) - return { - name: chunk.fileName, - group: isCSS ? 'CSS' : 'Assets', - size: chunk.source.length, - mapSize: null, // Rollup doesn't support CSS maps? - compressedSize: isCompressible - ? await getCompressedSize(environment, chunk.source) - : null, - } - } - }, - ), - ) - ).filter(isDefined) - if (tty) clearLine() - - let longest = 0 - let biggestSize = 0 - let biggestMap = 0 - let biggestCompressSize = 0 - for (const entry of entries) { - if (entry.name.length > longest) longest = entry.name.length - if (entry.size > biggestSize) biggestSize = entry.size - if (entry.mapSize && entry.mapSize > biggestMap) { - biggestMap = entry.mapSize - } - if ( - entry.compressedSize && - entry.compressedSize > biggestCompressSize - ) { - biggestCompressSize = entry.compressedSize - } - } - - const sizePad = displaySize(biggestSize).length - const mapPad = displaySize(biggestMap).length - const compressPad = displaySize(biggestCompressSize).length - - const relativeOutDir = normalizePath( - path.relative( - config.root, - path.resolve( - config.root, - outDir ?? environment.options.build.outDir, - ), - ), - ) - const assetsDir = path.join(environment.options.build.assetsDir, '/') - - for (const group of groups) { - const filtered = entries.filter((e) => e.group === group.name) - if (!filtered.length) continue - for (const entry of filtered.sort((a, z) => a.size - z.size)) { - const isLarge = - group.name === 'JS' && entry.size / 1000 > chunkLimit - if (isLarge) hasLargeChunks = true - const sizeColor = isLarge ? colors.yellow : colors.dim - let log = colors.dim(withTrailingSlash(relativeOutDir)) - log += - !config.build.lib && - entry.name.startsWith(withTrailingSlash(assetsDir)) - ? colors.dim(assetsDir) + - group.color( - entry.name - .slice(assetsDir.length) - .padEnd(longest + 2 - assetsDir.length), - ) - : group.color(entry.name.padEnd(longest + 2)) - log += colors.bold( - sizeColor(displaySize(entry.size).padStart(sizePad)), - ) - if (entry.compressedSize) { - log += colors.dim( - ` │ gzip: ${displaySize(entry.compressedSize).padStart( - compressPad, - )}`, - ) - } - if (entry.mapSize) { - log += colors.dim( - ` │ map: ${displaySize(entry.mapSize).padStart(mapPad)}`, - ) - } - config.logger.info(log) - } - } - } else { - hasLargeChunks = Object.values(output).some((chunk) => { - return chunk.type === 'chunk' && chunk.code.length / 1000 > chunkLimit - }) - } - - if ( - hasLargeChunks && - environment.options.build.minify && - !config.build.lib && - !environment.options.build.ssr - ) { - config.logger.warn( - colors.yellow( - `\n(!) Some chunks are larger than ${chunkLimit} kB after minification. Consider:\n` + - `- Using dynamic import() to code-split the application\n` + - `- Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/configuration-options/#output-manualchunks\n` + - `- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.`, - ), - ) - } + async writeBundle({ dir }, output) { + await chunksReporter(this).log(output, dir) }, } } diff --git a/packages/vite/src/node/publicUtils.ts b/packages/vite/src/node/publicUtils.ts index 7415f5792e8fc7..a9cad7a5106db6 100644 --- a/packages/vite/src/node/publicUtils.ts +++ b/packages/vite/src/node/publicUtils.ts @@ -15,7 +15,6 @@ export { mergeConfig, mergeAlias, createFilter, - createWeakData, rollupVersion, } from './utils' export { send } from './server/send' diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index d90c0c778c0c73..c0e3091a51b184 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -1438,22 +1438,3 @@ export function partialEncodeURIPath(uri: string): string { const postfix = filePath !== uri ? uri.slice(filePath.length) : '' return filePath.replaceAll('%', '%25') + postfix } - -/** - * Creates a function that hides the complexities of a WeakMap with an initial value - * to implement object metadata. Used by plugins to implement cross hooks per - * environment metadata - */ -export function createWeakData( - initial: (key: Key) => Data, -): (key: Key) => Data { - const cache = new WeakMap() - return function (key: Key) { - let data = cache.get(key) - if (!data) { - data = initial(key) - cache.set(key, data) - } - return data - } -} From 398a49c3a3ab85102dafa1c3660cba55211e4922 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Fri, 24 May 2024 13:09:14 +0200 Subject: [PATCH 093/123] feat(types): `defineVitePlugin` utils (#17298) --- packages/vite/src/node/index.ts | 8 ++++++- packages/vite/src/node/plugin.ts | 38 ++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index 8f6f55ad26eb0f..fab3c9ffd1ad17 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -8,6 +8,7 @@ export { resolveConfig, sortUserPlugins, } from './config' +export { defineVitePlugin } from './plugin' export { createServer } from './server' export { preview } from './preview' export { build, createBuilder } from './build' @@ -51,7 +52,12 @@ export type { DevEnvironmentOptions, ResolvedDevEnvironmentOptions, } from './config' -export type { Plugin, PluginOption, HookHandler } from './plugin' +export type { + Plugin, + PluginOption, + PluginWithEnvironment, + HookHandler, +} from './plugin' export type { Environment } from './environment' export type { FilterPattern } from './utils' export type { CorsOptions, CorsOrigin, CommonServerOptions } from './http' diff --git a/packages/vite/src/node/plugin.ts b/packages/vite/src/node/plugin.ts index 124ff55b478253..1cb18873a8909e 100644 --- a/packages/vite/src/node/plugin.ts +++ b/packages/vite/src/node/plugin.ts @@ -53,6 +53,13 @@ import type { Environment } from './environment' */ export interface PluginContextExtension { + /** + * Vite-specific environment instance + * + * Will always be defined in Vite 6+. Keep the type optional for backward compatible. + * + * @see TODO:link-to-docs + */ environment?: Environment } @@ -322,3 +329,34 @@ export function resolveEnvironmentPlugins(environment: Environment): Plugin[] { !plugin.applyToEnvironment || plugin.applyToEnvironment(environment), ) } + +/** + * Internal type to make sure `this.environment` is always defined + * + * Should be used via `defineVitePlugin` + */ +export type PluginWithEnvironment = { + [K in keyof Plugin]: Plugin[K] extends ObjectHook + ? ObjectHook> + : HandlerWithEnvironment +} + +export type HandlerWithEnvironment = H extends ( + this: infer This, + ...args: infer Args +) => infer Returns + ? This extends { environment?: Environment } + ? (this: This & { environment: Environment }, ...args: Args) => Returns + : H + : H + +/** + * Type utility to define a Vite-specific plugin where `this.environment` is always defined. + * + * Should only be used when you expect the plugin **only** runs on Vite 6+. + * + * It does an internal cast to returns a Rollup-compatible plugin type so you can pass into the array as before. + */ +export function defineVitePlugin(plugin: PluginWithEnvironment): Plugin { + return plugin as Plugin +} From 909fe2838c4a12bfb81be67926611205fd37f61c Mon Sep 17 00:00:00 2001 From: patak-dev Date: Fri, 24 May 2024 14:24:36 +0200 Subject: [PATCH 094/123] fix: lastHMRTimestamp compat --- packages/vite/src/node/server/mixedModuleGraph.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/node/server/mixedModuleGraph.ts b/packages/vite/src/node/server/mixedModuleGraph.ts index 7bc2b8b7112147..fb1c310667c8cc 100644 --- a/packages/vite/src/node/server/mixedModuleGraph.ts +++ b/packages/vite/src/node/server/mixedModuleGraph.ts @@ -129,15 +129,24 @@ export class ModuleNode { return this._ssrModule?.ssrError ?? null } get lastHMRTimestamp(): number { - return this._clientModule?.lastHMRTimestamp ?? 0 + return Math.max( + this._clientModule?.lastHMRTimestamp ?? 0, + this._ssrModule?.lastHMRTimestamp ?? 0, + ) } set lastHMRTimestamp(value: number) { if (this._clientModule) { this._clientModule.lastHMRTimestamp = value } + if (this._ssrModule) { + this._ssrModule.lastHMRTimestamp = value + } } get lastInvalidationTimestamp(): number { - return this._clientModule?.lastInvalidationTimestamp ?? 0 + return Math.max( + this._clientModule?.lastInvalidationTimestamp ?? 0, + this._ssrModule?.lastInvalidationTimestamp ?? 0, + ) } get invalidationState(): TransformResult | 'HARD_INVALIDATED' | undefined { return this._clientModule?.invalidationState From b7cbd3d3ea32e1cac4e071c71644669fe720e121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arnaud=20Barr=C3=A9?= Date: Fri, 24 May 2024 15:23:07 +0200 Subject: [PATCH 095/123] fix: missing HMRPayload export --- packages/vite/src/node/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index fab3c9ffd1ad17..3a07408e1796dd 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -162,6 +162,7 @@ export type { ServerModuleRunnerOptions } from './ssr/runtime/serverModuleRunner export type { BindCLIShortcutsOptions, CLIShortcut } from './shortcuts' export type { + HMRPayload, HotPayload, ConnectedPayload, UpdatePayload, From bdd1b912d84204a581770ff162849349f453173f Mon Sep 17 00:00:00 2001 From: patak <583075+patak-dev@users.noreply.github.com> Date: Fri, 24 May 2024 17:18:58 +0200 Subject: [PATCH 096/123] feat: non-nullable this.environment (#17302) --- packages/vite/src/node/build.ts | 26 ++++++------- packages/vite/src/node/environment.ts | 7 ---- packages/vite/src/node/index.ts | 8 +--- packages/vite/src/node/plugin.ts | 37 +------------------ packages/vite/src/node/plugins/asset.ts | 7 ++-- .../vite/src/node/plugins/clientInjections.ts | 1 - packages/vite/src/node/plugins/css.ts | 1 - packages/vite/src/node/plugins/define.ts | 2 - packages/vite/src/node/plugins/html.ts | 2 - .../vite/src/node/plugins/importAnalysis.ts | 1 - .../src/node/plugins/importAnalysisBuild.ts | 1 - .../vite/src/node/plugins/importMetaGlob.ts | 33 ++++++++--------- .../vite/src/node/plugins/loadFallback.ts | 1 - packages/vite/src/node/plugins/manifest.ts | 1 - packages/vite/src/node/plugins/resolve.ts | 8 ++-- packages/vite/src/node/plugins/worker.ts | 2 +- 16 files changed, 39 insertions(+), 99 deletions(-) diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 0742a90da7c6bf..f21e1ced1069e6 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -611,7 +611,7 @@ export async function buildEnvironment( // inject environment and ssr arg to plugin load/transform hooks // TODO: rework lib mode const plugins = (libOptions ? config : environment).plugins.map((p) => - injectEnvironmentToHooks(p, environment), + injectEnvironmentToHooks(environment, p), ) const rollupOptions: RollupOptions = { @@ -1119,12 +1119,10 @@ function isExternal(id: string, test: string | RegExp) { } } -// TODO: -// - Could we get Rollup to let us extends PluginContext in a more performant way? -// - Extend for all hooks? +// TODO: Could we get Rollup to let us extends PluginContext in a more performant way? export function injectEnvironmentToHooks( + environment: BuildEnvironment, plugin: Plugin, - environment?: BuildEnvironment, ): Plugin { const { resolveId, load, transform } = plugin @@ -1133,17 +1131,17 @@ export function injectEnvironmentToHooks( for (const hook of Object.keys(clone) as RollupPluginHooks[]) { switch (hook) { case 'resolveId': - clone[hook] = wrapEnvironmentResolveId(resolveId, environment) + clone[hook] = wrapEnvironmentResolveId(environment, resolveId) break case 'load': - clone[hook] = wrapEnvironmentLoad(load, environment) + clone[hook] = wrapEnvironmentLoad(environment, load) break case 'transform': - clone[hook] = wrapEnvironmentTransform(transform, environment) + clone[hook] = wrapEnvironmentTransform(environment, transform) break default: if (ROLLUP_HOOKS.includes(hook)) { - ;(clone as any)[hook] = wrapEnvironmentHook(clone[hook], environment) + ;(clone as any)[hook] = wrapEnvironmentHook(environment, clone[hook]) } break } @@ -1153,8 +1151,8 @@ export function injectEnvironmentToHooks( } function wrapEnvironmentResolveId( + environment: BuildEnvironment, hook?: Plugin['resolveId'], - environment?: BuildEnvironment, ): Plugin['resolveId'] { if (!hook) return @@ -1179,8 +1177,8 @@ function wrapEnvironmentResolveId( } function wrapEnvironmentLoad( + environment: BuildEnvironment, hook?: Plugin['load'], - environment?: BuildEnvironment, ): Plugin['load'] { if (!hook) return @@ -1204,8 +1202,8 @@ function wrapEnvironmentLoad( } function wrapEnvironmentTransform( + environment: BuildEnvironment, hook?: Plugin['transform'], - environment?: BuildEnvironment, ): Plugin['transform'] { if (!hook) return @@ -1230,8 +1228,8 @@ function wrapEnvironmentTransform( } function wrapEnvironmentHook( + environment: BuildEnvironment, hook?: Plugin[HookName], - environment?: BuildEnvironment, ): Plugin[HookName] { if (!hook) return @@ -1257,7 +1255,7 @@ function wrapEnvironmentHook( function injectEnvironmentInContext( context: Context, - environment?: BuildEnvironment, + environment: BuildEnvironment, ) { context.environment ??= environment return context diff --git a/packages/vite/src/node/environment.ts b/packages/vite/src/node/environment.ts index 30a5295bc6495b..638604f8e992b6 100644 --- a/packages/vite/src/node/environment.ts +++ b/packages/vite/src/node/environment.ts @@ -21,13 +21,6 @@ export function usePerEnvironmentState( const stateMap = new WeakMap() return function (context: PluginContext) { const { environment } = context - if (!environment) { - context.error( - new Error( - `Per environment state called with undefined environment. You may be using a Vite v6+ plugin in Vite v5 or Rollup.`, - ), - ) - } let state = stateMap.get(environment) if (!state) { state = initial(environment) diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index 3a07408e1796dd..3d0a753e587ec1 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -8,7 +8,6 @@ export { resolveConfig, sortUserPlugins, } from './config' -export { defineVitePlugin } from './plugin' export { createServer } from './server' export { preview } from './preview' export { build, createBuilder } from './build' @@ -52,12 +51,7 @@ export type { DevEnvironmentOptions, ResolvedDevEnvironmentOptions, } from './config' -export type { - Plugin, - PluginOption, - PluginWithEnvironment, - HookHandler, -} from './plugin' +export type { Plugin, PluginOption, HookHandler } from './plugin' export type { Environment } from './environment' export type { FilterPattern } from './utils' export type { CorsOptions, CorsOrigin, CommonServerOptions } from './http' diff --git a/packages/vite/src/node/plugin.ts b/packages/vite/src/node/plugin.ts index 1cb18873a8909e..97b7555b4a274c 100644 --- a/packages/vite/src/node/plugin.ts +++ b/packages/vite/src/node/plugin.ts @@ -55,12 +55,8 @@ import type { Environment } from './environment' export interface PluginContextExtension { /** * Vite-specific environment instance - * - * Will always be defined in Vite 6+. Keep the type optional for backward compatible. - * - * @see TODO:link-to-docs */ - environment?: Environment + environment: Environment } export interface PluginContext @@ -329,34 +325,3 @@ export function resolveEnvironmentPlugins(environment: Environment): Plugin[] { !plugin.applyToEnvironment || plugin.applyToEnvironment(environment), ) } - -/** - * Internal type to make sure `this.environment` is always defined - * - * Should be used via `defineVitePlugin` - */ -export type PluginWithEnvironment = { - [K in keyof Plugin]: Plugin[K] extends ObjectHook - ? ObjectHook> - : HandlerWithEnvironment -} - -export type HandlerWithEnvironment = H extends ( - this: infer This, - ...args: infer Args -) => infer Returns - ? This extends { environment?: Environment } - ? (this: This & { environment: Environment }, ...args: Args) => Returns - : H - : H - -/** - * Type utility to define a Vite-specific plugin where `this.environment` is always defined. - * - * Should only be used when you expect the plugin **only** runs on Vite 6+. - * - * It does an internal cast to returns a Rollup-compatible plugin type so you can pass into the array as before. - */ -export function defineVitePlugin(plugin: PluginWithEnvironment): Plugin { - return plugin as Plugin -} diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index 05862d3e05f967..d0c5d8dffe1361 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -227,8 +227,6 @@ export function assetPlugin(config: ResolvedConfig): Plugin { }, generateBundle(_, bundle) { - const environment = this.environment! - // Remove empty entry point file for (const file in bundle) { const chunk = bundle[file] @@ -243,7 +241,10 @@ export function assetPlugin(config: ResolvedConfig): Plugin { } // do not emit assets for SSR build - if (config.command === 'build' && !environment.options.build.emitAssets) { + if ( + config.command === 'build' && + !this.environment.options.build.emitAssets + ) { for (const file in bundle) { if ( bundle[file].type === 'asset' && diff --git a/packages/vite/src/node/plugins/clientInjections.ts b/packages/vite/src/node/plugins/clientInjections.ts index f624b4091608f1..1572c58fa065df 100644 --- a/packages/vite/src/node/plugins/clientInjections.ts +++ b/packages/vite/src/node/plugins/clientInjections.ts @@ -90,7 +90,6 @@ export function clientInjectionsPlugin(config: ResolvedConfig): Plugin { } }, async transform(code, id, options) { - if (!this.environment) return // TODO: !environment.options.nodeCompatible ? // TODO: Remove options?.ssr, Vitest currently hijacks this plugin const ssr = options?.ssr ?? this.environment.name !== 'client' diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 8664ac35c76088..be08fc2cf99d91 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -556,7 +556,6 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { }, async renderChunk(code, chunk, opts) { - if (!this.environment) return const generatedAssets = generatedAssetsMap.get(this.environment)! let chunkCSS = '' diff --git a/packages/vite/src/node/plugins/define.ts b/packages/vite/src/node/plugins/define.ts index 284a7253abd2bc..983e6dfb4fa241 100644 --- a/packages/vite/src/node/plugins/define.ts +++ b/packages/vite/src/node/plugins/define.ts @@ -119,8 +119,6 @@ export function definePlugin(config: ResolvedConfig): Plugin { name: 'vite:define', async transform(code, id) { - if (!this.environment) return - if (this.environment.name === 'client' && !isBuild) { // for dev we inject actual global defines in the vite client to // avoid the transform cost. see the `clientInjection` and diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index 46088a802eed48..8326cde9ee0e31 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -328,7 +328,6 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { async transform(html, id) { if (id.endsWith('.html')) { - if (!this.environment) return const { modulePreload } = this.environment.options.build id = normalizePath(id) @@ -691,7 +690,6 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { }, async generateBundle(options, bundle) { - if (!this.environment) return const { modulePreload } = this.environment.options.build const analyzedChunk: Map = new Map() diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index 19ee2d352252b7..a3f0c3538548db 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -205,7 +205,6 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { name: 'vite:import-analysis', async transform(source, importer) { - if (!this.environment) return const environment = this.environment as DevEnvironment const ssr = environment.name !== 'client' // TODO const moduleGraph = environment.moduleGraph diff --git a/packages/vite/src/node/plugins/importAnalysisBuild.ts b/packages/vite/src/node/plugins/importAnalysisBuild.ts index db6877cb705490..6c21836767e91d 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -340,7 +340,6 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { }, generateBundle({ format }, bundle) { - if (!this.environment) return const ssr = this.environment.name !== 'client' // TODO if (format !== 'es' || ssr || isWorker) { return diff --git a/packages/vite/src/node/plugins/importMetaGlob.ts b/packages/vite/src/node/plugins/importMetaGlob.ts index 85030d714e2e4f..ae2bc694d7da15 100644 --- a/packages/vite/src/node/plugins/importMetaGlob.ts +++ b/packages/vite/src/node/plugins/importMetaGlob.ts @@ -67,24 +67,23 @@ export function importGlobPlugin(config: ResolvedConfig): Plugin { config.logger, ) if (result) { - if (this.environment) { - const allGlobs = result.matches.map((i) => i.globsResolved) - if (!importGlobMaps.has(this.environment)) { - importGlobMaps.set(this.environment, new Map()) - } - importGlobMaps.get(this.environment)!.set( - id, - allGlobs.map((globs) => { - const affirmed: string[] = [] - const negated: string[] = [] - - for (const glob of globs) { - ;(glob[0] === '!' ? negated : affirmed).push(glob) - } - return { affirmed, negated } - }), - ) + const allGlobs = result.matches.map((i) => i.globsResolved) + if (!importGlobMaps.has(this.environment)) { + importGlobMaps.set(this.environment, new Map()) } + importGlobMaps.get(this.environment)!.set( + id, + allGlobs.map((globs) => { + const affirmed: string[] = [] + const negated: string[] = [] + + for (const glob of globs) { + ;(glob[0] === '!' ? negated : affirmed).push(glob) + } + return { affirmed, negated } + }), + ) + return transformStableResult(result.s, id, config) } }, diff --git a/packages/vite/src/node/plugins/loadFallback.ts b/packages/vite/src/node/plugins/loadFallback.ts index 761ef1846edadd..34ae8a0bce0eaf 100644 --- a/packages/vite/src/node/plugins/loadFallback.ts +++ b/packages/vite/src/node/plugins/loadFallback.ts @@ -23,7 +23,6 @@ export function loadFallbackPlugin(config: ResolvedConfig): Plugin { return { name: 'vite:load-fallback', async load(id, options) { - if (!this.environment) return const environment = this.environment as DevEnvironment let code: string | null = null diff --git a/packages/vite/src/node/plugins/manifest.ts b/packages/vite/src/node/plugins/manifest.ts index 71227d2d64de59..0cf36c5060eff4 100644 --- a/packages/vite/src/node/plugins/manifest.ts +++ b/packages/vite/src/node/plugins/manifest.ts @@ -39,7 +39,6 @@ export function manifestPlugin(): Plugin { }, generateBundle({ format }, bundle) { - if (!this.environment) return const { root } = this.environment.config const buildOptions = this.environment.options.build diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index 1c6edfb7e08a35..edd8bc8d8e16bc 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -217,7 +217,7 @@ export function resolvePlugin( // 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' + resolveOptions.optimizeDeps && this.environment.mode === 'dev' ? this.environment?.depsOptimizer : undefined @@ -229,7 +229,7 @@ export function resolvePlugin( const isRequire: boolean = resolveOpts?.custom?.['node-resolve']?.isRequire ?? false - const environmentName = this.environment?.name ?? (ssr ? 'ssr' : 'client') + const environmentName = this.environment.name ?? (ssr ? 'ssr' : 'client') const currentEnvironmentOptions = this.environment?.options || environmentsOptions?.[environmentName] const environmentResolveOptions = currentEnvironmentOptions?.resolve @@ -247,7 +247,7 @@ export function resolvePlugin( scan: resolveOpts?.scan ?? resolveOptions.scan, } - const depsOptimizerOptions = this.environment?.options.dev.optimizeDeps + const depsOptimizerOptions = this.environment.options.dev.optimizeDeps const resolvedImports = resolveSubpathImports(id, importer, options) if (resolvedImports) { @@ -409,7 +409,7 @@ export function resolvePlugin( if (bareImportRE.test(id)) { const external = options.externalize && - shouldExternalize(this.environment!, id, importer) // TODO + shouldExternalize(this.environment, id, importer) if ( !external && asSrc && diff --git a/packages/vite/src/node/plugins/worker.ts b/packages/vite/src/node/plugins/worker.ts index 0d38b559ad820e..0daeda88fd809a 100644 --- a/packages/vite/src/node/plugins/worker.ts +++ b/packages/vite/src/node/plugins/worker.ts @@ -76,7 +76,7 @@ async function bundleWorkerEntry( ...rollupOptions, input, plugins: resolvedPlugins.map((p) => - injectEnvironmentToHooks(p, workerEnvironment), + injectEnvironmentToHooks(workerEnvironment, p), ), onwarn(warning, warn) { onRollupWarning(warning, warn, config) From 611ce5c6779bd44c4dc31438d8c4714c56cb4649 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 24 May 2024 18:43:42 +0200 Subject: [PATCH 097/123] fix: check lastHMRTimestamp for entries in moduleRunner --- packages/vite/src/module-runner/runner.ts | 12 +++++++++--- packages/vite/src/module-runner/types.ts | 6 ++++++ packages/vite/src/module-runner/utils.ts | 4 ++-- packages/vite/src/node/ssr/fetchModule.ts | 6 +++++- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/vite/src/module-runner/runner.ts b/packages/vite/src/module-runner/runner.ts index 4dbec334d06341..3149263632039f 100644 --- a/packages/vite/src/module-runner/runner.ts +++ b/packages/vite/src/module-runner/runner.ts @@ -102,7 +102,7 @@ export class ModuleRunner { */ public async import(url: string): Promise { url = this.normalizeEntryUrl(url) - const fetchedModule = await this.cachedModule(url) + const fetchedModule = await this.cachedModule(url, undefined, false) return await this.cachedRequest(url, fetchedModule) } @@ -233,12 +233,14 @@ export class ModuleRunner { private async cachedModule( url: string, importer?: string, + // the entry point should check if time is different + cache = true, ): Promise { if (this.destroyed) { throw new Error(`Vite module runner has been destroyed.`) } const normalized = this.urlToIdMap.get(url) - if (normalized) { + if (cache && normalized) { const mod = this.moduleCache.getByModuleId(normalized) if (mod.meta) { return mod @@ -253,10 +255,14 @@ export class ModuleRunner { : await this.transport.fetchModule(url, importer) ) as ResolvedResult + const lastHMRTimestamp = + 'lastHMRTimestamp' in fetchedModule + ? fetchedModule.lastHMRTimestamp || 0 + : 0 // 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, then a module will be duplicated - const { query, timestamp } = parseUrl(url) + const { query, timestamp = lastHMRTimestamp } = parseUrl(url) const file = 'file' in fetchedModule ? fetchedModule.file : undefined const fileId = file ? `${file}${query}` : url const moduleId = this.moduleCache.normalize(fileId) diff --git a/packages/vite/src/module-runner/types.ts b/packages/vite/src/module-runner/types.ts index 59c45a57afba3f..dff658047135d2 100644 --- a/packages/vite/src/module-runner/types.ts +++ b/packages/vite/src/module-runner/types.ts @@ -103,8 +103,14 @@ export interface ViteFetchResult { /** * File path of the module on disk. * This will be resolved as import.meta.url/filename + * Will be equal to `null` for virtual modules */ file: string | null + /** + * Timestamp when HMR was triggered for this module + * Usually automatically comes with ?t=timestamp query + */ + lastHMRTimestamp?: number } export type ResolvedResult = (ExternalFetchResult | ViteFetchResult) & { diff --git a/packages/vite/src/module-runner/utils.ts b/packages/vite/src/module-runner/utils.ts index 60070fc22531a2..d6da58980f27b4 100644 --- a/packages/vite/src/module-runner/utils.ts +++ b/packages/vite/src/module-runner/utils.ts @@ -20,12 +20,12 @@ const timestampRegex = /[?&]t=(\d{13})(&?)/ interface ParsedPath { query: string - timestamp: number + timestamp: number | undefined } export function parseUrl(url: string): ParsedPath { const idQuery = url.split('?')[1] - let timestamp = 0 + let timestamp = undefined // 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 diff --git a/packages/vite/src/node/ssr/fetchModule.ts b/packages/vite/src/node/ssr/fetchModule.ts index bb27fd61aaf40a..ea076913864909 100644 --- a/packages/vite/src/node/ssr/fetchModule.ts +++ b/packages/vite/src/node/ssr/fetchModule.ts @@ -113,7 +113,11 @@ export async function fetchModule( if (result.code[0] === '#') result.code = result.code.replace(/^#!.*/, (s) => ' '.repeat(s.length)) - return { code: result.code, file: mod.file } + return { + code: result.code, + file: mod.file, + lastHMRTimestamp: mod.lastHMRTimestamp, + } } const OTHER_SOURCE_MAP_REGEXP = new RegExp( From d8aa74c9099f34fc27121dc468fc4fedb19a61ef Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Sat, 25 May 2024 12:31:56 +0200 Subject: [PATCH 098/123] docs: assign code for each deprecation changes, provide option to opt-in (#17305) --- docs/.vitepress/config.ts | 31 +++++ docs/deprecations/index.md | 5 + docs/deprecations/vd001.md | 23 ++++ docs/deprecations/vd002.md | 45 ++++++++ docs/deprecations/vd003.md | 26 +++++ docs/deprecations/vd004.md | 23 ++++ packages/vite/src/node/config.ts | 15 +++ packages/vite/src/node/deprecations.ts | 106 ++++++++++++++++++ packages/vite/src/node/server/hmr.ts | 16 ++- packages/vite/src/node/server/index.ts | 18 ++- .../vite/src/node/server/pluginContainer.ts | 17 ++- 11 files changed, 319 insertions(+), 6 deletions(-) create mode 100644 docs/deprecations/index.md create mode 100644 docs/deprecations/vd001.md create mode 100644 docs/deprecations/vd002.md create mode 100644 docs/deprecations/vd003.md create mode 100644 docs/deprecations/vd004.md create mode 100644 packages/vite/src/node/deprecations.ts diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index a58aed971e0fc0..44fa447831bf8d 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -264,6 +264,10 @@ export default defineConfig({ text: 'Migration from v4', link: '/guide/migration', }, + { + text: 'Deprecations Guide', + link: '/deprecations/', + }, ], }, { @@ -331,6 +335,33 @@ export default defineConfig({ ], }, ], + '/deprecations/': [ + { + text: 'Deprecations Guide', + link: '/deprecations/', + }, + { + text: 'Deprecations List', + items: [ + { + text: 'VD001 - handleHotUpdate()', + link: '/deprecations/vd001', + }, + { + text: 'VD002 - options.ssr', + link: '/deprecations/vd002', + }, + { + text: 'VD003 - Dev Server APIs', + link: '/deprecations/vd003', + }, + { + text: 'VD004 - ssrLoadModule', + link: '/deprecations/vd004', + }, + ], + }, + ], }, outline: { diff --git a/docs/deprecations/index.md b/docs/deprecations/index.md new file mode 100644 index 00000000000000..f9c1d8025f5c8d --- /dev/null +++ b/docs/deprecations/index.md @@ -0,0 +1,5 @@ +# Deprecations Guide + +A list of guides for planned future deprecations/removals in Vite. + +// TODO: diff --git a/docs/deprecations/vd001.md b/docs/deprecations/vd001.md new file mode 100644 index 00000000000000..e1f9929a3cefe9 --- /dev/null +++ b/docs/deprecations/vd001.md @@ -0,0 +1,23 @@ +# VD001 - Plugin Hook `handleHotUpdate` + +Deprecate plugin hook `handleHotUpdate` in favor of [`hotUpdate` hook](/guide/api-vite-environment#the-hotupdate-hook) to be Environment API aware, and handle additional watch events with `create` and `delete`. + +::: tip Future Deprecation +The deprecation is plannde in the future, where you could start migrating your plugin to use the new API if you move fast. To identify your usage, set `future.deprecationWarnings.pluginHookHandleHotUpdate` to `true` in your vite config. +::: + +Affect scope: `Vite Plugin Authors` + +| Stages | Version | +| ---------------- | ---------------------------------------- | +| First Introduced | `v6.0.0` | +| Deprecation | (planned in `v7.0.0`) | +| Feature Removal | (currently no plan to remove completely) | + +## Motivation + +// TODO: + +## Migration Guide + +// TODO: diff --git a/docs/deprecations/vd002.md b/docs/deprecations/vd002.md new file mode 100644 index 00000000000000..d721cae211e89f --- /dev/null +++ b/docs/deprecations/vd002.md @@ -0,0 +1,45 @@ +# VD002 - Plugin Hook Argument `options.ssr` + +Deprecate plugin hook argument `options.ssr` in `resolveId`, `load` and `transform` in favor of the `this.environment` plugin context property. + +::: tip Future Deprecation +The deprecation is plannde in the future, where you could start migrating your plugin to use the new API if you move fast. To identify your usage, set `future.deprecationWarnings.pluginHookSsrArgument` to `true` in your vite config. +::: + +Affect scope: `Vite Plugin Authors` + +| Stages | Version | +| ---------------- | ---------------------------------------- | +| First Introduced | `v6.0.0` | +| Deprecation | (planned in `v7.0.0`) | +| Feature Removal | (currently no plan to remove completely) | + +## Motivation + +// TODO: + +## Migration Guide + +For the existing plugin to do a quick migration, replace the `options.ssr` argument with `this.environment.name !== 'client'` in the `resolveId`, `load` and `transform` hooks: + +```ts +import { Plugin } from 'vite' + +export function myPlugin(): Plugin { + return { + name: 'my-plugin', + resolveId(id, importer, options) { + const isSSR = options.ssr // [!CODE --] + const isSSR = this.environment.name !== 'client' // [!CODE ++] + + if (isSSR) { + // SSR specific logic + } else { + // Client specific logic + } + }, + } +} +``` + +For a more robust long term implemtation, plugin should provide handling for [multiple environments](/guide/api-vite-environment.html#accessing-the-current-environment-in-hooks). diff --git a/docs/deprecations/vd003.md b/docs/deprecations/vd003.md new file mode 100644 index 00000000000000..54c91304face0f --- /dev/null +++ b/docs/deprecations/vd003.md @@ -0,0 +1,26 @@ +# VD003 - Dev Server APIs + +Multiple APIs from ViteDevServer related to module graph has replaced with more isolated Environment APIs. + +- `server.moduleGraph` -> [`environment.moduleGraph`](/guide/api-vite-environment#separate-module-graphs) +- `server.transformRequest` -> `environment.transformRequest` + +::: tip Future Deprecation +The deprecation is plannde in the future, where you could start migrating your plugin to use the new API if you move fast. To identify your usage, set `future.deprecationWarnings` in your vite config. +::: + +Affect scope: `Vite Plugin Authors` + +| Stages | Version | +| ---------------- | ---------------------------------------- | +| First Introduced | `v6.0.0` | +| Deprecation | (planned in `v7.0.0`) | +| Feature Removal | (currently no plan to remove completely) | + +## Motivation + +// TODO: + +## Migration Guide + +// TODO: diff --git a/docs/deprecations/vd004.md b/docs/deprecations/vd004.md new file mode 100644 index 00000000000000..5247c5fb93cfe0 --- /dev/null +++ b/docs/deprecations/vd004.md @@ -0,0 +1,23 @@ +# VD004 - `ssrLoadModule` + +`server.ssrLoadModule` has been replaced new [Module Runner](/guide/api-vite-environment#modulerunner). + +::: tip Future Deprecation +The deprecation is plannde in the future, where you could start migrating your plugin to use the new API if you move fast. To identify your usage, set `future.deprecationWarnings.ssrLoadModule` to `true` in your vite config. +::: + +Affect scope: `Vite Plugin Authors` + +| Stages | Version | +| ---------------- | ---------------------------------------- | +| First Introduced | `v6.0.0` | +| Deprecation | (planned in `v7.0.0`) | +| Feature Removal | (currently no plan to remove completely) | + +## Motivation + +// TODO: + +## Migration Guide + +// TODO: diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 8bd49eef80b130..aab5ba1dcbe8fd 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -88,6 +88,7 @@ import { loadEnv, resolveEnvPrefix } from './env' import type { ResolvedSSROptions, SSROptions } from './ssr' import { resolveSSROptions } from './ssr' import { FutureCompatEnvironment } from './baseEnvironment' +import type { FutureDeprecationWarningsOptions } from './deprecations' const debug = createDebugger('vite:config') const promisifiedRealpath = promisify(fs.realpath) @@ -357,6 +358,10 @@ export interface UserConfig extends DefaultEnvironmentOptions { * @experimental */ experimental?: ExperimentalOptions + /** + * Options to opt-in to future behavior + */ + future?: FutureOptions /** * Legacy options * @@ -443,6 +448,15 @@ export interface HTMLOptions { cspNonce?: string } +export interface FutureOptions { + /** + * Emit warning messages for deprecated/will-deprecated features at runtime. + * + * Setting to `true` to enable all warnings + */ + deprecationWarnings?: boolean | FutureDeprecationWarningsOptions +} + export interface ExperimentalOptions { /** * Append fake `&lang.(ext)` when queries are specified, to preserve the file extension for following plugins to process. @@ -1186,6 +1200,7 @@ export async function resolveConfig( hmrPartialAccept: false, ...config.experimental, }, + future: config.future, // Backward compatibility, users should use environment.config.dev.optimizeDeps optimizeDeps: backwardCompatibleOptimizeDeps, diff --git a/packages/vite/src/node/deprecations.ts b/packages/vite/src/node/deprecations.ts new file mode 100644 index 00000000000000..c1062c833e70f6 --- /dev/null +++ b/packages/vite/src/node/deprecations.ts @@ -0,0 +1,106 @@ +import colors from 'picocolors' +import type { ResolvedConfig } from './config' + +// TODO: switch to production docs URL +const docsURL = 'https://deploy-preview-16471--vite-docs-main.netlify.app' + +export interface FutureDeprecationWarningsOptions { + pluginHookHandleHotUpdate?: boolean + pluginHookSsrArgument?: boolean + + serverModuleGraph?: boolean + serverHot?: boolean + serverTransformRequest?: boolean + + ssrLoadModule?: boolean +} + +const _futureDeprecationCode = { + pluginHookHandleHotUpdate: 'VD001', + pluginHookSsrArgument: 'VD002', + + serverModuleGraph: 'VD003', + serverHot: 'VD003', + serverTransformRequest: 'VD003', + + ssrLoadModule: 'VD004', +} satisfies Record + +const _futureDeprecationMessages = { + pluginHookHandleHotUpdate: + 'Plugin hook `handleHotUpdate()` is replaced with `hotUpdate()`.', + pluginHookSsrArgument: + 'Plugin hook `options.ssr` is replaced with `this.environment.name !== "client"`.', + + serverModuleGraph: + 'The `server.moduleGraph` is replaced with `this.environment.moduleGraph`.', + serverHot: 'The `server.hot` is replaced with `this.environment.hot`.', + serverTransformRequest: + 'The `server.transformRequest` is replaced with `this.environment.transformRequest`.', + + ssrLoadModule: + 'The `server.ssrLoadModule` is replaced with Environment Runner.', +} satisfies Record + +let _ignoreDeprecationWarnings = false + +/** + * Warn about future deprecations. + */ +export function warnFutureDeprecation( + config: ResolvedConfig, + type: keyof FutureDeprecationWarningsOptions, + extraMessage?: string, + stacktrace = true, +): void { + if (_ignoreDeprecationWarnings) return + + if (!config.future?.deprecationWarnings) return + + if ( + config.future.deprecationWarnings !== true && + !config.future.deprecationWarnings[type] + ) + return + + let msg = `[vite future] [${_futureDeprecationCode[type]}] ${_futureDeprecationMessages[type]}` + if (extraMessage) { + msg += ` ${extraMessage}` + } + msg = colors.yellow(msg) + + const docs = `${docsURL}/deprecations/${_futureDeprecationCode[type].toLowerCase()}` + msg += + colors.gray(`\n ${stacktrace ? '├' : '└'}─── `) + + colors.underline(docs) + + '\n' + + if (stacktrace) { + const stack = new Error().stack + if (stack) { + const stacks = stack + .split('\n') + .slice(3) + .filter((i) => !i.includes('/node_modules/vite/dist/')) + if (stacks.length === 0) stacks.push('No stack trace found.') + msg += + colors.dim( + stacks + .map( + (i, idx) => + ` ${idx === stacks.length - 1 ? '└' : '│'} ${i.trim()}`, + ) + .join('\n'), + ) + '\n' + } + } + config.logger.warnOnce(msg) +} + +export function ignoreDeprecationWarnings(fn: () => T): T { + const before = _ignoreDeprecationWarnings + _ignoreDeprecationWarnings = true + const ret = fn() + _ignoreDeprecationWarnings = before + return ret +} diff --git a/packages/vite/src/node/server/hmr.ts b/packages/vite/src/node/server/hmr.ts index 7f2fd70b7ad769..44304695f02888 100644 --- a/packages/vite/src/node/server/hmr.ts +++ b/packages/vite/src/node/server/hmr.ts @@ -15,6 +15,10 @@ import { getEnvFilesForMode } from '../env' import type { Environment } from '../environment' import { withTrailingSlash, wrapId } from '../../shared/utils' import type { Plugin } from '../plugin' +import { + ignoreDeprecationWarnings, + warnFutureDeprecation, +} from '../deprecations' import type { EnvironmentModuleNode } from './moduleGraph' import type { ModuleNode } from './mixedModuleGraph' import type { DevEnvironment } from './environment' @@ -163,6 +167,8 @@ export async function handleHMRUpdate( server: ViteDevServer, ): Promise { const { config } = server + const mixedModuleGraph = ignoreDeprecationWarnings(() => server.moduleGraph) + const environments = Object.values(server.environments) const shortFile = getShortName(file, config.root) @@ -234,7 +240,7 @@ export async function handleHMRUpdate( hotMap.set(environment, { context }) } - const mixedMods = new Set(server.moduleGraph.getModulesByFile(file)) + const mixedMods = new Set(mixedModuleGraph.getModulesByFile(file)) const mixedHmrContext: HmrContext = { ...contextMeta, @@ -270,11 +276,17 @@ export async function handleHMRUpdate( ), ) .map((mod) => - server.moduleGraph.getBackwardCompatibleModuleNode(mod), + mixedModuleGraph.getBackwardCompatibleModuleNode(mod), ), ) } } else if (type === 'update') { + warnFutureDeprecation( + config, + 'pluginHookHandleHotUpdate', + `Used in plugin "${plugin.name}".`, + false, + ) // later on, we'll need: if (runtime === 'client') // Backward compatibility with mixed client and ssr moduleGraph const filteredModules = await getHookHandler(plugin.handleHotUpdate!)( diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 5b39f884db1f7a..76241e5be08f10 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -45,6 +45,7 @@ import type { BindCLIShortcutsOptions } from '../shortcuts' import { CLIENT_DIR, DEFAULT_DEV_PORT } from '../constants' import type { Logger } from '../logger' import { printServerUrls } from '../logger' +import { warnFutureDeprecation } from '../deprecations' import { createNoopWatcher, getResolvedOutDirs, @@ -505,7 +506,7 @@ export async function _createServer( // Backward compatibility - const moduleGraph = new ModuleGraph({ + let moduleGraph = new ModuleGraph({ client: () => environments.client.moduleGraph, ssr: () => environments.ssr.moduleGraph, }) @@ -526,7 +527,13 @@ export async function _createServer( environments, pluginContainer, - moduleGraph, + get moduleGraph() { + warnFutureDeprecation(config, 'serverModuleGraph') + return moduleGraph + }, + set moduleGraph(graph) { + moduleGraph = graph + }, resolvedUrls: null, // will be set on listen ssrTransform( @@ -543,6 +550,11 @@ export async function _createServer( // 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) { + warnFutureDeprecation( + config, + 'serverTransformRequest', + 'server.transformRequest() is deprecated. Use environment.transformRequest() instead.', + ) const environment = server.environments[options?.ssr ? 'ssr' : 'client'] return transformRequest(environment, url, options) }, @@ -569,6 +581,8 @@ export async function _createServer( return devHtmlTransformFn(server, url, html, originalUrl) }, async ssrLoadModule(url, opts?: { fixStacktrace?: boolean }) { + warnFutureDeprecation(config, 'ssrLoadModule') + return ssrLoadModule( url, server, diff --git a/packages/vite/src/node/server/pluginContainer.ts b/packages/vite/src/node/server/pluginContainer.ts index 07557b9b102c6e..23386d23273ab3 100644 --- a/packages/vite/src/node/server/pluginContainer.ts +++ b/packages/vite/src/node/server/pluginContainer.ts @@ -79,6 +79,7 @@ import { FS_PREFIX } from '../constants' import { createPluginHookUtils, getHookHandler } from '../plugins' import { cleanUrl, unwrapId } from '../../shared/utils' import type { Environment } from '../environment' +import { warnFutureDeprecation } from '../deprecations' import type { DevEnvironment } from './environment' import { buildErrorMessage } from './middlewares/error' import type { EnvironmentModuleNode } from './moduleGraph' @@ -736,7 +737,13 @@ export async function createEnvironmentPluginContainer( }, async load(id, options) { - options = options ? { ...options, ssr } : { ssr } + options = { + ...options, + get ssr() { + warnFutureDeprecation(config, 'pluginHookSsrArgument') + return ssr + }, + } const ctx = new Context() for (const plugin of getSortedPlugins('load')) { if (closed && environment?.options.dev.recoverable) @@ -760,7 +767,13 @@ export async function createEnvironmentPluginContainer( }, async transform(code, id, options) { - options = options ? { ...options, ssr } : { ssr } + options = { + ...options, + get ssr() { + warnFutureDeprecation(config, 'pluginHookSsrArgument') + return ssr + }, + } const inMap = options?.inMap const ctx = new TransformContext(id, code, inMap as SourceMap) for (const plugin of getSortedPlugins('transform')) { From ca940e8a0c4312dbf0938075ba55bebbf31510bd Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Sat, 25 May 2024 12:37:56 +0200 Subject: [PATCH 099/123] chore: refactor code style --- packages/vite/src/node/deprecations.ts | 28 ++++++++++++-------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/packages/vite/src/node/deprecations.ts b/packages/vite/src/node/deprecations.ts index c1062c833e70f6..43d2e59a59ea90 100644 --- a/packages/vite/src/node/deprecations.ts +++ b/packages/vite/src/node/deprecations.ts @@ -15,7 +15,7 @@ export interface FutureDeprecationWarningsOptions { ssrLoadModule?: boolean } -const _futureDeprecationCode = { +const deprecationCode = { pluginHookHandleHotUpdate: 'VD001', pluginHookSsrArgument: 'VD002', @@ -26,7 +26,7 @@ const _futureDeprecationCode = { ssrLoadModule: 'VD004', } satisfies Record -const _futureDeprecationMessages = { +const deprecationMessages = { pluginHookHandleHotUpdate: 'Plugin hook `handleHotUpdate()` is replaced with `hotUpdate()`.', pluginHookSsrArgument: @@ -44,6 +44,7 @@ const _futureDeprecationMessages = { let _ignoreDeprecationWarnings = false +// Later we could have a `warnDeprecation` utils when the deprecation is landed /** * Warn about future deprecations. */ @@ -63,13 +64,13 @@ export function warnFutureDeprecation( ) return - let msg = `[vite future] [${_futureDeprecationCode[type]}] ${_futureDeprecationMessages[type]}` + let msg = `[vite future] [${deprecationCode[type]}] ${deprecationMessages[type]}` if (extraMessage) { msg += ` ${extraMessage}` } msg = colors.yellow(msg) - const docs = `${docsURL}/deprecations/${_futureDeprecationCode[type].toLowerCase()}` + const docs = `${docsURL}/deprecations/${deprecationCode[type].toLowerCase()}` msg += colors.gray(`\n ${stacktrace ? '├' : '└'}─── `) + colors.underline(docs) + @@ -78,20 +79,17 @@ export function warnFutureDeprecation( if (stacktrace) { const stack = new Error().stack if (stack) { - const stacks = stack + let stacks = stack .split('\n') .slice(3) .filter((i) => !i.includes('/node_modules/vite/dist/')) - if (stacks.length === 0) stacks.push('No stack trace found.') - msg += - colors.dim( - stacks - .map( - (i, idx) => - ` ${idx === stacks.length - 1 ? '└' : '│'} ${i.trim()}`, - ) - .join('\n'), - ) + '\n' + if (stacks.length === 0) { + stacks.push('No stack trace found.') + } + stacks = stacks.map( + (i, idx) => ` ${idx === stacks.length - 1 ? '└' : '│'} ${i.trim()}`, + ) + msg += colors.dim(stacks.join('\n')) + '\n' } } config.logger.warnOnce(msg) From 1af7081cf9124c271b10ff2ae7a0420bd42a66d4 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Sat, 25 May 2024 13:08:16 +0200 Subject: [PATCH 100/123] chore: ignore test-dts in tsconfig --- packages/vite/src/module-runner/tsconfig.json | 2 +- packages/vite/src/shared/tsconfig.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/module-runner/tsconfig.json b/packages/vite/src/module-runner/tsconfig.json index b664c0ea7a093f..40840789f59fc8 100644 --- a/packages/vite/src/module-runner/tsconfig.json +++ b/packages/vite/src/module-runner/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.base.json", "include": ["./", "../node", "../dep-types", "../types"], - "exclude": ["**/__tests__"], + "exclude": ["**/__tests__", "**/__tests_dts__"], "compilerOptions": { "lib": ["ESNext", "DOM"], "stripInternal": true diff --git a/packages/vite/src/shared/tsconfig.json b/packages/vite/src/shared/tsconfig.json index a7f7890f1d0e7b..96451a95759b42 100644 --- a/packages/vite/src/shared/tsconfig.json +++ b/packages/vite/src/shared/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.base.json", "include": ["./", "../dep-types", "../types"], - "exclude": ["**/__tests__"], + "exclude": ["**/__tests__", "**/__tests_dts__"], "compilerOptions": { "lib": ["ESNext", "DOM"], "stripInternal": true From 7d78761a04c3b12a19256e81cb5d89e15ede92eb Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Sat, 25 May 2024 21:29:50 +0900 Subject: [PATCH 101/123] test(v6): test ssrLoadModule and virtual module invalidation (#17313) --- .../node/ssr/__tests__/ssrLoadModule.spec.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/packages/vite/src/node/ssr/__tests__/ssrLoadModule.spec.ts b/packages/vite/src/node/ssr/__tests__/ssrLoadModule.spec.ts index b3f3c2364ef04b..f4e396ed9fe0e5 100644 --- a/packages/vite/src/node/ssr/__tests__/ssrLoadModule.spec.ts +++ b/packages/vite/src/node/ssr/__tests__/ssrLoadModule.spec.ts @@ -62,3 +62,45 @@ test('import.meta.filename/dirname returns same value with Node', async () => { expect(viteValue.dirname).toBe(path.dirname(filename)) expect(viteValue.filename).toBe(filename) }) + +test('virtual module invalidation', async () => { + const server = await createServer({ + configFile: false, + root, + logLevel: 'silent', + optimizeDeps: { + noDiscovery: true, + }, + plugins: [ + { + name: 'virtual-test', + resolveId(id) { + if (id === 'virtual:test') { + return '\0virtual:test' + } + }, + load(id) { + if (id === '\0virtual:test') { + return ` + globalThis.__virtual_test_state ??= 0; + globalThis.__virtual_test_state++; + export default globalThis.__virtual_test_state; + ` + } + }, + }, + ], + }) + await server.pluginContainer.buildStart({}) + + const mod1 = await server.ssrLoadModule('virtual:test') + expect(mod1.default).toEqual(1) + const mod2 = await server.ssrLoadModule('virtual:test') + expect(mod2.default).toEqual(1) + + const modNode = server.moduleGraph.getModuleById('\0virtual:test') + server.moduleGraph.invalidateModule(modNode!) + + const mod3 = await server.ssrLoadModule('virtual:test') + expect(mod3.default).toEqual(2) +}) From fd46a2a420b5a45e69bc93be4be15d30b07e75ae Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Sat, 25 May 2024 14:33:09 +0200 Subject: [PATCH 102/123] fix: correctly invalidate module in the module runner if it was invalidated on the server --- packages/vite/src/module-runner/runner.ts | 17 +++++++++++------ packages/vite/src/module-runner/types.ts | 5 ++--- packages/vite/src/node/ssr/fetchModule.ts | 2 +- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/vite/src/module-runner/runner.ts b/packages/vite/src/module-runner/runner.ts index 3149263632039f..30c7599d113f0f 100644 --- a/packages/vite/src/module-runner/runner.ts +++ b/packages/vite/src/module-runner/runner.ts @@ -255,22 +255,27 @@ export class ModuleRunner { : await this.transport.fetchModule(url, importer) ) as ResolvedResult - const lastHMRTimestamp = - 'lastHMRTimestamp' in fetchedModule - ? fetchedModule.lastHMRTimestamp || 0 - : 0 + const cached = 'cached' in fetchedModule ? fetchedModule.cached : null // 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, then a module will be duplicated - const { query, timestamp = lastHMRTimestamp } = parseUrl(url) + const { query, timestamp } = parseUrl(url) const file = 'file' in fetchedModule ? fetchedModule.file : undefined const fileId = file ? `${file}${query}` : url const moduleId = this.moduleCache.normalize(fileId) const mod = this.moduleCache.getByModuleId(moduleId) + if (cached !== null && !cached) { + this.moduleCache.invalidateModule(mod) + } // 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) { + else if ( + timestamp != null && + mod.timestamp != null && + timestamp > 0 && + mod.timestamp < timestamp + ) { this.moduleCache.invalidateModule(mod) } diff --git a/packages/vite/src/module-runner/types.ts b/packages/vite/src/module-runner/types.ts index dff658047135d2..2d35b36b6475fa 100644 --- a/packages/vite/src/module-runner/types.ts +++ b/packages/vite/src/module-runner/types.ts @@ -107,10 +107,9 @@ export interface ViteFetchResult { */ file: string | null /** - * Timestamp when HMR was triggered for this module - * Usually automatically comes with ?t=timestamp query + * Is module cached on the server */ - lastHMRTimestamp?: number + cached: boolean } export type ResolvedResult = (ExternalFetchResult | ViteFetchResult) & { diff --git a/packages/vite/src/node/ssr/fetchModule.ts b/packages/vite/src/node/ssr/fetchModule.ts index ea076913864909..11fc35cd81de1b 100644 --- a/packages/vite/src/node/ssr/fetchModule.ts +++ b/packages/vite/src/node/ssr/fetchModule.ts @@ -116,7 +116,7 @@ export async function fetchModule( return { code: result.code, file: mod.file, - lastHMRTimestamp: mod.lastHMRTimestamp, + cached: !!mod.transformResult, } } From e91e269a28403fd85d2c015045e19d86c601564a Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Sat, 25 May 2024 14:52:07 +0200 Subject: [PATCH 103/123] fix: check last invalidation time --- packages/vite/src/module-runner/runner.ts | 17 ++++++----------- packages/vite/src/module-runner/types.ts | 5 +++-- packages/vite/src/node/ssr/fetchModule.ts | 5 ++++- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/packages/vite/src/module-runner/runner.ts b/packages/vite/src/module-runner/runner.ts index 30c7599d113f0f..5091ed6d41dde5 100644 --- a/packages/vite/src/module-runner/runner.ts +++ b/packages/vite/src/module-runner/runner.ts @@ -255,27 +255,22 @@ export class ModuleRunner { : await this.transport.fetchModule(url, importer) ) as ResolvedResult - const cached = 'cached' in fetchedModule ? fetchedModule.cached : null + const invalidationTimestamp = + 'invalidationTimestamp' in fetchedModule + ? fetchedModule.invalidationTimestamp + : 0 // 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, then a module will be duplicated - const { query, timestamp } = parseUrl(url) + const { query, timestamp = invalidationTimestamp } = parseUrl(url) const file = 'file' in fetchedModule ? fetchedModule.file : undefined const fileId = file ? `${file}${query}` : url const moduleId = this.moduleCache.normalize(fileId) const mod = this.moduleCache.getByModuleId(moduleId) - if (cached !== null && !cached) { - this.moduleCache.invalidateModule(mod) - } // if URL has a ?t= query, it might've been invalidated due to HMR // checking if we should also invalidate the module - else if ( - timestamp != null && - mod.timestamp != null && - timestamp > 0 && - mod.timestamp < timestamp - ) { + if (mod.timestamp != null && timestamp > 0 && mod.timestamp < timestamp) { this.moduleCache.invalidateModule(mod) } diff --git a/packages/vite/src/module-runner/types.ts b/packages/vite/src/module-runner/types.ts index 2d35b36b6475fa..3b03e105c30daa 100644 --- a/packages/vite/src/module-runner/types.ts +++ b/packages/vite/src/module-runner/types.ts @@ -107,9 +107,10 @@ export interface ViteFetchResult { */ file: string | null /** - * Is module cached on the server + * Timestamp when HMR was triggered for this module + * Usually automatically comes with ?t=timestamp query */ - cached: boolean + invalidationTimestamp: number } export type ResolvedResult = (ExternalFetchResult | ViteFetchResult) & { diff --git a/packages/vite/src/node/ssr/fetchModule.ts b/packages/vite/src/node/ssr/fetchModule.ts index 11fc35cd81de1b..76458c30e4630f 100644 --- a/packages/vite/src/node/ssr/fetchModule.ts +++ b/packages/vite/src/node/ssr/fetchModule.ts @@ -116,7 +116,10 @@ export async function fetchModule( return { code: result.code, file: mod.file, - cached: !!mod.transformResult, + invalidationTimestamp: Math.max( + mod.lastHMRTimestamp, + mod.lastInvalidationTimestamp, + ), } } From da354ea3b8ba27c1fd1f3e5c72e15068534d78b5 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Sun, 26 May 2024 00:00:06 +0200 Subject: [PATCH 104/123] fix: default value of `depOptimization` --- packages/vite/src/node/config.ts | 4 ++- .../src/node/optimizer/esbuildDepPlugin.ts | 2 +- packages/vite/src/node/optimizer/index.ts | 27 ++++++++----------- packages/vite/src/node/plugins/index.ts | 6 ++--- packages/vite/src/node/server/environment.ts | 4 +-- 5 files changed, 20 insertions(+), 23 deletions(-) diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index aab5ba1dcbe8fd..fe229045da3035 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -591,6 +591,7 @@ export function resolveDevEnvironmentOptions( optimizeDeps: resolveDepOptimizationOptions( dev?.optimizeDeps, preserverSymlinks, + environmentName, ), createEnvironment: dev?.createEnvironment, recoverable: dev?.recoverable ?? environmentName === 'client', @@ -750,6 +751,7 @@ function resolveEnvironmentResolveOptions( function resolveDepOptimizationOptions( optimizeDeps: DepOptimizationOptions | undefined, preserveSymlinks: boolean, + environmentName: string | undefined, ): DepOptimizationOptions { optimizeDeps ??= {} return { @@ -757,7 +759,7 @@ function resolveDepOptimizationOptions( exclude: optimizeDeps.exclude ?? [], needsInterop: optimizeDeps.needsInterop ?? [], extensions: optimizeDeps.extensions ?? [], - noDiscovery: optimizeDeps.noDiscovery ?? false, + noDiscovery: optimizeDeps.noDiscovery ?? environmentName !== 'client', holdUntilCrawlEnd: optimizeDeps.holdUntilCrawlEnd ?? true, esbuildOptions: { preserveSymlinks, // TODO: ? diff --git a/packages/vite/src/node/optimizer/esbuildDepPlugin.ts b/packages/vite/src/node/optimizer/esbuildDepPlugin.ts index 768e14d5555927..023fa72661e0de 100644 --- a/packages/vite/src/node/optimizer/esbuildDepPlugin.ts +++ b/packages/vite/src/node/optimizer/esbuildDepPlugin.ts @@ -52,7 +52,7 @@ export function esbuildDepPlugin( qualified: Record, external: string[], ): Plugin { - const { config } = environment + const config = environment.config const { extensions } = environment.options.dev.optimizeDeps // remove optimizable extensions from `externalTypes` list diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 9a17f82971abd3..0763efee91c3e6 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -163,12 +163,13 @@ export type DepOptimizationOptions = DepOptimizationConfig & { force?: boolean } -export function isDepOptimizationEnabled( +export function isDepOptimizationDisabled( optimizeDeps: DepOptimizationOptions, ): boolean { return ( - !(optimizeDeps.disabled === true || optimizeDeps.disabled === 'dev') && - !(optimizeDeps.noDiscovery && !optimizeDeps.include?.length) + optimizeDeps.disabled === true || + optimizeDeps.disabled === 'dev' || + (!!optimizeDeps.noDiscovery && !optimizeDeps.include?.length) ) } @@ -290,7 +291,7 @@ export async function optimizeExplicitEnvironmentDeps( ): Promise { const cachedMetadata = await loadCachedDepOptimizationMetadata( environment, - environment.config.optimizeDeps?.force ?? false, // TODO: should force be per-environment? + environment.options.dev.optimizeDeps.force ?? false, // TODO: should force be per-environment? false, ) if (cachedMetadata) { @@ -468,11 +469,6 @@ export function runOptimizeDeps( } { const optimizerContext = { cancelled: false } - const config: ResolvedConfig = { - ...environment.config, - command: 'build', - } - const depsCacheDir = getDepsCacheDir(environment) const processingCacheDir = getProcessingDepsCacheDir(environment) @@ -610,7 +606,9 @@ export function runOptimizeDeps( const runResult = preparedRun.then(({ context, idToExports }) => { 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 || optimizerContext.cancelled) { @@ -724,11 +722,6 @@ async function prepareEsbuildOptimizerRun( context?: BuildContext idToExports: Record }> { - const config: ResolvedConfig = { - ...environment.config, - command: 'build', - } - // esbuild generates nested directory output with lowest common ancestor base // this is unpredictable and makes it difficult to analyze entry / output // mapping. So what we do here is: @@ -765,7 +758,9 @@ async function prepareEsbuildOptimizerRun( if (optimizerContext.cancelled) return { context: undefined, idToExports } const define = { - 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || config.mode), + 'process.env.NODE_ENV': JSON.stringify( + process.env.NODE_ENV || environment.config.mode, + ), } const platform = environment.options.webCompatible ? 'browser' : 'node' diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index 1bd609d0a75839..d5a7a72aad1992 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -1,7 +1,7 @@ import aliasPlugin, { type ResolverFunction } from '@rollup/plugin-alias' import type { ObjectHook } from 'rollup' import type { PluginHookUtils, ResolvedConfig } from '../config' -import { isDepOptimizationEnabled } from '../optimizer' +import { isDepOptimizationDisabled } from '../optimizer' import type { HookHandler, Plugin, PluginWithRequiredHook } from '../plugin' import { watchPackageDataPlugin } from '../packages' import { getFsUtils } from '../fsUtils' @@ -40,8 +40,8 @@ export async function resolvePlugins( const { modulePreload } = config.build const depOptimizationEnabled = !isBuild && - Object.values(config.environments).some((environment) => - isDepOptimizationEnabled(environment.dev.optimizeDeps), + Object.values(config.environments).some( + (environment) => !isDepOptimizationDisabled(environment.dev.optimizeDeps), ) return [ diff --git a/packages/vite/src/node/server/environment.ts b/packages/vite/src/node/server/environment.ts index f52b27b335b046..baf71bd11c6d96 100644 --- a/packages/vite/src/node/server/environment.ts +++ b/packages/vite/src/node/server/environment.ts @@ -13,7 +13,7 @@ import { mergeConfig, promiseWithResolvers } from '../utils' import type { FetchModuleOptions } from '../ssr/fetchModule' import { fetchModule } from '../ssr/fetchModule' import type { DepsOptimizer } from '../optimizer' -import { isDepOptimizationEnabled } from '../optimizer' +import { isDepOptimizationDisabled } from '../optimizer' import { createDepsOptimizer, createExplicitDepsOptimizer, @@ -139,7 +139,7 @@ export class DevEnvironment extends BaseEnvironment { const { optimizeDeps } = this.options.dev if (setup.depsOptimizer) { this.depsOptimizer = setup.depsOptimizer - } else if (!isDepOptimizationEnabled(optimizeDeps)) { + } else if (isDepOptimizationDisabled(optimizeDeps)) { this.depsOptimizer = undefined } else { // We only support auto-discovery for the client environment, for all other From 5b766477150dcf11fbb283ec9fffe1fcdc792aeb Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Sun, 26 May 2024 19:17:17 +0200 Subject: [PATCH 105/123] chore: typo --- docs/guide/api-vite-environment.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/guide/api-vite-environment.md b/docs/guide/api-vite-environment.md index 8c550587a02b34..2d43ed983e9b89 100644 --- a/docs/guide/api-vite-environment.md +++ b/docs/guide/api-vite-environment.md @@ -33,7 +33,7 @@ environment.transformRequest(url) console.log(server.environments.ssr.moduleGraph) ``` -Most of the time, the current `environment` instance will be available as part of the context of the code being run so the need to access them through `server.environments` should be rare. For example, inside plugin hooks the enviornment is exposed as part of the `PluginContext`, so it can be accessed using `this.environment`. +Most of the time, the current `environment` instance will be available as part of the context of the code being run so the need to access them through `server.environments` should be rare. For example, inside plugin hooks the environment is exposed as part of the `PluginContext`, so it can be accessed using `this.environment`. A dev environment is an instance of the `DevEnvironment` class: @@ -841,11 +841,13 @@ When `builder.entireApp` is `true` (or when calling `vite build --app`), `vite b ```js export default { builder: { - buildApp: asnyc (builder) => { + buildApp: async (builder) => { const environments = Object.values(builder.environments) - return Promise.all(environments.map(environment => builder.build(environment))) - } - } + return Promise.all( + environments.map((environment) => builder.build(environment)), + ) + }, + }, } ``` From 03bc5904380eb6a9cc61bb68f89298bb1bd3cabf Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 28 May 2024 12:19:28 +0200 Subject: [PATCH 106/123] fix: improve invalidation sync in the module runner --- packages/vite/src/module-runner/index.ts | 1 + packages/vite/src/module-runner/runner.ts | 58 +++++++++---------- packages/vite/src/module-runner/types.ts | 27 +++++---- packages/vite/src/module-runner/utils.ts | 27 --------- packages/vite/src/node/config.ts | 5 +- packages/vite/src/node/server/environment.ts | 13 ++++- packages/vite/src/node/ssr/fetchModule.ts | 15 +++-- .../node/ssr/runtime/serverModuleRunner.ts | 3 +- 8 files changed, 69 insertions(+), 80 deletions(-) diff --git a/packages/vite/src/module-runner/index.ts b/packages/vite/src/module-runner/index.ts index efcd72c340a623..836b261a8b8687 100644 --- a/packages/vite/src/module-runner/index.ts +++ b/packages/vite/src/module-runner/index.ts @@ -13,6 +13,7 @@ export type { ModuleCache, FetchResult, FetchFunction, + FetchFunctionOptions, ResolvedResult, SSRImportMetadata, ModuleRunnerHMRConnection, diff --git a/packages/vite/src/module-runner/runner.ts b/packages/vite/src/module-runner/runner.ts index 5091ed6d41dde5..8084924020a5a9 100644 --- a/packages/vite/src/module-runner/runner.ts +++ b/packages/vite/src/module-runner/runner.ts @@ -23,7 +23,6 @@ import type { SSRImportMetadata, } from './types' import { - parseUrl, posixDirname, posixPathToFileHref, posixResolve, @@ -80,14 +79,8 @@ export class ModuleRunner { ? silentConsole : options.hmr.logger || hmrLogger, options.hmr.connection, - ({ acceptedPath, explicitImportRequired, timestamp }) => { - const [acceptedPathWithoutQuery, query] = acceptedPath.split(`?`) - const url = - acceptedPathWithoutQuery + - `?${explicitImportRequired ? 'import&' : ''}t=${timestamp}${ - query ? `&${query}` : '' - }` - return this.import(url) + ({ acceptedPath }) => { + return this.import(acceptedPath) }, ) options.hmr.connection.onUpdate(createHMRHandler(this)) @@ -102,7 +95,7 @@ export class ModuleRunner { */ public async import(url: string): Promise { url = this.normalizeEntryUrl(url) - const fetchedModule = await this.cachedModule(url, undefined, false) + const fetchedModule = await this.cachedModule(url) return await this.cachedRequest(url, fetchedModule) } @@ -233,50 +226,51 @@ export class ModuleRunner { private async cachedModule( url: string, importer?: string, - // the entry point should check if time is different - cache = true, ): Promise { if (this.destroyed) { throw new Error(`Vite module runner has been destroyed.`) } + + this.debug?.('[module runner] fetching', url) + const normalized = this.urlToIdMap.get(url) - if (cache && normalized) { - const mod = this.moduleCache.getByModuleId(normalized) - if (mod.meta) { - return mod - } + let cachedModule = normalized && this.moduleCache.getByModuleId(normalized) + if (!cachedModule) { + cachedModule = this.moduleCache.getByModuleId(url) } - this.debug?.('[module runner] fetching', url) - // fast return for established externalized patterns - const fetchedModule = ( + const fetchedModule = // fast return for established externalized pattern + ( url.startsWith('data:') ? { externalize: url, type: 'builtin' } - : await this.transport.fetchModule(url, importer) + : await this.transport.fetchModule(url, importer, { + cached: !!(typeof cachedModule === 'object' && cachedModule.meta), + }) ) as ResolvedResult - const invalidationTimestamp = - 'invalidationTimestamp' in fetchedModule - ? fetchedModule.invalidationTimestamp - : 0 + if ('cache' in fetchedModule) { + if (!cachedModule || !cachedModule.meta) { + throw new Error( + `Module "${url}" was mistakenly invalidated during fetch phase.`, + ) + } + return cachedModule + } + // 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, then a module will be duplicated - const { query, timestamp = invalidationTimestamp } = parseUrl(url) + const idQuery = url.split('?')[1] + const query = idQuery ? `?${idQuery}` : '' const file = 'file' in fetchedModule ? fetchedModule.file : undefined const fileId = file ? `${file}${query}` : url const moduleId = this.moduleCache.normalize(fileId) const mod = this.moduleCache.getByModuleId(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) - } + this.moduleCache.invalidateModule(mod) fetchedModule.id = moduleId mod.meta = fetchedModule - mod.timestamp = timestamp if (file) { const fileModules = this.fileToIdMap.get(file) || [] diff --git a/packages/vite/src/module-runner/types.ts b/packages/vite/src/module-runner/types.ts index 3b03e105c30daa..0d4522c841e407 100644 --- a/packages/vite/src/module-runner/types.ts +++ b/packages/vite/src/module-runner/types.ts @@ -70,7 +70,6 @@ export interface ModuleCache { evaluated?: boolean map?: DecodedMap meta?: ResolvedResult - timestamp?: number /** * Module ids that imports this module */ @@ -78,7 +77,18 @@ export interface ModuleCache { imports?: Set } -export type FetchResult = ExternalFetchResult | ViteFetchResult +export type FetchResult = + | CachedFetchResult + | ExternalFetchResult + | ViteFetchResult + +export interface CachedFetchResult { + /** + * If module cached in the runner, we can just confirm + * it wasn't invalidated on the server side. + */ + cache: true +} export interface ExternalFetchResult { /** @@ -106,25 +116,22 @@ export interface ViteFetchResult { * Will be equal to `null` for virtual modules */ file: string | null - /** - * Timestamp when HMR was triggered for this module - * Usually automatically comes with ?t=timestamp query - */ - invalidationTimestamp: number } export type ResolvedResult = (ExternalFetchResult | ViteFetchResult) & { id: string } -/** - * @experimental - */ export type FetchFunction = ( id: string, importer?: string, + options?: FetchFunctionOptions, ) => Promise +export interface FetchFunctionOptions { + cached?: boolean +} + export interface ModuleRunnerHmr { /** * Configure how HMR communicates between the client and the server. diff --git a/packages/vite/src/module-runner/utils.ts b/packages/vite/src/module-runner/utils.ts index d6da58980f27b4..12e06a3ebb1882 100644 --- a/packages/vite/src/module-runner/utils.ts +++ b/packages/vite/src/module-runner/utils.ts @@ -16,33 +16,6 @@ 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 | undefined -} - -export function parseUrl(url: string): ParsedPath { - const idQuery = url.split('?')[1] - let timestamp = undefined - // 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/config.ts b/packages/vite/src/node/config.ts index fe229045da3035..5b90dede236a96 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -616,7 +616,8 @@ function resolveEnvironmentOptions( resolve, nodeCompatible: options.nodeCompatible ?? environmentName !== 'client', webCompatible: options.webCompatible ?? environmentName === 'client', - injectInvalidationTimestamp: options.injectInvalidationTimestamp ?? true, + injectInvalidationTimestamp: + options.injectInvalidationTimestamp ?? environmentName === 'client', dev: resolveDevEnvironmentOptions( options.dev, resolve.preserveSymlinks, @@ -649,7 +650,7 @@ export function getDefaultResolvedEnvironmentOptions( resolve: config.resolve, nodeCompatible: true, webCompatible: false, - injectInvalidationTimestamp: true, + injectInvalidationTimestamp: false, dev: config.dev, build: config.build, } diff --git a/packages/vite/src/node/server/environment.ts b/packages/vite/src/node/server/environment.ts index baf71bd11c6d96..c767a59254f049 100644 --- a/packages/vite/src/node/server/environment.ts +++ b/packages/vite/src/node/server/environment.ts @@ -1,4 +1,4 @@ -import type { FetchResult } from 'vite/module-runner' +import type { FetchFunctionOptions, FetchResult } from 'vite/module-runner' import type { FSWatcher } from 'dep-types/chokidar' import colors from 'picocolors' import { BaseEnvironment } from '../baseEnvironment' @@ -175,8 +175,15 @@ export class DevEnvironment extends BaseEnvironment { // TODO: move warmup here } - fetchModule(id: string, importer?: string): Promise { - return fetchModule(this, id, importer, this._ssrRunnerOptions) + fetchModule( + id: string, + importer?: string, + options?: FetchFunctionOptions, + ): Promise { + return fetchModule(this, id, importer, { + ...this._ssrRunnerOptions, + ...options, + }) } transformRequest(url: string): Promise { diff --git a/packages/vite/src/node/ssr/fetchModule.ts b/packages/vite/src/node/ssr/fetchModule.ts index 76458c30e4630f..3cfa278857c3f4 100644 --- a/packages/vite/src/node/ssr/fetchModule.ts +++ b/packages/vite/src/node/ssr/fetchModule.ts @@ -12,6 +12,7 @@ import { genSourceMapUrl } from '../server/sourcemap' import type { DevEnvironment } from '../server/environment' export interface FetchModuleOptions { + cached?: boolean inlineSourceMap?: boolean processSourceMap?>(map: T): T } @@ -84,6 +85,14 @@ export async function fetchModule( url = unwrapId(url) + let mod: EnvironmentModuleNode | undefined + + // if url is already cached, we can just confirm it's also cached on the server + if (options.cached) { + mod = await environment.moduleGraph.getModuleByUrl(url) + if (mod?.transformResult) return { cache: true } + } + let result = await environment.transformRequest(url) if (!result) { @@ -95,7 +104,7 @@ export async function fetchModule( } // module entry should be created by transformRequest - const mod = await environment.moduleGraph.getModuleByUrl(url) + mod ??= await environment.moduleGraph.getModuleByUrl(url) if (!mod) { throw new Error( @@ -116,10 +125,6 @@ export async function fetchModule( return { code: result.code, file: mod.file, - invalidationTimestamp: Math.max( - mod.lastHMRTimestamp, - mod.lastInvalidationTimestamp, - ), } } diff --git a/packages/vite/src/node/ssr/runtime/serverModuleRunner.ts b/packages/vite/src/node/ssr/runtime/serverModuleRunner.ts index 2df093ead35fa7..931bd07b80a5c7 100644 --- a/packages/vite/src/node/ssr/runtime/serverModuleRunner.ts +++ b/packages/vite/src/node/ssr/runtime/serverModuleRunner.ts @@ -92,7 +92,8 @@ export function createServerModuleRunner( ...options, root: environment.config.root, transport: { - fetchModule: (id, importer) => environment.fetchModule(id, importer), + fetchModule: (id, importer, options) => + environment.fetchModule(id, importer, options), }, hmr, sourcemapInterceptor: resolveSourceMapOptions(options), From a63263cb1c9b482c4c17974a535bb68530fc5b01 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 28 May 2024 13:45:10 +0200 Subject: [PATCH 107/123] fix: do not invalidate modules in the runner if not invalidated on the server --- packages/vite/src/module-runner/runner.ts | 8 +++-- packages/vite/src/node/ssr/fetchModule.ts | 9 ++--- playground/hmr-ssr/__tests__/hmr-ssr.spec.ts | 37 ++++++++++---------- 3 files changed, 30 insertions(+), 24 deletions(-) diff --git a/packages/vite/src/module-runner/runner.ts b/packages/vite/src/module-runner/runner.ts index 8084924020a5a9..9c9bfc7d5436fe 100644 --- a/packages/vite/src/module-runner/runner.ts +++ b/packages/vite/src/module-runner/runner.ts @@ -239,12 +239,14 @@ export class ModuleRunner { cachedModule = this.moduleCache.getByModuleId(url) } + const isCached = !!(typeof cachedModule === 'object' && cachedModule.meta) + const fetchedModule = // fast return for established externalized pattern ( url.startsWith('data:') ? { externalize: url, type: 'builtin' } : await this.transport.fetchModule(url, importer, { - cached: !!(typeof cachedModule === 'object' && cachedModule.meta), + cached: isCached, }) ) as ResolvedResult @@ -267,7 +269,9 @@ export class ModuleRunner { const moduleId = this.moduleCache.normalize(fileId) const mod = this.moduleCache.getByModuleId(moduleId) - this.moduleCache.invalidateModule(mod) + if ('invalidate' in fetchedModule && fetchedModule.invalidate) { + this.moduleCache.invalidateModule(mod) + } fetchedModule.id = moduleId mod.meta = fetchedModule diff --git a/packages/vite/src/node/ssr/fetchModule.ts b/packages/vite/src/node/ssr/fetchModule.ts index 3cfa278857c3f4..6af5e0d5f3af7b 100644 --- a/packages/vite/src/node/ssr/fetchModule.ts +++ b/packages/vite/src/node/ssr/fetchModule.ts @@ -85,12 +85,12 @@ export async function fetchModule( url = unwrapId(url) - let mod: EnvironmentModuleNode | undefined + let mod = await environment.moduleGraph.getModuleByUrl(url) + const cached = !!mod?.transformResult // if url is already cached, we can just confirm it's also cached on the server - if (options.cached) { - mod = await environment.moduleGraph.getModuleByUrl(url) - if (mod?.transformResult) return { cache: true } + if (options.cached && cached) { + return { cache: true } } let result = await environment.transformRequest(url) @@ -125,6 +125,7 @@ export async function fetchModule( return { code: result.code, file: mod.file, + invalidate: !cached, } } diff --git a/playground/hmr-ssr/__tests__/hmr-ssr.spec.ts b/playground/hmr-ssr/__tests__/hmr-ssr.spec.ts index c3b3f4410c8ae8..9e51525766e2ae 100644 --- a/playground/hmr-ssr/__tests__/hmr-ssr.spec.ts +++ b/playground/hmr-ssr/__tests__/hmr-ssr.spec.ts @@ -297,27 +297,28 @@ describe('hmr works correctly', () => { // expect((await page.$$('link')).length).toBe(1) // }) - // #2255 - test('importing reloaded', async () => { - const outputEle = () => hmr('.importing-reloaded') + // #2255 - not applicable to SSR becaue invlaidateModule expects the module + // to always be reloaded again + // test('importing reloaded', async () => { + // const outputEle = () => hmr('.importing-reloaded') - await untilUpdated(outputEle, ['a.js: a0', 'b.js: b0,a0'].join('
')) + // await untilUpdated(outputEle, ['a.js: a0', 'b.js: b0,a0'].join('
')) - editFile('importing-updated/a.js', (code) => code.replace("'a0'", "'a1'")) - await untilUpdated( - outputEle, - ['a.js: a0', 'b.js: b0,a0', 'a.js: a1'].join('
'), - ) + // editFile('importing-updated/a.js', (code) => code.replace("'a0'", "'a1'")) + // await untilUpdated( + // outputEle, + // ['a.js: a0', 'b.js: b0,a0', 'a.js: a1'].join('
'), + // ) - editFile('importing-updated/b.js', (code) => - code.replace('`b0,${a}`', '`b1,${a}`'), - ) - // note that "a.js: a1" should not happen twice after "b.js: b0,a0'" - await untilUpdated( - outputEle, - ['a.js: a0', 'b.js: b0,a0', 'a.js: a1', 'b.js: b1,a1'].join('
'), - ) - }) + // editFile('importing-updated/b.js', (code) => + // code.replace('`b0,${a}`', '`b1,${a}`'), + // ) + // // note that "a.js: a1" should not happen twice after "b.js: b0,a0'" + // await untilUpdated( + // outputEle, + // ['a.js: a0', 'b.js: b0,a0', 'a.js: a1', 'b.js: b1,a1'].join('
'), + // ) + // }) }) describe('acceptExports', () => { From 28e22c600e85819a7aade8825c59915f88670dbb Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 28 May 2024 13:45:35 +0200 Subject: [PATCH 108/123] chore: cleanup --- packages/vite/src/module-runner/types.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/vite/src/module-runner/types.ts b/packages/vite/src/module-runner/types.ts index 0d4522c841e407..ce53841c8443cf 100644 --- a/packages/vite/src/module-runner/types.ts +++ b/packages/vite/src/module-runner/types.ts @@ -116,6 +116,10 @@ export interface ViteFetchResult { * Will be equal to `null` for virtual modules */ file: string | null + /** + * Invalidate module on the client side. + */ + invalidate: boolean } export type ResolvedResult = (ExternalFetchResult | ViteFetchResult) & { From c373251123ffbfddf2f703b6a62a5d9b1a03db67 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 28 May 2024 13:55:35 +0200 Subject: [PATCH 109/123] chore: prettier --- packages/vite/src/module-runner/runner.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/vite/src/module-runner/runner.ts b/packages/vite/src/module-runner/runner.ts index 9c9bfc7d5436fe..7ee5c126ceb3b2 100644 --- a/packages/vite/src/module-runner/runner.ts +++ b/packages/vite/src/module-runner/runner.ts @@ -242,13 +242,13 @@ export class ModuleRunner { const isCached = !!(typeof cachedModule === 'object' && cachedModule.meta) const fetchedModule = // fast return for established externalized pattern - ( - url.startsWith('data:') - ? { externalize: url, type: 'builtin' } - : await this.transport.fetchModule(url, importer, { - cached: isCached, - }) - ) as ResolvedResult + ( + url.startsWith('data:') + ? { externalize: url, type: 'builtin' } + : await this.transport.fetchModule(url, importer, { + cached: isCached, + }) + ) as ResolvedResult if ('cache' in fetchedModule) { if (!cachedModule || !cachedModule.meta) { From 909782f2924262e7a9c7b0674acafb67b7c17a9f Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Wed, 29 May 2024 16:19:01 +0200 Subject: [PATCH 110/123] refactor: plugin container (#17288) --- .../vite/src/node/plugins/importAnalysis.ts | 11 +- .../vite/src/node/server/pluginContainer.ts | 1306 +++++++++-------- packages/vite/src/node/utils.ts | 2 +- 3 files changed, 691 insertions(+), 628 deletions(-) diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index a3f0c3538548db..b0617bc2385504 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -63,6 +63,7 @@ import { withTrailingSlash, wrapId, } from '../../shared/utils' +import type { TransformPluginContext } from '../server/pluginContainer' import { throwOutdatedRequest } from './optimizedDeps' import { isCSSRequest, isDirectCSSRequest } from './css' import { browserExternalId } from './resolve' @@ -243,7 +244,10 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { throwOutdatedRequest(importer) } - if (!imports.length && !(this as any)._addedImports) { + if ( + !imports.length && + !(this as unknown as TransformPluginContext)._addedImports + ) { importerModule.isSelfAccepting = false debug?.( `${timeFrom(msAtStart)} ${colors.dim( @@ -730,9 +734,8 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { // note that we want to handle .css?raw and .css?url here if (!isCSSRequest(importer) || SPECIAL_QUERY_RE.test(importer)) { // attached by pluginContainer.addWatchFile - const pluginImports = (this as any)._addedImports as - | Set - | undefined + const pluginImports = (this as unknown as TransformPluginContext) + ._addedImports if (pluginImports) { ;( await Promise.all( diff --git a/packages/vite/src/node/server/pluginContainer.ts b/packages/vite/src/node/server/pluginContainer.ts index 23386d23273ab3..6c774270515ab6 100644 --- a/packages/vite/src/node/server/pluginContainer.ts +++ b/packages/vite/src/node/server/pluginContainer.ts @@ -52,6 +52,7 @@ import type { RollupError, RollupLog, PluginContext as RollupPluginContext, + TransformPluginContext as RollupTransformPluginContext, SourceDescription, SourceMap, TransformResult, @@ -59,7 +60,7 @@ import type { import type { RawSourceMap } from '@ampproject/remapping' import { TraceMap, originalPositionFor } from '@jridgewell/trace-mapping' import MagicString from 'magic-string' -import type { FSWatcher } from 'chokidar' +import type { FSWatcher } from 'dep-types/chokidar' import colors from 'picocolors' import type { Plugin } from '../plugin' import { @@ -78,14 +79,33 @@ import { import { FS_PREFIX } from '../constants' import { createPluginHookUtils, getHookHandler } from '../plugins' import { cleanUrl, unwrapId } from '../../shared/utils' +import type { PluginHookUtils } from '../config' import type { Environment } from '../environment' -import { warnFutureDeprecation } from '../deprecations' import type { DevEnvironment } from './environment' import { buildErrorMessage } from './middlewares/error' -import type { EnvironmentModuleNode } from './moduleGraph' +import type { + EnvironmentModuleGraph, + EnvironmentModuleNode, +} from './moduleGraph' const noop = () => {} +// same default value of "moduleInfo.meta" as in Rollup +const EMPTY_OBJECT = Object.freeze({}) + +const debugSourcemapCombineFilter = + process.env.DEBUG_VITE_SOURCEMAP_COMBINE_FILTER +const debugSourcemapCombine = createDebugger('vite:sourcemap-combine', { + onlyWhenFocused: true, +}) +const debugResolve = createDebugger('vite:resolve') +const debugPluginResolve = createDebugger('vite:plugin-resolve', { + onlyWhenFocused: 'vite:plugin', +}) +const debugPluginTransform = createDebugger('vite:plugin-transform', { + onlyWhenFocused: 'vite:plugin', +}) + export const ERR_CLOSED_SERVER = 'ERR_CLOSED_SERVER' export function throwClosedServerError(): never { @@ -105,44 +125,6 @@ export interface PluginContainerOptions { writeFile?: (name: string, source: string | Uint8Array) => void } -export interface EnvironmentPluginContainer { - options: InputOptions - buildStart(options: InputOptions): Promise - resolveId( - id: string, - importer: string | undefined, - options?: { - attributes?: Record - custom?: CustomPluginOptions - skip?: Set - /** - * @internal - */ - scan?: boolean - isEntry?: boolean - }, - ): Promise - transform( - code: string, - id: string, - options?: { - inMap?: SourceDescription['map'] - }, - ): Promise<{ code: string; map: SourceMap | { mappings: '' } | null }> - load(id: string, options?: {}): Promise - watchChange( - id: string, - change: { event: 'create' | 'update' | 'delete' }, - ): Promise - close(): Promise -} - -type PluginContext = Omit< - RollupPluginContext, - // not documented - 'cache' -> - /** * 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 @@ -153,78 +135,125 @@ export async function createEnvironmentPluginContainer( plugins: Plugin[], watcher?: FSWatcher, ): Promise { - const { - config, - logger, - options: { - build: { rollupOptions }, - }, - } = environment - const { root } = config + const container = new EnvironmentPluginContainer( + environment, + plugins, + watcher, + ) + await container.resolveRollupOptions() + return container +} + +class EnvironmentPluginContainer { + private _pluginContextMap = new Map() + private _resolvedRollupOptions?: InputOptions + private _processesing = new Set>() + private _seenResolves: Record = {} - // Backward compatibility - const ssr = environment.name !== 'client' - - const moduleGraph = - environment.mode === 'dev' ? environment.moduleGraph : undefined - - const { getSortedPluginHooks, getSortedPlugins } = - createPluginHookUtils(plugins) - - const seenResolves: Record = {} - const debugResolve = createDebugger('vite:resolve') - const debugPluginResolve = createDebugger('vite:plugin-resolve', { - onlyWhenFocused: 'vite:plugin', - }) - const debugPluginTransform = createDebugger('vite:plugin-transform', { - onlyWhenFocused: 'vite:plugin', - }) - const debugSourcemapCombineFilter = - process.env.DEBUG_VITE_SOURCEMAP_COMBINE_FILTER - const debugSourcemapCombine = createDebugger('vite:sourcemap-combine', { - onlyWhenFocused: true, - }) - - // --------------------------------------------------------------------------- - - 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< + private _moduleNodeToLoadAddedImports = new WeakMap< EnvironmentModuleNode, Set | null >() - const minimalContext: MinimalPluginContext = { - meta: { - rollupVersion, - watchMode: true, - }, - debug: noop, - info: noop, - warn: noop, - // @ts-expect-error noop - error: noop, + getSortedPluginHooks: PluginHookUtils['getSortedPluginHooks'] + getSortedPlugins: PluginHookUtils['getSortedPlugins'] + + moduleGraph: EnvironmentModuleGraph | undefined + watchFiles = new Set() + minimalContext: MinimalPluginContext + + private _closed = false + + /** + * @internal use `createEnvironmentPluginContainer` instead + */ + constructor( + public environment: Environment, + public plugins: Plugin[], + public watcher?: FSWatcher, + ) { + this.minimalContext = { + meta: { + rollupVersion, + watchMode: true, + }, + debug: noop, + info: noop, + warn: noop, + // @ts-expect-error noop + error: noop, + } + const utils = createPluginHookUtils(plugins) + this.getSortedPlugins = utils.getSortedPlugins + this.getSortedPluginHooks = utils.getSortedPluginHooks + this.moduleGraph = + environment.mode === 'dev' ? environment.moduleGraph : undefined } - function warnIncompatibleMethod(method: string, plugin: string) { - logger.warn( - colors.cyan(`[plugin:${plugin}] `) + - colors.yellow( - `context method ${colors.bold( - `${method}()`, - )} is not supported in serve mode. This plugin is likely not vite-compatible.`, - ), - ) + private _updateModuleLoadAddedImports( + id: string, + addedImports: Set | null, + ): void { + const module = this.moduleGraph?.getModuleById(id) + if (module) { + this._moduleNodeToLoadAddedImports.set(module, addedImports) + } + } + + private _getAddedImports(id: string): Set | null { + const module = this.moduleGraph?.getModuleById(id) + return module + ? this._moduleNodeToLoadAddedImports.get(module) || null + : null + } + + // keeps track of hook promises so that we can wait for them all to finish upon closing the server + private handleHookPromise(maybePromise: undefined | T | Promise) { + if (!(maybePromise as any)?.then) { + return maybePromise + } + const promise = maybePromise as Promise + this._processesing.add(promise) + return promise.finally(() => this._processesing.delete(promise)) + } + + get options(): InputOptions { + return this._resolvedRollupOptions! + } + + async resolveRollupOptions(): Promise { + if (!this._resolvedRollupOptions) { + let options = this.environment.options.build.rollupOptions + for (const optionsHook of this.getSortedPluginHooks('options')) { + if (this._closed) { + throwClosedServerError() + } + options = + (await this.handleHookPromise( + optionsHook.call(this.minimalContext, options), + )) || options + } + this._resolvedRollupOptions = options + } + return this._resolvedRollupOptions + } + + private _getPluginContext(plugin: Plugin) { + if (!this._pluginContextMap.has(plugin)) { + this._pluginContextMap.set(plugin, new PluginContext(plugin, this)) + } + return this._pluginContextMap.get(plugin)! } // parallel, ignores returns - async function hookParallel( + private async hookParallel( hookName: H, context: (plugin: Plugin) => ThisType, args: (plugin: Plugin) => Parameters, ): Promise { const parallelPromises: Promise[] = [] - for (const plugin of getSortedPlugins(hookName)) { + for (const plugin of this.getSortedPlugins(hookName)) { // Don't throw here if closed, so buildEnd and closeBundle hooks can finish running const hook = plugin[hookName] if (!hook) continue @@ -241,205 +270,398 @@ export async function createEnvironmentPluginContainer( await Promise.all(parallelPromises) } - // throw when an unsupported ModuleInfo property is accessed, - // so that incompatible plugins fail in a non-cryptic way. - const ModuleInfoProxy: ProxyHandler = { - get(info: any, key: string) { - if (key in info) { - return info[key] - } - // Don't throw an error when returning from an async function - if (key === 'then') { - return undefined - } - throw Error( - `[vite] The "${key}" property of ModuleInfo is not supported.`, - ) - }, + async buildStart(_options?: InputOptions): Promise { + await this.handleHookPromise( + this.hookParallel( + 'buildStart', + (plugin) => this._getPluginContext(plugin), + () => [this.options as NormalizedInputOptions], + ), + ) } - // same default value of "moduleInfo.meta" as in Rollup - const EMPTY_OBJECT = Object.freeze({}) - - // 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: Environment - meta = minimalContext.meta - ssr = false - _scan = false - _activePlugin: Plugin | null - _activeId: string | null = null - _activeCode: string | null = null - _resolveSkips?: Set - _addedImports: Set | null = null - - constructor(initialPlugin?: Plugin) { - this.environment = environment - this._activePlugin = initialPlugin || null - } - - parse(code: string, opts: any) { - return rollupParseAst(code, opts) - } + async resolveId( + rawId: string, + importer: string | undefined = join( + this.environment.config.root, + 'index.html', + ), + options?: { + attributes?: Record + custom?: CustomPluginOptions + skip?: Set + /** + * @internal + */ + scan?: boolean + isEntry?: boolean + }, + ): Promise { + const skip = options?.skip + const scan = !!options?.scan + const ssr = this.environment.name !== 'client' + const ctx = new ResolveIdContext(this, skip, scan) + + const resolveStart = debugResolve ? performance.now() : 0 + let id: string | null = null + const partial: Partial = {} + for (const plugin of this.getSortedPlugins('resolveId')) { + if (this._closed && this.environment?.options.dev.recoverable) + throwClosedServerError() + if (!plugin.resolveId) continue + if (skip?.has(plugin)) continue + + ctx._plugin = plugin + + const pluginResolveStart = debugPluginResolve ? performance.now() : 0 + const handler = getHookHandler(plugin.resolveId) + const result = await this.handleHookPromise( + handler.call(ctx as any, rawId, importer, { + attributes: options?.attributes ?? {}, + custom: options?.custom, + isEntry: !!options?.isEntry, + ssr, + scan, + }), + ) + if (!result) continue - async resolve( - id: string, - importer?: string, - options?: { - attributes?: Record - custom?: CustomPluginOptions - isEntry?: boolean - skipSelf?: boolean - }, - ) { - let skip: Set | undefined - if (options?.skipSelf !== false && this._activePlugin) { - skip = new Set(this._resolveSkips) - skip.add(this._activePlugin) + if (typeof result === 'string') { + id = result + } else { + id = result.id + Object.assign(partial, result) } - let out = await container.resolveId(id, importer, { - attributes: options?.attributes, - custom: options?.custom, - isEntry: !!options?.isEntry, - skip, - scan: this._scan, - }) - if (typeof out === 'string') out = { id: out } - return out as ResolvedId | null - } - async load( - options: { - id: string - resolveDependencies?: boolean - } & Partial>, - ): Promise { - // We may not have added this to our module graph yet, so ensure it exists - 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 - this._updateModuleInfo(options.id, options) - - const loadResult = await container.load(options.id) - const code = - typeof loadResult === 'object' ? loadResult?.code : loadResult - if (code != null) { - await container.transform(code, options.id) - } + debugPluginResolve?.( + timeFrom(pluginResolveStart), + plugin.name, + prettifyUrl(id, this.environment.config.root), + ) - const moduleInfo = this.getModuleInfo(options.id) - // This shouldn't happen due to calling ensureEntryFromUrl, but 1) our types can't ensure that - // and 2) moduleGraph may not have been provided (though in the situations where that happens, - // we should never have plugins calling this.load) - if (!moduleInfo) - throw Error(`Failed to load module with id ${options.id}`) - return moduleInfo + // resolveId() is hookFirst - first non-null result is returned. + break } - 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, + if (debugResolve && rawId !== id && !rawId.startsWith(FS_PREFIX)) { + const key = rawId + id + // avoid spamming + if (!this._seenResolves[key]) { + this._seenResolves[key] = true + debugResolve( + `${timeFrom(resolveStart)} ${colors.cyan(rawId)} -> ${colors.dim( + id, + )}`, ) } - return module.info } - _updateModuleInfo(id: string, { meta }: { meta?: object | null }) { - if (meta) { - const moduleInfo = this.getModuleInfo(id) - if (moduleInfo) { - moduleInfo.meta = { ...moduleInfo.meta, ...meta } - } - } + if (id) { + partial.id = isExternalUrl(id) ? id : normalizePath(id) + return partial as PartialResolvedId + } else { + return null } + } - _updateModuleLoadAddedImports(id: string) { - const module = moduleGraph?.getModuleById(id) - if (module) { - moduleNodeToLoadAddedImports.set(module, this._addedImports) + async load(id: string, options?: {}): Promise { + const ssr = this.environment.name !== 'client' + options = options ? { ...options, ssr } : { ssr } + const ctx = new LoadPluginContext(this) + for (const plugin of this.getSortedPlugins('load')) { + if (this._closed && this.environment?.options.dev.recoverable) + throwClosedServerError() + if (!plugin.load) continue + ctx._plugin = plugin + const handler = getHookHandler(plugin.load) + const result = await this.handleHookPromise( + handler.call(ctx as any, id, options), + ) + if (result != null) { + if (isObject(result)) { + ctx._updateModuleInfo(id, result) + } + this._updateModuleLoadAddedImports(id, ctx._addedImports) + return result } } + this._updateModuleLoadAddedImports(id, ctx._addedImports) + return null + } - getModuleIds() { - return ( - moduleGraph?.idToModuleMap.keys() ?? Array.prototype[Symbol.iterator]() + async transform( + code: string, + id: string, + options?: { + inMap?: SourceDescription['map'] + }, + ): Promise<{ code: string; map: SourceMap | { mappings: '' } | null }> { + const ssr = this.environment.name !== 'client' + const optionsWithSSR = options ? { ...options, ssr } : { ssr } + const inMap = options?.inMap + + const ctx = new TransformPluginContext(this, id, code, inMap as SourceMap) + ctx._addedImports = this._getAddedImports(id) + + for (const plugin of this.getSortedPlugins('transform')) { + if (this._closed && this.environment?.options.dev.recoverable) + throwClosedServerError() + if (!plugin.transform) continue + + ctx._updateActiveInfo(plugin, id, code) + const start = debugPluginTransform ? performance.now() : 0 + let result: TransformResult | string | undefined + const handler = getHookHandler(plugin.transform) + try { + result = await this.handleHookPromise( + handler.call(ctx as any, code, id, optionsWithSSR), + ) + } catch (e) { + ctx.error(e) + } + if (!result) continue + debugPluginTransform?.( + timeFrom(start), + plugin.name, + prettifyUrl(id, this.environment.config.root), ) + if (isObject(result)) { + if (result.code !== undefined) { + code = result.code + if (result.map) { + if (debugSourcemapCombine) { + // @ts-expect-error inject plugin name for debug purpose + result.map.name = plugin.name + } + ctx.sourcemapChain.push(result.map) + } + } + ctx._updateModuleInfo(id, result) + } else { + code = result + } } - - addWatchFile(id: string) { - watchFiles.add(id) - ;(this._addedImports || (this._addedImports = new Set())).add(id) - if (watcher) ensureWatchedFile(watcher, id, root) + return { + code, + map: ctx._getCombinedSourcemap(), } + } - getWatchFiles() { - return [...watchFiles] - } + async watchChange( + id: string, + change: { event: 'create' | 'update' | 'delete' }, + ): Promise { + await this.hookParallel( + 'watchChange', + (plugin) => this._getPluginContext(plugin), + () => [id, change], + ) + } - emitFile(assetOrFile: EmittedFile) { - warnIncompatibleMethod(`emitFile`, this._activePlugin!.name) - return '' - } + async close(): Promise { + if (this._closed) return + this._closed = true + await Promise.allSettled(Array.from(this._processesing)) + await this.hookParallel( + 'buildEnd', + (plugin) => this._getPluginContext(plugin), + () => [], + ) + await this.hookParallel( + 'closeBundle', + (plugin) => this._getPluginContext(plugin), + () => [], + ) + } +} + +class PluginContext implements Omit { + ssr = false + _scan = false + _activeId: string | null = null + _activeCode: string | null = null + _resolveSkips?: Set + meta: RollupPluginContext['meta'] + environment: Environment + + constructor( + public _plugin: Plugin, + public _container: EnvironmentPluginContainer, + ) { + this.environment = this._container.environment + this.meta = this._container.minimalContext.meta + } + + parse(code: string, opts: any) { + return rollupParseAst(code, opts) + } - setAssetSource() { - warnIncompatibleMethod(`setAssetSource`, this._activePlugin!.name) + async resolve( + id: string, + importer?: string, + options?: { + attributes?: Record + custom?: CustomPluginOptions + isEntry?: boolean + skipSelf?: boolean + }, + ) { + let skip: Set | undefined + if (options?.skipSelf !== false && this._plugin) { + skip = new Set(this._resolveSkips) + skip.add(this._plugin) } + let out = await this._container.resolveId(id, importer, { + attributes: options?.attributes, + custom: options?.custom, + isEntry: !!options?.isEntry, + skip, + scan: this._scan, + }) + if (typeof out === 'string') out = { id: out } + return out as ResolvedId | null + } - getFileName() { - warnIncompatibleMethod(`getFileName`, this._activePlugin!.name) - return '' + async load( + options: { + id: string + resolveDependencies?: boolean + } & Partial>, + ): Promise { + // We may not have added this to our module graph yet, so ensure it exists + await this._container.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 + this._updateModuleInfo(options.id, options) + + const loadResult = await this._container.load(options.id) + const code = typeof loadResult === 'object' ? loadResult?.code : loadResult + if (code != null) { + await this._container.transform(code, options.id) } - warn( - e: string | RollupLog | (() => string | RollupLog), - position?: number | { column: number; line: number }, - ) { - const err = formatError(typeof e === 'function' ? e() : e, position, this) - const msg = buildErrorMessage( - err, - [colors.yellow(`warning: ${err.message}`)], - false, + const moduleInfo = this.getModuleInfo(options.id) + // This shouldn't happen due to calling ensureEntryFromUrl, but 1) our types can't ensure that + // and 2) moduleGraph may not have been provided (though in the situations where that happens, + // we should never have plugins calling this.load) + if (!moduleInfo) throw Error(`Failed to load module with id ${options.id}`) + return moduleInfo + } + + getModuleInfo(id: string): ModuleInfo | null { + const module = this._container.moduleGraph?.getModuleById(id) + if (!module) { + return null + } + if (!module.info) { + module.info = new Proxy( + { id, meta: module.meta || EMPTY_OBJECT } as ModuleInfo, + + // throw when an unsupported ModuleInfo property is accessed, + // so that incompatible plugins fail in a non-cryptic way. + { + get(info: any, key: string) { + if (key in info) { + return info[key] + } + // Don't throw an error when returning from an async function + if (key === 'then') { + return undefined + } + throw Error( + `[vite] The "${key}" property of ModuleInfo is not supported.`, + ) + }, + }, ) - logger.warn(msg, { - clear: true, - timestamp: true, - }) } + return module.info ?? null + } - error( - e: string | RollupError, - position?: number | { column: number; line: number }, - ): never { - // error thrown here is caught by the transform middleware and passed on - // the the error middleware. - throw formatError(e, position, this) + _updateModuleInfo(id: string, { meta }: { meta?: object | null }) { + if (meta) { + const moduleInfo = this.getModuleInfo(id) + if (moduleInfo) { + moduleInfo.meta = { ...moduleInfo.meta, ...meta } + } } + } + + getModuleIds(): IterableIterator { + return this._container.moduleGraph + ? this._container.moduleGraph.idToModuleMap.keys() + : Array.prototype[Symbol.iterator]() + } + + addWatchFile(id: string): void { + this._container.watchFiles.add(id) + // ;(this._addedImports || (this._addedImports = new Set())).add(id) + if (this._container.watcher) + ensureWatchedFile( + this._container.watcher, + id, + this.environment.config.root, + ) + } + + getWatchFiles(): string[] { + return [...this._container.watchFiles] + } + + emitFile(assetOrFile: EmittedFile): string { + this._warnIncompatibleMethod(`emitFile`) + return '' + } + + setAssetSource(): void { + this._warnIncompatibleMethod(`setAssetSource`) + } + + getFileName(): string { + this._warnIncompatibleMethod(`getFileName`) + return '' + } + + warn( + e: string | RollupLog | (() => string | RollupLog), + position?: number | { column: number; line: number }, + ): void { + const err = this._formatError(typeof e === 'function' ? e() : e, position) + const msg = buildErrorMessage( + err, + [colors.yellow(`warning: ${err.message}`)], + false, + ) + this.environment.logger.warn(msg, { + clear: true, + timestamp: true, + }) + } - debug = noop - info = noop + error( + e: string | RollupError, + position?: number | { column: number; line: number }, + ): never { + // error thrown here is caught by the transform middleware and passed on + // the the error middleware. + throw this._formatError(e, position) } - function formatError( + debug = noop + info = noop + + private _formatError( e: string | RollupError, position: number | { column: number; line: number } | undefined, - ctx: Context, - ) { + ): RollupError { const err = (typeof e === 'string' ? new Error(e) : e) as RollupError if (err.pluginCode) { return err // The plugin likely called `this.error` } - if (ctx._activePlugin) err.plugin = ctx._activePlugin.name - if (ctx._activeId && !err.id) err.id = ctx._activeId - if (ctx._activeCode) { - err.pluginCode = ctx._activeCode + if (this._plugin) err.plugin = this._plugin.name + if (this._activeId && !err.id) err.id = this._activeId + if (this._activeCode) { + err.pluginCode = this._activeCode // some rollup plugins, e.g. json, sets err.position instead of err.pos const pos = position ?? err.pos ?? (err as any).position @@ -447,9 +669,9 @@ export async function createEnvironmentPluginContainer( if (pos != null) { let errLocation try { - errLocation = numberToPos(ctx._activeCode, pos) + errLocation = numberToPos(this._activeCode, pos) } catch (err2) { - logger.error( + this.environment.logger.error( colors.red( `Error in error handler:\n${err2.stack || err2.message}\n`, ), @@ -462,11 +684,11 @@ export async function createEnvironmentPluginContainer( file: err.id, ...errLocation, } - err.frame = err.frame || generateCodeFrame(ctx._activeCode, pos) + err.frame = err.frame || generateCodeFrame(this._activeCode, pos) } else if (err.loc) { // css preprocessors may report errors in an included file if (!err.frame) { - let code = ctx._activeCode + let code = this._activeCode if (err.loc.file) { err.id = normalizePath(err.loc.file) try { @@ -481,15 +703,16 @@ export async function createEnvironmentPluginContainer( line: (err as any).line, column: (err as any).column, } - err.frame = err.frame || generateCodeFrame(ctx._activeCode, err.loc) + err.frame = err.frame || generateCodeFrame(this._activeCode, err.loc) } + // TODO: move it to overrides if ( - ctx instanceof TransformContext && + this instanceof TransformPluginContext && typeof err.loc?.line === 'number' && typeof err.loc?.column === 'number' ) { - const rawSourceMap = ctx._getCombinedSourcemap() + const rawSourceMap = this._getCombinedSourcemap() if (rawSourceMap && 'version' in rawSourceMap) { const traced = new TraceMap(rawSourceMap as any) const { source, line, column } = originalPositionFor(traced, { @@ -529,424 +752,261 @@ export async function createEnvironmentPluginContainer( return err } - class TransformContext extends Context { - filename: string - originalCode: string - originalSourcemap: SourceMap | null = null - sourcemapChain: NonNullable[] = [] - combinedMap: SourceMap | { mappings: '' } | null = null - - constructor(id: string, code: string, inMap?: SourceMap | string) { - super() - this.filename = id - this.originalCode = code - if (inMap) { - if (debugSourcemapCombine) { - // @ts-expect-error inject name for debug purpose - inMap.name = '$inMap' - } - this.sourcemapChain.push(inMap) - } - // Inherit `_addedImports` from the `load()` hook - const node = moduleGraph?.getModuleById(id) - if (node) { - this._addedImports = moduleNodeToLoadAddedImports.get(node) ?? null - } - } - - _getCombinedSourcemap() { - if ( - debugSourcemapCombine && - debugSourcemapCombineFilter && - this.filename.includes(debugSourcemapCombineFilter) - ) { - debugSourcemapCombine('----------', this.filename) - debugSourcemapCombine(this.combinedMap) - debugSourcemapCombine(this.sourcemapChain) - debugSourcemapCombine('----------') - } + _warnIncompatibleMethod(method: string): void { + this.environment.logger.warn( + colors.cyan(`[plugin:${this._plugin.name}] `) + + colors.yellow( + `context method ${colors.bold( + `${method}()`, + )} is not supported in serve mode. This plugin is likely not vite-compatible.`, + ), + ) + } +} - let combinedMap = this.combinedMap - // { mappings: '' } - if ( - combinedMap && - !('version' in combinedMap) && - combinedMap.mappings === '' - ) { - this.sourcemapChain.length = 0 - return combinedMap - } +class ResolveIdContext extends PluginContext { + constructor( + container: EnvironmentPluginContainer, + skip: Set | undefined, + scan: boolean, + ) { + super(null!, container) + this._resolveSkips = skip + this._scan = scan + } +} - for (let m of this.sourcemapChain) { - if (typeof m === 'string') m = JSON.parse(m) - if (!('version' in (m as SourceMap))) { - // { mappings: '' } - if ((m as SourceMap).mappings === '') { - combinedMap = { mappings: '' } - break - } - // empty, nullified source map - combinedMap = null - break - } - if (!combinedMap) { - const sm = m as SourceMap - // sourcemap should not include `sources: [null]` (because `sources` should be string) nor - // `sources: ['']` (because `''` means the path of sourcemap) - // but MagicString generates this when `filename` option is not set. - // Rollup supports these and therefore we support this as well - if (sm.sources.length === 1 && !sm.sources[0]) { - combinedMap = { - ...sm, - sources: [this.filename], - sourcesContent: [this.originalCode], - } - } else { - combinedMap = sm - } - } else { - combinedMap = combineSourcemaps(cleanUrl(this.filename), [ - m as RawSourceMap, - combinedMap as RawSourceMap, - ]) as SourceMap - } - } - if (combinedMap !== this.combinedMap) { - this.combinedMap = combinedMap - this.sourcemapChain.length = 0 - } - return this.combinedMap - } +class LoadPluginContext extends PluginContext { + _addedImports: Set | null = null - getCombinedSourcemap() { - const map = this._getCombinedSourcemap() - if (!map || (!('version' in map) && map.mappings === '')) { - return new MagicString(this.originalCode).generateMap({ - includeContent: true, - hires: 'boundary', - source: cleanUrl(this.filename), - }) - } - return map - } + constructor(container: EnvironmentPluginContainer) { + super(null!, container) } - let closed = false - const processesing = new Set>() - // keeps track of hook promises so that we can wait for them all to finish upon closing the server - function handleHookPromise(maybePromise: undefined | T | Promise) { - if (!(maybePromise as any)?.then) { - return maybePromise + override addWatchFile(id: string): void { + if (!this._addedImports) { + this._addedImports = new Set() } - const promise = maybePromise as Promise - processesing.add(promise) - return promise.finally(() => processesing.delete(promise)) + this._addedImports.add(id) + super.addWatchFile(id) } +} - const container: PluginContainer = { - options: await (async () => { - let options = rollupOptions - for (const optionsHook of getSortedPluginHooks('options')) { - if (closed) throwClosedServerError() - options = - (await handleHookPromise( - optionsHook.call(minimalContext, options), - )) || options +class TransformPluginContext + extends LoadPluginContext + implements Omit +{ + filename: string + originalCode: string + originalSourcemap: SourceMap | null = null + sourcemapChain: NonNullable[] = [] + combinedMap: SourceMap | { mappings: '' } | null = null + + constructor( + container: EnvironmentPluginContainer, + id: string, + code: string, + inMap?: SourceMap | string, + ) { + super(container) + + this.filename = id + this.originalCode = code + if (inMap) { + if (debugSourcemapCombine) { + // @ts-expect-error inject name for debug purpose + inMap.name = '$inMap' } - return options - })(), - - async buildStart() { - await handleHookPromise( - hookParallel( - 'buildStart', - (plugin) => new Context(plugin), - () => [container.options as NormalizedInputOptions], - ), - ) - }, - - async resolveId(rawId, importer = join(root, 'index.html'), options) { - const skip = options?.skip - const scan = !!options?.scan + this.sourcemapChain.push(inMap) + } + } - const ctx = new Context() - ctx._resolveSkips = skip - ctx._scan = scan + _getCombinedSourcemap(): SourceMap { + if ( + debugSourcemapCombine && + debugSourcemapCombineFilter && + this.filename.includes(debugSourcemapCombineFilter) + ) { + debugSourcemapCombine('----------', this.filename) + debugSourcemapCombine(this.combinedMap) + debugSourcemapCombine(this.sourcemapChain) + debugSourcemapCombine('----------') + } - const resolveStart = debugResolve ? performance.now() : 0 - let id: string | null = null - const partial: Partial = {} - for (const plugin of getSortedPlugins('resolveId')) { - if (closed && environment?.options.dev.recoverable) - throwClosedServerError() - if (!plugin.resolveId) continue - if (skip?.has(plugin)) continue - - ctx._activePlugin = plugin - - const pluginResolveStart = debugPluginResolve ? performance.now() : 0 - const handler = getHookHandler(plugin.resolveId) - const result = await handleHookPromise( - handler.call(ctx as any, rawId, importer, { - attributes: options?.attributes ?? {}, - custom: options?.custom, - isEntry: !!options?.isEntry, - ssr, - scan, - }), - ) - if (!result) continue + let combinedMap = this.combinedMap + // { mappings: '' } + if ( + combinedMap && + !('version' in combinedMap) && + combinedMap.mappings === '' + ) { + this.sourcemapChain.length = 0 + return combinedMap as SourceMap + } - if (typeof result === 'string') { - id = result - } else { - id = result.id - Object.assign(partial, result) + for (let m of this.sourcemapChain) { + if (typeof m === 'string') m = JSON.parse(m) + if (!('version' in (m as SourceMap))) { + // { mappings: '' } + if ((m as SourceMap).mappings === '') { + combinedMap = { mappings: '' } + break } - - debugPluginResolve?.( - timeFrom(pluginResolveStart), - plugin.name, - prettifyUrl(id, root), - ) - - // resolveId() is hookFirst - first non-null result is returned. + // empty, nullified source map + combinedMap = null break } - - if (debugResolve && rawId !== id && !rawId.startsWith(FS_PREFIX)) { - const key = rawId + id - // avoid spamming - if (!seenResolves[key]) { - seenResolves[key] = true - debugResolve( - `${timeFrom(resolveStart)} ${colors.cyan(rawId)} -> ${colors.dim( - id, - )}`, - ) - } - } - - if (id) { - partial.id = isExternalUrl(id) ? id : normalizePath(id) - return partial as PartialResolvedId - } else { - return null - } - }, - - async load(id, options) { - options = { - ...options, - get ssr() { - warnFutureDeprecation(config, 'pluginHookSsrArgument') - return ssr - }, - } - const ctx = new Context() - for (const plugin of getSortedPlugins('load')) { - 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, options), - ) - if (result != null) { - if (isObject(result)) { - ctx._updateModuleInfo(id, result) + if (!combinedMap) { + const sm = m as SourceMap + // sourcemap should not include `sources: [null]` (because `sources` should be string) nor + // `sources: ['']` (because `''` means the path of sourcemap) + // but MagicString generates this when `filename` option is not set. + // Rollup supports these and therefore we support this as well + if (sm.sources.length === 1 && !sm.sources[0]) { + combinedMap = { + ...sm, + sources: [this.filename], + sourcesContent: [this.originalCode], } - ctx._updateModuleLoadAddedImports(id) - return result - } - } - ctx._updateModuleLoadAddedImports(id) - return null - }, - - async transform(code, id, options) { - options = { - ...options, - get ssr() { - warnFutureDeprecation(config, 'pluginHookSsrArgument') - return ssr - }, - } - const inMap = options?.inMap - const ctx = new TransformContext(id, code, inMap as SourceMap) - for (const plugin of getSortedPlugins('transform')) { - if (closed && environment?.options.dev.recoverable) - throwClosedServerError() - if (!plugin.transform) continue - ctx._activePlugin = plugin - ctx._activeId = id - ctx._activeCode = code - const start = debugPluginTransform ? performance.now() : 0 - let result: TransformResult | string | undefined - const handler = getHookHandler(plugin.transform) - try { - result = await handleHookPromise( - handler.call(ctx as any, code, id, options), - ) - } catch (e) { - ctx.error(e) - } - if (!result) continue - debugPluginTransform?.( - timeFrom(start), - plugin.name, - prettifyUrl(id, root), - ) - if (isObject(result)) { - if (result.code !== undefined) { - code = result.code - if (result.map) { - if (debugSourcemapCombine) { - // @ts-expect-error inject plugin name for debug purpose - result.map.name = plugin.name - } - ctx.sourcemapChain.push(result.map) - } - } - ctx._updateModuleInfo(id, result) } else { - code = result + combinedMap = sm } + } else { + combinedMap = combineSourcemaps(cleanUrl(this.filename), [ + m as RawSourceMap, + combinedMap as RawSourceMap, + ]) as SourceMap } - return { - code, - map: ctx._getCombinedSourcemap(), - } - }, + } + if (combinedMap !== this.combinedMap) { + this.combinedMap = combinedMap + this.sourcemapChain.length = 0 + } + return this.combinedMap as SourceMap + } - async watchChange(id, change) { - const ctx = new Context() - await hookParallel( - 'watchChange', - () => ctx, - () => [id, change], - ) - }, + getCombinedSourcemap(): SourceMap { + const map = this._getCombinedSourcemap() as SourceMap | { mappings: '' } + if (!map || (!('version' in map) && map.mappings === '')) { + return new MagicString(this.originalCode).generateMap({ + includeContent: true, + hires: 'boundary', + source: cleanUrl(this.filename), + }) + } + return map + } - async close() { - if (closed) return - closed = true - await Promise.allSettled(Array.from(processesing)) - const ctx = new Context() - await hookParallel( - 'buildEnd', - () => ctx, - () => [], - ) - await hookParallel( - 'closeBundle', - () => ctx, - () => [], - ) - }, + _updateActiveInfo(plugin: Plugin, id: string, code: string): void { + this._plugin = plugin + this._activeId = id + this._activeCode = code } +} - return container +export type { + EnvironmentPluginContainer, + TransformPluginContext, + TransformResult, } -// Backward compatiblity +// Backward compatibility +class PluginContainer { + constructor(private environments: Record) {} -export interface PluginContainer { - options: InputOptions - buildStart(options: InputOptions): Promise - resolveId( - id: string, + // 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 + private _getEnvironment(options?: { + ssr?: boolean + environment?: Environment + }) { + return options?.environment + ? options.environment + : this.environments?.[options?.ssr ? 'ssr' : 'client'] + } + + private _getPluginContainer(options?: { + ssr?: boolean + environment?: Environment + }) { + return (this._getEnvironment(options) as DevEnvironment).pluginContainer! + } + + get options(): InputOptions { + return (this.environments.client as DevEnvironment).pluginContainer!.options + } + + async buildStart(_options?: InputOptions): Promise { + ;(this.environments.client as DevEnvironment).pluginContainer!.buildStart( + _options, + ) + } + + async resolveId( + rawId: string, importer?: string, options?: { attributes?: Record custom?: CustomPluginOptions skip?: Set ssr?: boolean - environment?: Environment /** * @internal */ scan?: boolean isEntry?: boolean }, - ): Promise - transform( - code: string, + ): Promise { + return this._getPluginContainer(options).resolveId(rawId, importer, options) + } + + async load( id: string, options?: { - inMap?: SourceDescription['map'] ssr?: boolean - environment?: Environment }, - ): Promise<{ code: string; map: SourceMap | { mappings: '' } | null }> - load( + ): Promise { + return this._getPluginContainer(options).load(id, options) + } + + async transform( + code: string, id: string, options?: { ssr?: boolean environment?: Environment + inMap?: SourceDescription['map'] }, - ): Promise - watchChange( - id: string, - change: { event: 'create' | 'update' | 'delete' }, - ): Promise - close(): Promise + ): Promise<{ code: string; map: SourceMap | { mappings: '' } | null }> { + return this._getPluginContainer(options).transform(code, id, options) + } + + async watchChange( + _id: string, + _change: { event: 'create' | 'update' | 'delete' }, + ): Promise { + // noop, watchChange is already called for each environment + } + + async close(): Promise { + // noop, close will be called for each environment + } } /** * 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. + * which 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() { - ;(environments.client as DevEnvironment).pluginContainer!.buildStart({}) - }, - - 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 + return new PluginContainer(environments) } + +export type { PluginContainer } diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index c0e3091a51b184..3b39734f76db1c 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -9,7 +9,7 @@ import { promises as dns } from 'node:dns' import { performance } from 'node:perf_hooks' import type { AddressInfo, Server } from 'node:net' import fsp from 'node:fs/promises' -import type { FSWatcher } from 'chokidar' +import type { FSWatcher } from 'dep-types/chokidar' import remapping from '@ampproject/remapping' import type { DecodedSourceMap, RawSourceMap } from '@ampproject/remapping' import colors from 'picocolors' From 2b3329d4aa0b91a5920527a9712b82ae59becba3 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Wed, 29 May 2024 17:56:26 +0200 Subject: [PATCH 111/123] chore: fix build --- packages/vite/src/node/server/ws.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/vite/src/node/server/ws.ts b/packages/vite/src/node/server/ws.ts index 8989b999f3e03a..2688709c2903ba 100644 --- a/packages/vite/src/node/server/ws.ts +++ b/packages/vite/src/node/server/ws.ts @@ -88,7 +88,6 @@ export function createWebSocketServer( ): WebSocketServer { if (config.server.ws === false) { return { - name: 'ws', get clients() { return new Set() }, From 97bfcd2feb465d8aa55aeaccb8d00ac8b2cf7c2d Mon Sep 17 00:00:00 2001 From: patak <583075+patak-dev@users.noreply.github.com> Date: Mon, 3 Jun 2024 12:02:52 +0200 Subject: [PATCH 112/123] docs: update Co-authored-by: Igor Minar --- docs/guide/api-vite-environment.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/api-vite-environment.md b/docs/guide/api-vite-environment.md index 2d43ed983e9b89..74ed5644f688f4 100644 --- a/docs/guide/api-vite-environment.md +++ b/docs/guide/api-vite-environment.md @@ -11,7 +11,7 @@ Resources: Feel free to send us PRs against the `v6/environment-api` branch to fix the issues you discover. Please share with us your feedback as you test the proposal. ::: -A single Vite dev server can be used to interact with different module execution environments concurrently. We'll use the word environment to refer to a configured Vite processing pipeline that can resolve ids, load, and process source code and is connected to a runtime where the code is executed. The transformed source code is called a module, and the relationships between the modules processed in each environment are kept in a module graph. The code for these modules is sent to the runtimes associated with each environment to be executed. When a module is evaluated, the runtime will request its imported modules triggering the processing of a section of the module graph. In a typical Vite app, an environments will be used for the ES modules served to the client and for the server program that does SSR. An app can do SSR on a Node server, but also on an edge runtime like Workerd. So we can have different types of environments on the same Vite server: browser environments, node environments, and workerd environments, to name a few. +A single Vite dev server can be used to interact with different module execution environments concurrently. We'll use the word environment to refer to a configured Vite processing pipeline that can resolve ids, load, and process source code and is connected to a runtime where the code is executed. The transformed source code is called a module, and the relationships between the modules processed in each environment are kept in a module graph. The code for these modules is sent to the runtimes associated with each environment to be executed. When a module is evaluated, the runtime will request its imported modules triggering the processing of a section of the module graph. In a typical Vite app, an environments will be used for the ES modules served to the client and for the server program that does SSR. An app can do SSR in a Node server, but also other JS runtimes like [Cloudflare's workerd](https://github.com/cloudflare/workerd). So we can have different types of environments on the same Vite server: browser environments, node environments, and workerd environments, to name a few. A Vite Module Runner allows running any code by processing it with Vite plugins first. It is different from `server.ssrLoadModule` because the runner implementation is decoupled from the server. This allows library and framework authors to implement their layer of communication between the Vite server and the runner. The browser communicates with its corresponding environment using the server Web Socket and through HTTP requests. The Node Module runner can directly do function calls to process modules as it is running in the same process. Other environments could run modules connecting to an edge runtime like workerd, or a Worker Thread as Vitest does. From 169dd121543304ecdf47a4b0f24b54d8298c1377 Mon Sep 17 00:00:00 2001 From: patak <583075+patak-dev@users.noreply.github.com> Date: Mon, 3 Jun 2024 12:03:10 +0200 Subject: [PATCH 113/123] docs: update Co-authored-by: Igor Minar --- docs/guide/api-vite-environment.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/api-vite-environment.md b/docs/guide/api-vite-environment.md index 74ed5644f688f4..8312517129ae2b 100644 --- a/docs/guide/api-vite-environment.md +++ b/docs/guide/api-vite-environment.md @@ -13,7 +13,7 @@ Feel free to send us PRs against the `v6/environment-api` branch to fix the issu A single Vite dev server can be used to interact with different module execution environments concurrently. We'll use the word environment to refer to a configured Vite processing pipeline that can resolve ids, load, and process source code and is connected to a runtime where the code is executed. The transformed source code is called a module, and the relationships between the modules processed in each environment are kept in a module graph. The code for these modules is sent to the runtimes associated with each environment to be executed. When a module is evaluated, the runtime will request its imported modules triggering the processing of a section of the module graph. In a typical Vite app, an environments will be used for the ES modules served to the client and for the server program that does SSR. An app can do SSR in a Node server, but also other JS runtimes like [Cloudflare's workerd](https://github.com/cloudflare/workerd). So we can have different types of environments on the same Vite server: browser environments, node environments, and workerd environments, to name a few. -A Vite Module Runner allows running any code by processing it with Vite plugins first. It is different from `server.ssrLoadModule` because the runner implementation is decoupled from the server. This allows library and framework authors to implement their layer of communication between the Vite server and the runner. The browser communicates with its corresponding environment using the server Web Socket and through HTTP requests. The Node Module runner can directly do function calls to process modules as it is running in the same process. Other environments could run modules connecting to an edge runtime like workerd, or a Worker Thread as Vitest does. +A Vite Module Runner allows running any code by processing it with Vite plugins first. It is different from `server.ssrLoadModule` because the runner implementation is decoupled from the server. This allows library and framework authors to implement their layer of communication between the Vite server and the runner. The browser communicates with its corresponding environment using the server Web Socket and through HTTP requests. The Node Module runner can directly do function calls to process modules as it is running in the same process. Other environments could run modules connecting to a JS runtime like workerd, or a Worker Thread as Vitest does. All these environments share Vite's HTTP server, middlewares, and Web Socket. The resolved config and plugins pipeline are also shared, but plugins can use `apply` so its hooks are only called for certain environments. The environment can also be accessed inside hooks for fine-grained control. From 7fd9596b0b4a792e46d88dbebeb91b4a046c184b Mon Sep 17 00:00:00 2001 From: patak <583075+patak-dev@users.noreply.github.com> Date: Mon, 3 Jun 2024 12:03:57 +0200 Subject: [PATCH 114/123] docs: update Co-authored-by: Igor Minar --- docs/guide/api-vite-environment.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/api-vite-environment.md b/docs/guide/api-vite-environment.md index 8312517129ae2b..39ffa1e49e85ed 100644 --- a/docs/guide/api-vite-environment.md +++ b/docs/guide/api-vite-environment.md @@ -21,7 +21,7 @@ All these environments share Vite's HTTP server, middlewares, and Web Socket. Th ## Using environments in the Vite server -A Vite dev server exposes two environments by default: a Client environment and a SSR environment. The client environment is a browser environment by default, and the module runner is implemented by importing the virtual module `/@vite/client` to client apps. The SSR environment runs in the same Node runtime as the Vite server by default and allows application servers to be used to render requests during dev with full HMR support. We'll discuss later how frameworks and users can change the environment types for the default client and SSR environments, or register new environments (for example to have a separate module graph for RSC). +A Vite dev server exposes two environments by default: a `client` environment and an `ssr` environment. The client environment is a browser environment by default, and the module runner is implemented by importing the virtual module `/@vite/client` to client apps. The SSR environment runs in the same Node runtime as the Vite server by default and allows application servers to be used to render requests during dev with full HMR support. We'll discuss later how frameworks and users can change the environment types for the default client and SSR environments, or register new environments (for example to have a separate module graph for [RSC](https://react.dev/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023#react-server-components)). The available environments can be accessed using `server.environments`: From 630a75abd7374c7562677a16c18334e1062bad73 Mon Sep 17 00:00:00 2001 From: patak <583075+patak-dev@users.noreply.github.com> Date: Mon, 3 Jun 2024 12:04:47 +0200 Subject: [PATCH 115/123] docs: update Co-authored-by: Igor Minar --- docs/guide/api-vite-environment.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/api-vite-environment.md b/docs/guide/api-vite-environment.md index 39ffa1e49e85ed..4d4672a349bd17 100644 --- a/docs/guide/api-vite-environment.md +++ b/docs/guide/api-vite-environment.md @@ -119,7 +119,7 @@ We are using `transformRequest(url)` and `warmupRequest(url)` in the current ver The initial proposal had a `run` method that would allow consumers to invoke an import on the runner side by using the `transport` option. During our testing we found out that the API was not unversal enough to start recommending it. We are open to implement a built-in layer for remote SSR implementation based on the frameworks feedback. In the meantime, Vite still exposes a [`RunnerTransport` API](#runnertransport) to hide the complexity of the runner RPC. ::: -For the default Node environment, Vite creates a module runner that implements evaluation using `new AsyncFunction` running in the same runtime as the server. This runner is an instance of `ModuleRunner` that exposes: +For the `ssr` environment running in Node by default, Vite creates a module runner that implements evaluation using `new AsyncFunction` running in the same JS runtime as the dev server. This runner is an instance of `ModuleRunner` that exposes: ```ts class ModuleRunner { From ef87aa2ddf9e63ff420ee734306af6d5d67c29bb Mon Sep 17 00:00:00 2001 From: patak <583075+patak-dev@users.noreply.github.com> Date: Mon, 3 Jun 2024 12:57:35 +0200 Subject: [PATCH 116/123] docs: update Co-authored-by: Igor Minar --- docs/guide/api-vite-environment.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/api-vite-environment.md b/docs/guide/api-vite-environment.md index 4d4672a349bd17..66d39e7f13454f 100644 --- a/docs/guide/api-vite-environment.md +++ b/docs/guide/api-vite-environment.md @@ -352,7 +352,7 @@ export default { } ``` -Vite's user config also extends from a environment config, letting users add defaults for all environments at the root level. This is quite useful for the common use case of configuring a Vite client only app, that can be done without going through `environments.client`. +All environment configs extend from user's root config, allowing users add defaults for all environments at the root level. This is quite useful for the common use case of configuring a Vite client only app, that can be done without going through `environments.client`. ```js export default { From 1f163bf4212fee11fbb26c6764d29821924c348b Mon Sep 17 00:00:00 2001 From: patak <583075+patak-dev@users.noreply.github.com> Date: Mon, 3 Jun 2024 13:10:32 +0200 Subject: [PATCH 117/123] docs: update Co-authored-by: Igor Minar --- docs/guide/api-vite-environment.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/api-vite-environment.md b/docs/guide/api-vite-environment.md index 66d39e7f13454f..1e8046655669db 100644 --- a/docs/guide/api-vite-environment.md +++ b/docs/guide/api-vite-environment.md @@ -460,7 +460,7 @@ export default { } ``` -In this case we see how a Workerd environment can be set for both the default SSR environment and for a new custom RSC environment. +In this case we see how the `ssr` environment can be configured to use workerd as it's runtime. Additionally a new custom RSC environment is also defined, backed by a separate instance of the workerd runtime. ## Plugins and environments From 8649049c8199f0e2f46303540a821e4ee36e4ef8 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Mon, 3 Jun 2024 17:24:05 +0200 Subject: [PATCH 118/123] docs: update --- docs/guide/api-vite-environment.md | 6 +++--- packages/vite/src/node/server/mixedModuleGraph.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/guide/api-vite-environment.md b/docs/guide/api-vite-environment.md index 1e8046655669db..f48a5e51a42ab9 100644 --- a/docs/guide/api-vite-environment.md +++ b/docs/guide/api-vite-environment.md @@ -69,9 +69,9 @@ class DevEnvironment { * global scope are taken as defaults for all environments, and can * be overridden (resolve conditions, external, optimizedDeps) */ - config: ResolvedDevEnvironmentConfig + options: ResolvedDevEnvironmentOptions - constructor(name, config, { hot, options }: DevEnvironmentOptions) + constructor(name, config, { hot, options }: DevEnvironmentSetup) /** * Resolve the URL to an id, load it, and process the code using the @@ -371,7 +371,7 @@ interface EnvironmentOptions extends SharedEnvironmentOptions { } ``` -As we explained, the `UserConfig` interface extends from `EnvironmentConfig`. Environment specific options defined at the root level of user config are used for the default client environment. And environments can be configured explicitely using the `environments` array. The Client and SSR environments, are always present, even if an empty object is set to `environments`. +As we explained, Environment specific options defined at the root level of user config are used for the default client environment (the `UserConfig` interface extends from the `EnvironmentOptions` interface). And environments can be configured explicitly using the `environments` record. The `client` and `ssr` environments are always present during dev, even if an empty object is set to `environments`. This allows backward compatibility with `server.ssrLoadModule(url)` and `server.moduleGraph`. During build, the `client` environment is always present, and the `ssr` environment is only present if it is explicitly configured (using `environments.ssr` or for backward compatibility `build.ssr`). ```ts interface UserConfig extends EnvironmentOptions { diff --git a/packages/vite/src/node/server/mixedModuleGraph.ts b/packages/vite/src/node/server/mixedModuleGraph.ts index fb1c310667c8cc..9c06920aa794d7 100644 --- a/packages/vite/src/node/server/mixedModuleGraph.ts +++ b/packages/vite/src/node/server/mixedModuleGraph.ts @@ -7,7 +7,7 @@ import type { } from './moduleGraph' /** - * Backward compatible ModuleNode and ModuleGraph with mixed nodes from both the client and ssr enviornments + * Backward compatible ModuleNode and ModuleGraph with mixed nodes from both the client and ssr environments * 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. From 217b2f4cfaffcbdbfa49f02c398210bf974684c1 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Mon, 3 Jun 2024 17:47:50 +0200 Subject: [PATCH 119/123] docs: add more context before jumping into details --- docs/guide/api-vite-environment.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/guide/api-vite-environment.md b/docs/guide/api-vite-environment.md index f48a5e51a42ab9..a967172487a140 100644 --- a/docs/guide/api-vite-environment.md +++ b/docs/guide/api-vite-environment.md @@ -11,6 +11,14 @@ Resources: Feel free to send us PRs against the `v6/environment-api` branch to fix the issues you discover. Please share with us your feedback as you test the proposal. ::: +Vite 6 formalizes the concept of Environments, introducing new APIs to create and configure them as well as accessing options and context utilities with a consistent API. Since Vite 2, there were two implicit Environments (`client` and `ssr`). Plugin Hooks received a `ssr` boolean in the last options parameter to identify the target environment for each processed module. Several APIs expected an optional last `ssr` parameter to properly associate modules to the correct environment (for example `server.moduleGraph.getModuleByUrl(url, { ssr })`). The `ssr` environment was configured using `config.ssr` that had a partial set of the options present in the client environment. During dev, both `client` and `ssr` environment were running concurrently with a single shared plugin pipeline. During build, each build got a new resolved config instance with a new set of plugins. + +The new Environment API not only makes these two default environment explicit, but allows users to create as many named environments as needed. There is a uniform way to configure environments (using `config.environments`) and the environment options and context utilities associated to a module being processed is accessible in plugin hooks using `this.environment`. APIs that previously expected a `ssr` boolean are now scoped to the proper environment (for example `environment.moduleGraph.getModuleByUrl(url)`). During dev, all environments are run concurrently as before. During build, for backward compatibility each build gets its own resolved config instance. But plugins or users can opt-in into a shared build pipeline. + +Even if there are big changes internally, and new opt-in APIs, there are no breaking changes from Vite 5. The initial goal of Vite 6 will be to move the ecosystem to the new major as smoothly as possible, delaying promoting the adoption of new APIs in plugins until there is enough users ready to consume the new versions of these plugins. + +## Using environments in the Vite server + A single Vite dev server can be used to interact with different module execution environments concurrently. We'll use the word environment to refer to a configured Vite processing pipeline that can resolve ids, load, and process source code and is connected to a runtime where the code is executed. The transformed source code is called a module, and the relationships between the modules processed in each environment are kept in a module graph. The code for these modules is sent to the runtimes associated with each environment to be executed. When a module is evaluated, the runtime will request its imported modules triggering the processing of a section of the module graph. In a typical Vite app, an environments will be used for the ES modules served to the client and for the server program that does SSR. An app can do SSR in a Node server, but also other JS runtimes like [Cloudflare's workerd](https://github.com/cloudflare/workerd). So we can have different types of environments on the same Vite server: browser environments, node environments, and workerd environments, to name a few. A Vite Module Runner allows running any code by processing it with Vite plugins first. It is different from `server.ssrLoadModule` because the runner implementation is decoupled from the server. This allows library and framework authors to implement their layer of communication between the Vite server and the runner. The browser communicates with its corresponding environment using the server Web Socket and through HTTP requests. The Node Module runner can directly do function calls to process modules as it is running in the same process. Other environments could run modules connecting to a JS runtime like workerd, or a Worker Thread as Vitest does. @@ -19,8 +27,6 @@ All these environments share Vite's HTTP server, middlewares, and Web Socket. Th ![Vite Environments](../images/vite-environments.svg) -## Using environments in the Vite server - A Vite dev server exposes two environments by default: a `client` environment and an `ssr` environment. The client environment is a browser environment by default, and the module runner is implemented by importing the virtual module `/@vite/client` to client apps. The SSR environment runs in the same Node runtime as the Vite server by default and allows application servers to be used to render requests during dev with full HMR support. We'll discuss later how frameworks and users can change the environment types for the default client and SSR environments, or register new environments (for example to have a separate module graph for [RSC](https://react.dev/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023#react-server-components)). The available environments can be accessed using `server.environments`: @@ -362,7 +368,7 @@ export default { } ``` -The `EnvironmentOptions` interface exposes all the per-environment options. There are `SharedEnvironmentOptions` that apply to both `build` and `dev` environments, like `resolve`. And there are `DevOptions` and `BuildOptions` +The `EnvironmentOptions` interface exposes all the per-environment options. There are `SharedEnvironmentOptions` that apply to both `build` and `dev`, like `resolve`. And there are `DevEnvironmentOptions` and `BuildEnvironmentOptions` for dev and build specific options (like `dev.optimizeDeps` or `build.outDir`). ```ts interface EnvironmentOptions extends SharedEnvironmentOptions { From ec90bd3e311f2adf0cc3d557e6484cedc5057941 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Mon, 3 Jun 2024 17:55:23 +0200 Subject: [PATCH 120/123] docs: update --- docs/guide/api-vite-environment.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/guide/api-vite-environment.md b/docs/guide/api-vite-environment.md index a967172487a140..24f3f96d71e6a2 100644 --- a/docs/guide/api-vite-environment.md +++ b/docs/guide/api-vite-environment.md @@ -394,7 +394,7 @@ The `ssr` top level property has many options in common with `EnvironmentOptions ## Custom environment instances -To register a new dev or build environment, you can use a `create` function: +To create custom dev or build environment instances, you can use the `dev.createEnvironment` or `build.createEnvironment` functions. ```js export default { @@ -402,11 +402,13 @@ export default { rsc: { dev: { createEnvironment(name, config) { + // Called with 'rsc' and the resolved config during dev return createNodeDevEnvironment(name, config) } }, build: { createEnvironment(name, config) { + // Called with 'rsc' and the resolved config during build return createNodeBuildEnvironment(name, config) } outDir: '/dist/rsc', @@ -418,7 +420,7 @@ export default { The environment will be accessible in middlewares or plugin hooks through `server.environments`. In plugin hooks, the environment instance is passed in the options so they can do conditions depending on the way they are configured. -Environment providers like Workerd, can expose an environment configurator for the most common case of using the same runtime for both dev and build environments. The default environment options can also be set so the user doesn't need to do it. +Environment providers like Workerd, can expose an environment provider for the most common case of using the same runtime for both dev and build environments. The default environment options can also be set so the user doesn't need to do it. ```js function createWorkedEnvironment(userConfig) { From 1e07003c65187c6ef7e3115f296908a5bd47aec8 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Mon, 3 Jun 2024 19:15:04 +0200 Subject: [PATCH 121/123] docs: typo --- docs/guide/api-vite-environment.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/guide/api-vite-environment.md b/docs/guide/api-vite-environment.md index 24f3f96d71e6a2..775b681250d9d3 100644 --- a/docs/guide/api-vite-environment.md +++ b/docs/guide/api-vite-environment.md @@ -438,7 +438,7 @@ function createWorkedEnvironment(userConfig) { }, build: { createEnvironment(name, config) { - return createWorkerdBuildEnvironment(builder, name) + return createWorkerdBuildEnvironment(name, config) }, }, }, @@ -447,7 +447,7 @@ function createWorkedEnvironment(userConfig) { } ``` -Then the config file can be writen as +Then the config file can be written as ```js import { workerdEnvironment } from 'vite-environment-workerd' @@ -501,7 +501,7 @@ An empty object is enough to register the environment, default values from the r ### Configuring environment using hooks -While the `config` hook is running, the complete list of environments isn't yet known and the environments can be affected by both the default values from the root level environment config or explicitely through the `config.environments` record. +While the `config` hook is running, the complete list of environments isn't yet known and the environments can be affected by both the default values from the root level environment config or explicitly through the `config.environments` record. Plugins should set default values using the `config` hook. To configure each environment, they can use the new `configEnvironment` hook. This hook is called for each environment with its partially resolved config including resolution of final defaults. ```ts From 3635ed8d6061d762e0daa7781c2656beda737fb6 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Tue, 4 Jun 2024 20:54:16 +0200 Subject: [PATCH 122/123] feat: expose createIdResolver --- packages/vite/src/node/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index 3d0a753e587ec1..ef1826656fc1a5 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -13,6 +13,7 @@ export { preview } from './preview' export { build, createBuilder } from './build' export { optimizeDeps } from './optimizer' +export { createIdResolver } from './idResolver' export { formatPostcssSourceMap, preprocessCSS } from './plugins/css' export { transformWithEsbuild } from './plugins/esbuild' From bbca0b402959ceeb71e08588808fab045f58b887 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Tue, 4 Jun 2024 20:55:51 +0200 Subject: [PATCH 123/123] release: v6.0.0-alpha.18 --- packages/vite/CHANGELOG.md | 185 +++++++++++++++++++++++++++++++++++++ packages/vite/package.json | 2 +- 2 files changed, 186 insertions(+), 1 deletion(-) diff --git a/packages/vite/CHANGELOG.md b/packages/vite/CHANGELOG.md index cf0978d3114c24..693cf25ed9f063 100644 --- a/packages/vite/CHANGELOG.md +++ b/packages/vite/CHANGELOG.md @@ -1,3 +1,188 @@ +## 6.0.0-alpha.18 (2024-06-04) + +* feat: expose createIdResolver ([3635ed8](https://github.com/vitejs/vite/commit/3635ed8)) +* feat: non-nullable this.environment (#17302) ([bdd1b91](https://github.com/vitejs/vite/commit/bdd1b91)), closes [#17302](https://github.com/vitejs/vite/issues/17302) +* feat(types): `defineVitePlugin` utils (#17298) ([398a49c](https://github.com/vitejs/vite/commit/398a49c)), closes [#17298](https://github.com/vitejs/vite/issues/17298) +* feat(typescript): update tsconfck to add support for `${configDir}` replacement in ts 5.5 (#17350) ([4835e2b](https://github.com/vitejs/vite/commit/4835e2b)), closes [#17350](https://github.com/vitejs/vite/issues/17350) +* docs: assign code for each deprecation changes, provide option to opt-in (#17305) ([d8aa74c](https://github.com/vitejs/vite/commit/d8aa74c)), closes [#17305](https://github.com/vitejs/vite/issues/17305) +* docs: update ([8649049](https://github.com/vitejs/vite/commit/8649049)) +* chore: add region comment (#17370) ([a8c7083](https://github.com/vitejs/vite/commit/a8c7083)), closes [#17370](https://github.com/vitejs/vite/issues/17370) +* chore: cleanup ([28e22c6](https://github.com/vitejs/vite/commit/28e22c6)) +* chore: fix build ([2b3329d](https://github.com/vitejs/vite/commit/2b3329d)) +* chore: ignore test-dts in tsconfig ([1af7081](https://github.com/vitejs/vite/commit/1af7081)) +* chore: prettier ([c373251](https://github.com/vitejs/vite/commit/c373251)) +* chore: refactor code style ([ca940e8](https://github.com/vitejs/vite/commit/ca940e8)) +* chore(deps): update all non-major dependencies (#17373) ([f2d52f1](https://github.com/vitejs/vite/commit/f2d52f1)), closes [#17373](https://github.com/vitejs/vite/issues/17373) +* fix: check last invalidation time ([e91e269](https://github.com/vitejs/vite/commit/e91e269)) +* fix: check lastHMRTimestamp for entries in moduleRunner ([611ce5c](https://github.com/vitejs/vite/commit/611ce5c)) +* fix: correctly invalidate module in the module runner if it was invalidated on the server ([fd46a2a](https://github.com/vitejs/vite/commit/fd46a2a)) +* fix: default value of `depOptimization` ([da354ea](https://github.com/vitejs/vite/commit/da354ea)) +* fix: do not invalidate modules in the runner if not invalidated on the server ([a63263c](https://github.com/vitejs/vite/commit/a63263c)) +* fix: improve invalidation sync in the module runner ([03bc590](https://github.com/vitejs/vite/commit/03bc590)) +* fix: lastHMRTimestamp compat ([909fe28](https://github.com/vitejs/vite/commit/909fe28)) +* fix: missing HMRPayload export ([b7cbd3d](https://github.com/vitejs/vite/commit/b7cbd3d)) +* fix(css): handle lightningcss minification in Deno (#17372) ([b3f5bd1](https://github.com/vitejs/vite/commit/b3f5bd1)), closes [#17372](https://github.com/vitejs/vite/issues/17372) +* fix(css): handle url replacing when preprocessing with lightningcss (#17364) ([6fbb5e0](https://github.com/vitejs/vite/commit/6fbb5e0)), closes [#17364](https://github.com/vitejs/vite/issues/17364) +* refactor: plugin container (#17288) ([909782f](https://github.com/vitejs/vite/commit/909782f)), closes [#17288](https://github.com/vitejs/vite/issues/17288) +* refactor: reporter using custom per env api (#17297) ([c43c987](https://github.com/vitejs/vite/commit/c43c987)), closes [#17297](https://github.com/vitejs/vite/issues/17297) +* test(v6): test ssrLoadModule and virtual module invalidation (#17313) ([7d78761](https://github.com/vitejs/vite/commit/7d78761)), closes [#17313](https://github.com/vitejs/vite/issues/17313) + + + +## 6.0.0-alpha.17 (2024-05-24) + +* release: v6.0.0-alpha.17 ([e45881c](https://github.com/vitejs/vite/commit/e45881c)) +* refactor!: rename to HMRPayload to HotPayload and remove HMRBroadcaster (#16875) ([87bbb04](https://github.com/vitejs/vite/commit/87bbb04)), closes [#16875](https://github.com/vitejs/vite/issues/16875) +* fix: dep optimization options ([624e751](https://github.com/vitejs/vite/commit/624e751)) +* fix: DepOptimizationConfig ([5c655e6](https://github.com/vitejs/vite/commit/5c655e6)) +* fix: handleHotUpdate compat (#17295) ([11bddb0](https://github.com/vitejs/vite/commit/11bddb0)), closes [#17295](https://github.com/vitejs/vite/issues/17295) +* fix: optimizeDeps back compat ([478a9aa](https://github.com/vitejs/vite/commit/478a9aa)) +* fix(types): avoid referencing `WeakKey` type ([e339959](https://github.com/vitejs/vite/commit/e339959)) +* feat: per-environment optimizeDeps entries and force ([006cfb7](https://github.com/vitejs/vite/commit/006cfb7)) +* feat: reporter as shared plugin using createWeakData (#17293) ([2b69389](https://github.com/vitejs/vite/commit/2b69389)), closes [#17293](https://github.com/vitejs/vite/issues/17293) +* chore: refactor isDepsOptimizerEnabled ([5f36aa6](https://github.com/vitejs/vite/commit/5f36aa6)) + + + +## 6.0.0-alpha.16 (2024-05-23) + +* release: v6.0.0-alpha.16 ([c7d02ac](https://github.com/vitejs/vite/commit/c7d02ac)) +* feat: createWeakData ([bcb1959](https://github.com/vitejs/vite/commit/bcb1959)) +* feat: improve Plugin option types ([21225c9](https://github.com/vitejs/vite/commit/21225c9)) +* feat: only shared plugins (#17289) ([e36f2f3](https://github.com/vitejs/vite/commit/e36f2f3)), closes [#17289](https://github.com/vitejs/vite/issues/17289) +* feat: provide `environment` in every hook context ([53734a8](https://github.com/vitejs/vite/commit/53734a8)) +* feat(types): expose PluginEnvironment type ([4d03124](https://github.com/vitejs/vite/commit/4d03124)) +* fix: argument rollup types for plugin context instead of wrapping ([89ec69c](https://github.com/vitejs/vite/commit/89ec69c)) +* fix: avoid duplicating values in shared optimizeDeps config (#16737) ([a3ee7f5](https://github.com/vitejs/vite/commit/a3ee7f5)), closes [#16737](https://github.com/vitejs/vite/issues/16737) +* fix: back compat for server.pluginContainer.buildStart ([46f21b8](https://github.com/vitejs/vite/commit/46f21b8)) +* fix: backcompat with config.optimizeDeps ([778e39d](https://github.com/vitejs/vite/commit/778e39d)) +* fix: keep plugin with environmentPlugins hook ([63de43a](https://github.com/vitejs/vite/commit/63de43a)) +* chore: remove extra symbol ([431455f](https://github.com/vitejs/vite/commit/431455f)) +* chore: rename to BaseEnvironment (#16797) ([d86553a](https://github.com/vitejs/vite/commit/d86553a)), closes [#16797](https://github.com/vitejs/vite/issues/16797) + + + +## 6.0.0-alpha.15 (2024-05-20) + +* release: v6.0.0-alpha.15 ([d231966](https://github.com/vitejs/vite/commit/d231966)) +* fix: only join base url in none ssr env ([7e9dd25](https://github.com/vitejs/vite/commit/7e9dd25)) +* fix: svelte back compat ([0166bfe](https://github.com/vitejs/vite/commit/0166bfe)) +* fix hmr connected message on client ([acb208e](https://github.com/vitejs/vite/commit/acb208e)) + + + +## 6.0.0-alpha.14 (2024-05-20) + +* release: v6.0.0-alpha.14 ([2b2d010](https://github.com/vitejs/vite/commit/2b2d010)) +* feat: back to environmentPlugins hook (#16732) ([54c219e](https://github.com/vitejs/vite/commit/54c219e)), closes [#16732](https://github.com/vitejs/vite/issues/16732) +* fix: export isFileLoadingAllowed ([34d8518](https://github.com/vitejs/vite/commit/34d8518)) +* fix: rely on the current environment in resolveId, if known ([8ee7fcd](https://github.com/vitejs/vite/commit/8ee7fcd)) +* fix: revert resolveId importer require ([3546d1e](https://github.com/vitejs/vite/commit/3546d1e)) + + + +## 6.0.0-alpha.13 (2024-05-20) + +* release: v6.0.0-alpha.13 ([2ae2fca](https://github.com/vitejs/vite/commit/2ae2fca)) +* fix: types ([82111bf](https://github.com/vitejs/vite/commit/82111bf)) + + + +## 6.0.0-alpha.12 (2024-05-20) + +* release: v6.0.0-alpha.12 ([371f7a0](https://github.com/vitejs/vite/commit/371f7a0)) +* chore: export ssrTransform ([0b3cf69](https://github.com/vitejs/vite/commit/0b3cf69)) +* chore: respect ssr flag in clientInjectsions transform ([8c5674e](https://github.com/vitejs/vite/commit/8c5674e)) +* fix: remove enforce inside IsolatedPlugins ([a5efd56](https://github.com/vitejs/vite/commit/a5efd56)) + + + +## 6.0.0-alpha.11 (2024-05-04) + +* release: v6.0.0-alpha.11 ([8851d9d](https://github.com/vitejs/vite/commit/8851d9d)) +* refactor: BoundedPlugin -> IsolatedPlugin ([1ec07a4](https://github.com/vitejs/vite/commit/1ec07a4)) + + + +## 6.0.0-alpha.10 (2024-05-02) + +* release: v6.0.0-alpha.10 ([57871a4](https://github.com/vitejs/vite/commit/57871a4)) +* feat: sharedDuringBuild for bounded plugins ([d213865](https://github.com/vitejs/vite/commit/d213865)) + + + +## 6.0.0-alpha.9 (2024-05-01) + +* release: v6.0.0-alpha.9 ([2e6abb3](https://github.com/vitejs/vite/commit/2e6abb3)) +* fix: lint ([848af54](https://github.com/vitejs/vite/commit/848af54)) +* fix: module-runner bundle issue with chokidar dep ([b8ef8ff](https://github.com/vitejs/vite/commit/b8ef8ff)) +* refactor: bounded plugins factory instead of create hook ([6b74221](https://github.com/vitejs/vite/commit/6b74221)) +* refactor: move reload on tsconfig change to the server ([1bcb67d](https://github.com/vitejs/vite/commit/1bcb67d)) + + + +## 6.0.0-alpha.8 (2024-04-30) + +* release: v6.0.0-alpha.8 ([1d9caef](https://github.com/vitejs/vite/commit/1d9caef)) +* fix: keep plugins with create hook ([12d467f](https://github.com/vitejs/vite/commit/12d467f)) +* refactor: environment hooks guard ([527621e](https://github.com/vitejs/vite/commit/527621e)) +* feat: define and html plugins ([7566aae](https://github.com/vitejs/vite/commit/7566aae)) + + + +## 6.0.0-alpha.7 (2024-04-29) + +* release: v6.0.0-alpha.7 ([05943cf](https://github.com/vitejs/vite/commit/05943cf)) +* feat: remove config.build from dynamicImportVars plugin ([8231283](https://github.com/vitejs/vite/commit/8231283)) +* feat: this.environment in buildStart, rework more internal plugins ([bda0dc5](https://github.com/vitejs/vite/commit/bda0dc5)) +* feat: this.environment in renderChunk and generateBundle ([a6fc1dd](https://github.com/vitejs/vite/commit/a6fc1dd)) + + + +## 6.0.0-alpha.6 (2024-04-28) + +* release: v6.0.0-alpha.6 ([e8473a6](https://github.com/vitejs/vite/commit/e8473a6)) +* fix: custom environment preload injection (#16541) ([00079da](https://github.com/vitejs/vite/commit/00079da)), closes [#16541](https://github.com/vitejs/vite/issues/16541) + + + +## 6.0.0-alpha.5 (2024-04-26) + +* release: v6.0.0-alpha.5 ([a8adcac](https://github.com/vitejs/vite/commit/a8adcac)) +* fix: use environment.plugins during build, support create hook ([a44810a](https://github.com/vitejs/vite/commit/a44810a)) + + + +## 6.0.0-alpha.4 (2024-04-26) + +* release: v6.0.0-alpha.4 ([e6fa9d9](https://github.com/vitejs/vite/commit/e6fa9d9)) +* fix: backcompat merging ([294a84e](https://github.com/vitejs/vite/commit/294a84e)) +* fix: disable hmr for ssrLoadModule ([33ef0fb](https://github.com/vitejs/vite/commit/33ef0fb)) +* fix: rework backcompat patching of environment config ([72150d6](https://github.com/vitejs/vite/commit/72150d6)) +* fix: sharedPlugins ([a63190c](https://github.com/vitejs/vite/commit/a63190c)) +* fix(cli): -> ([4cf322d](https://github.com/vitejs/vite/commit/4cf322d)) +* fix(v6): fix `ssrEmitAssets` compat (#16480) ([5c5efe4](https://github.com/vitejs/vite/commit/5c5efe4)), closes [#16480](https://github.com/vitejs/vite/issues/16480) +* feat: builder.entireApp, remove --environment ([1925eeb](https://github.com/vitejs/vite/commit/1925eeb)) +* feat: opt-in shared plugins during build ([2866d4f](https://github.com/vitejs/vite/commit/2866d4f)) +* feat: use environment.logger in buildEnvironment ([8843221](https://github.com/vitejs/vite/commit/8843221)) +* refactor: align createBuilder params with createServer ([62921e6](https://github.com/vitejs/vite/commit/62921e6)) +* refactor: build --app ([29dd26e](https://github.com/vitejs/vite/commit/29dd26e)) +* refactor: remove server from createServerModuleRunner ([9d6a152](https://github.com/vitejs/vite/commit/9d6a152)) +* refactor: rename createViteBuilder to createBuilder, align with createServer ([4dc2a75](https://github.com/vitejs/vite/commit/4dc2a75)) +* chore: remove the deprecation notice from ssrLoadModule for now ([bf65476](https://github.com/vitejs/vite/commit/bf65476)) + + + +## 6.0.0-alpha.3 (2024-04-20) + +* release: v6.0.0-alpha.3 ([635aad5](https://github.com/vitejs/vite/commit/635aad5)) +* fix: export missing types ([431cd4b](https://github.com/vitejs/vite/commit/431cd4b)) +* chore: remove configureServer from more plugins ([ad2b0bf](https://github.com/vitejs/vite/commit/ad2b0bf)) +* chore: rename plugin.split to plugin.create ([5f6b62f](https://github.com/vitejs/vite/commit/5f6b62f)) +* feat: environment api (#16129) ([f684d4c](https://github.com/vitejs/vite/commit/f684d4c)), closes [#16129](https://github.com/vitejs/vite/issues/16129) + + + ## 6.0.0-alpha.17 (2024-05-24) * refactor!: rename to HMRPayload to HotPayload and remove HMRBroadcaster (#16875) ([87bbb04](https://github.com/vitejs/vite/commit/87bbb04)), closes [#16875](https://github.com/vitejs/vite/issues/16875) diff --git a/packages/vite/package.json b/packages/vite/package.json index d5a60a2bcf878b..21a941bd741334 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -1,6 +1,6 @@ { "name": "vite", - "version": "6.0.0-alpha.17", + "version": "6.0.0-alpha.18", "type": "module", "license": "MIT", "author": "Evan You",