diff --git a/docs/config/index.md b/docs/config/index.md index 0583f69bd52d13..3dac90dc386d06 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -22,6 +22,10 @@ You can also explicitly specify a config file to use with the `--config` CLI opt vite --config my-config.js ``` +::: tip BUNDLING THE CONFIG +By default, Vite uses `esbuild` to bundle the config into a temporary file. This can cause issues when importing TypeScript files in a monorepo. If you encounter any issues with this approach, you can specify `--configLoader=runner` to use the module runner instead - it will not create a temporary config and will transform any files on the fly. Note that module runner doesn't support CJS in config files, but external CJS packages should work as usual. +::: + ## Config Intellisense Since Vite ships with TypeScript typings, you can leverage your IDE's intellisense with jsdoc type hints: diff --git a/docs/config/preview-options.md b/docs/config/preview-options.md index 0202d77f963b87..79b2a90542c81a 100644 --- a/docs/config/preview-options.md +++ b/docs/config/preview-options.md @@ -19,6 +19,15 @@ See [`server.host`](./server-options#server-host) for more details. ::: +## preview.allowedHosts + +- **Type:** `string | true` +- **Default:** [`server.allowedHosts`](./server-options#server-allowedhosts) + +The hostnames that Vite is allowed to respond to. + +See [`server.allowedHosts`](./server-options#server-allowedhosts) for more details. + ## preview.port - **Type:** `number` @@ -78,7 +87,9 @@ Uses [`http-proxy`](https://github.com/http-party/node-http-proxy). Full options - **Type:** `boolean | CorsOptions` - **Default:** [`server.cors`](./server-options#server-cors) -Configure CORS for the preview server. This is enabled by default and allows any origin. Pass an [options object](https://github.com/expressjs/cors#configuration-options) to fine tune the behavior or `false` to disable. +Configure CORS for the preview server. + +See [`server.cors`](./server-options#server-cors) for more details. ## preview.headers diff --git a/docs/config/server-options.md b/docs/config/server-options.md index 9aa26b1aca79af..a4031d6828e584 100644 --- a/docs/config/server-options.md +++ b/docs/config/server-options.md @@ -42,6 +42,20 @@ See [the WSL document](https://learn.microsoft.com/en-us/windows/wsl/networking# ::: +## server.allowedHosts + +- **Type:** `string[] | true` +- **Default:** `[]` + +The hostnames that Vite is allowed to respond to. +`localhost` and domains under `.localhost` and all IP addresses are allowed by default. +When using HTTPS, this check is skipped. + +If a string starts with `.`, it will allow that hostname without the `.` and all subdomains under the hostname. For example, `.example.com` will allow `example.com`, `foo.example.com`, and `foo.bar.example.com`. + +If set to `true`, the server is allowed to respond to requests for any hosts. +This is not recommended as it will be vulnerable to DNS rebinding attacks. + ## server.port - **Type:** `number` @@ -147,8 +161,15 @@ export default defineConfig({ ## server.cors - **Type:** `boolean | CorsOptions` +- **Default:** `{ origin: /^https?:\/\/(?:(?:[^:]+\.)?localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/ }` (allows localhost, `127.0.0.1` and `::1`) -Configure CORS for the dev server. This is enabled by default and allows any origin. Pass an [options object](https://github.com/expressjs/cors#configuration-options) to fine tune the behavior or `false` to disable. +Configure CORS for the dev server. Pass an [options object](https://github.com/expressjs/cors#configuration-options) to fine tune the behavior or `true` to allow any origin. + +:::warning + +We recommend setting a specific value rather than `true` to avoid exposing the source code to untrusted origins. + +::: ## server.headers diff --git a/docs/guide/api-environment-frameworks.md b/docs/guide/api-environment-frameworks.md index 6c16d934486ddd..00493c0c844129 100644 --- a/docs/guide/api-environment-frameworks.md +++ b/docs/guide/api-environment-frameworks.md @@ -46,8 +46,13 @@ The `runner` is evaluated eagerly when it's accessed for the first time. Beware 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 fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' import { createServer } from 'vite' +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + const server = await createServer({ server: { middlewareMode: true }, appType: 'custom', diff --git a/docs/guide/backend-integration.md b/docs/guide/backend-integration.md index 5f3cc938774530..34319601f94b67 100644 --- a/docs/guide/backend-integration.md +++ b/docs/guide/backend-integration.md @@ -12,6 +12,12 @@ If you need a custom integration, you can follow the steps in this guide to conf import { defineConfig } from 'vite' // ---cut--- export default defineConfig({ + server: { + cors: { + // the origin you will be accessing via browser + origin: 'http://my-backend.example.com', + }, + }, build: { // generate .vite/manifest.json in outDir manifest: true, diff --git a/docs/guide/build.md b/docs/guide/build.md index e08ad32b247f72..2bfe05314112b9 100644 --- a/docs/guide/build.md +++ b/docs/guide/build.md @@ -4,14 +4,19 @@ When it is time to deploy your app for production, simply run the `vite build` c ## Browser Compatibility -The production bundle assumes support for modern JavaScript. By default, Vite targets browsers which support the [native ES Modules](https://caniuse.com/es6-module), [native ESM dynamic import](https://caniuse.com/es6-module-dynamic-import), and [`import.meta`](https://caniuse.com/mdn-javascript_operators_import_meta): +By default, the production bundle assumes support for modern JavaScript, including [native ES Modules](https://caniuse.com/es6-module), [native ESM dynamic import](https://caniuse.com/es6-module-dynamic-import), and [`import.meta`](https://caniuse.com/mdn-javascript_operators_import_meta). The default browser support range is: - Chrome >=87 - Firefox >=78 - Safari >=14 - Edge >=88 -You can specify custom targets via the [`build.target` config option](/config/build-options.md#build-target), where the lowest target is `es2015`. +You can specify custom targets via the [`build.target` config option](/config/build-options.md#build-target), where the lowest target is `es2015`. If a lower target is set, Vite will still require these minimum browser support ranges as it relies on [native ESM dynamic import](https://caniuse.com/es6-module-dynamic-import) and [`import.meta`](https://caniuse.com/mdn-javascript_operators_import_meta): + +- Chrome >=64 +- Firefox >=67 +- Safari >=11.1 +- Edge >=79 Note that by default, Vite only handles syntax transforms and **does not cover polyfills**. You can check out https://cdnjs.cloudflare.com/polyfill/ which automatically generates polyfill bundles based on the user's browser UserAgent string. @@ -106,9 +111,12 @@ During dev, simply navigate or link to `/nested/` - it works as expected, just l During build, all you need to do is to specify multiple `.html` files as entry points: ```js twoslash [vite.config.js] -import { resolve } from 'path' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' import { defineConfig } from 'vite' +const __dirname = dirname(fileURLToPath(import.meta.url)) + export default defineConfig({ build: { rollupOptions: { @@ -134,9 +142,12 @@ When it is time to bundle your library for distribution, use the [`build.lib` co ::: code-group ```js twoslash [vite.config.js (single entry)] -import { resolve } from 'path' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' import { defineConfig } from 'vite' +const __dirname = dirname(fileURLToPath(import.meta.url)) + export default defineConfig({ build: { lib: { @@ -162,9 +173,12 @@ export default defineConfig({ ``` ```js twoslash [vite.config.js (multiple entries)] -import { resolve } from 'path' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' import { defineConfig } from 'vite' +const __dirname = dirname(fileURLToPath(import.meta.url)) + export default defineConfig({ build: { lib: { diff --git a/docs/guide/cli.md b/docs/guide/cli.md index eb3254be98b80a..faaeefa18cb20f 100644 --- a/docs/guide/cli.md +++ b/docs/guide/cli.md @@ -14,24 +14,25 @@ vite [root] #### Options -| Options | | -| ------------------------ | ------------------------------------------------------------------------------------------------------------------ | -| `--host [host]` | Specify hostname (`string`) | -| `--port ` | Specify port (`number`) | -| `--open [path]` | Open browser on startup (`boolean \| string`) | -| `--cors` | Enable CORS (`boolean`) | -| `--strictPort` | Exit if specified port is already in use (`boolean`) | -| `--force` | Force the optimizer to ignore the cache and re-bundle (`boolean`) | -| `-c, --config ` | Use specified config file (`string`) | -| `--base ` | Public base path (default: `/`) (`string`) | -| `-l, --logLevel ` | info \| warn \| error \| silent (`string`) | -| `--clearScreen` | Allow/disable clear screen when logging (`boolean`) | -| `--profile` | Start built-in Node.js inspector (check [Performance bottlenecks](/guide/troubleshooting#performance-bottlenecks)) | -| `-d, --debug [feat]` | Show debug logs (`string \| boolean`) | -| `-f, --filter ` | Filter debug logs (`string`) | -| `-m, --mode ` | Set env mode (`string`) | -| `-h, --help` | Display available CLI options | -| `-v, --version` | Display version number | +| Options | | +| ------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `--host [host]` | Specify hostname (`string`) | +| `--port ` | Specify port (`number`) | +| `--open [path]` | Open browser on startup (`boolean \| string`) | +| `--cors` | Enable CORS (`boolean`) | +| `--strictPort` | Exit if specified port is already in use (`boolean`) | +| `--force` | Force the optimizer to ignore the cache and re-bundle (`boolean`) | +| `-c, --config ` | Use specified config file (`string`) | +| `--base ` | Public base path (default: `/`) (`string`) | +| `-l, --logLevel ` | info \| warn \| error \| silent (`string`) | +| `--clearScreen` | Allow/disable clear screen when logging (`boolean`) | +| `--configLoader ` | Use `bundle` to bundle the config with esbuild or `runner` (experimental) to process it on the fly (default: `bundle`) | +| `--profile` | Start built-in Node.js inspector (check [Performance bottlenecks](/guide/troubleshooting#performance-bottlenecks)) | +| `-d, --debug [feat]` | Show debug logs (`string \| boolean`) | +| `-f, --filter ` | Filter debug logs (`string`) | +| `-m, --mode ` | Set env mode (`string`) | +| `-h, --help` | Display available CLI options | +| `-v, --version` | Display version number | ## Build @@ -47,29 +48,30 @@ vite build [root] #### Options -| Options | | -| ------------------------------ | ------------------------------------------------------------------------------------------------------------------- | -| `--target ` | Transpile target (default: `"modules"`) (`string`) | -| `--outDir ` | Output directory (default: `dist`) (`string`) | -| `--assetsDir ` | Directory under outDir to place assets in (default: `"assets"`) (`string`) | -| `--assetsInlineLimit ` | Static asset base64 inline threshold in bytes (default: `4096`) (`number`) | -| `--ssr [entry]` | Build specified entry for server-side rendering (`string`) | -| `--sourcemap [output]` | Output source maps for build (default: `false`) (`boolean \| "inline" \| "hidden"`) | -| `--minify [minifier]` | Enable/disable minification, or specify minifier to use (default: `"esbuild"`) (`boolean \| "terser" \| "esbuild"`) | -| `--manifest [name]` | Emit build manifest json (`boolean \| string`) | -| `--ssrManifest [name]` | Emit ssr manifest json (`boolean \| string`) | -| `--emptyOutDir` | Force empty outDir when it's outside of root (`boolean`) | -| `-w, --watch` | Rebuilds when modules have changed on disk (`boolean`) | -| `-c, --config ` | Use specified config file (`string`) | -| `--base ` | Public base path (default: `/`) (`string`) | -| `-l, --logLevel ` | Info \| warn \| error \| silent (`string`) | -| `--clearScreen` | Allow/disable clear screen when logging (`boolean`) | -| `--profile` | Start built-in Node.js inspector (check [Performance bottlenecks](/guide/troubleshooting#performance-bottlenecks)) | -| `-d, --debug [feat]` | Show debug logs (`string \| boolean`) | -| `-f, --filter ` | Filter debug logs (`string`) | -| `-m, --mode ` | Set env mode (`string`) | -| `-h, --help` | Display available CLI options | -| `--app` | Build all environments, same as `builder: {}` (`boolean`, experimental) | +| Options | | +| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | +| `--target ` | Transpile target (default: `"modules"`) (`string`) | +| `--outDir ` | Output directory (default: `dist`) (`string`) | +| `--assetsDir ` | Directory under outDir to place assets in (default: `"assets"`) (`string`) | +| `--assetsInlineLimit ` | Static asset base64 inline threshold in bytes (default: `4096`) (`number`) | +| `--ssr [entry]` | Build specified entry for server-side rendering (`string`) | +| `--sourcemap [output]` | Output source maps for build (default: `false`) (`boolean \| "inline" \| "hidden"`) | +| `--minify [minifier]` | Enable/disable minification, or specify minifier to use (default: `"esbuild"`) (`boolean \| "terser" \| "esbuild"`) | +| `--manifest [name]` | Emit build manifest json (`boolean \| string`) | +| `--ssrManifest [name]` | Emit ssr manifest json (`boolean \| string`) | +| `--emptyOutDir` | Force empty outDir when it's outside of root (`boolean`) | +| `-w, --watch` | Rebuilds when modules have changed on disk (`boolean`) | +| `-c, --config ` | Use specified config file (`string`) | +| `--base ` | Public base path (default: `/`) (`string`) | +| `-l, --logLevel ` | Info \| warn \| error \| silent (`string`) | +| `--clearScreen` | Allow/disable clear screen when logging (`boolean`) | +| `--configLoader ` | Use `bundle` to bundle the config with esbuild or `runner` (experimental) to process it on the fly (default: `bundle`) | +| `--profile` | Start built-in Node.js inspector (check [Performance bottlenecks](/guide/troubleshooting#performance-bottlenecks)) | +| `-d, --debug [feat]` | Show debug logs (`string \| boolean`) | +| `-f, --filter ` | Filter debug logs (`string`) | +| `-m, --mode ` | Set env mode (`string`) | +| `-h, --help` | Display available CLI options | +| `--app` | Build all environments, same as `builder: {}` (`boolean`, experimental) | ## Others @@ -85,17 +87,18 @@ vite optimize [root] #### Options -| Options | | -| ------------------------ | ----------------------------------------------------------------- | -| `--force` | Force the optimizer to ignore the cache and re-bundle (`boolean`) | -| `-c, --config ` | Use specified config file (`string`) | -| `--base ` | Public base path (default: `/`) (`string`) | -| `-l, --logLevel ` | Info \| warn \| error \| silent (`string`) | -| `--clearScreen` | Allow/disable clear screen when logging (`boolean`) | -| `-d, --debug [feat]` | Show debug logs (`string \| boolean`) | -| `-f, --filter ` | Filter debug logs (`string`) | -| `-m, --mode ` | Set env mode (`string`) | -| `-h, --help` | Display available CLI options | +| Options | | +| ------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `--force` | Force the optimizer to ignore the cache and re-bundle (`boolean`) | +| `-c, --config ` | Use specified config file (`string`) | +| `--base ` | Public base path (default: `/`) (`string`) | +| `-l, --logLevel ` | Info \| warn \| error \| silent (`string`) | +| `--clearScreen` | Allow/disable clear screen when logging (`boolean`) | +| `--configLoader ` | Use `bundle` to bundle the config with esbuild or `runner` (experimental) to process it on the fly (default: `bundle`) | +| `-d, --debug [feat]` | Show debug logs (`string \| boolean`) | +| `-f, --filter ` | Filter debug logs (`string`) | +| `-m, --mode ` | Set env mode (`string`) | +| `-h, --help` | Display available CLI options | ### `vite preview` @@ -109,18 +112,19 @@ vite preview [root] #### Options -| Options | | -| ------------------------ | ---------------------------------------------------- | -| `--host [host]` | Specify hostname (`string`) | -| `--port ` | Specify port (`number`) | -| `--strictPort` | Exit if specified port is already in use (`boolean`) | -| `--open [path]` | Open browser on startup (`boolean \| string`) | -| `--outDir ` | Output directory (default: `dist`)(`string`) | -| `-c, --config ` | Use specified config file (`string`) | -| `--base ` | Public base path (default: `/`) (`string`) | -| `-l, --logLevel ` | Info \| warn \| error \| silent (`string`) | -| `--clearScreen` | Allow/disable clear screen when logging (`boolean`) | -| `-d, --debug [feat]` | Show debug logs (`string \| boolean`) | -| `-f, --filter ` | Filter debug logs (`string`) | -| `-m, --mode ` | Set env mode (`string`) | -| `-h, --help` | Display available CLI options | +| Options | | +| ------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `--host [host]` | Specify hostname (`string`) | +| `--port ` | Specify port (`number`) | +| `--strictPort` | Exit if specified port is already in use (`boolean`) | +| `--open [path]` | Open browser on startup (`boolean \| string`) | +| `--outDir ` | Output directory (default: `dist`)(`string`) | +| `-c, --config ` | Use specified config file (`string`) | +| `--base ` | Public base path (default: `/`) (`string`) | +| `-l, --logLevel ` | Info \| warn \| error \| silent (`string`) | +| `--clearScreen` | Allow/disable clear screen when logging (`boolean`) | +| `--configLoader ` | Use `bundle` to bundle the config with esbuild or `runner` (experimental) to process it on the fly (default: `bundle`) | +| `-d, --debug [feat]` | Show debug logs (`string \| boolean`) | +| `-f, --filter ` | Filter debug logs (`string`) | +| `-m, --mode ` | Set env mode (`string`) | +| `-h, --help` | Display available CLI options | diff --git a/eslint.config.js b/eslint.config.js index 968a7a89b5e1a4..6fc2d08520070a 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -8,7 +8,6 @@ import tseslint from 'typescript-eslint' import globals from 'globals' const require = createRequire(import.meta.url) -const pkg = require('./package.json') const pkgVite = require('./packages/vite/package.json') // Some rules work better with typechecking enabled, but as enabling it is slow, @@ -51,6 +50,11 @@ export default tseslint.config( ...globals.node, }, }, + settings: { + node: { + version: '^18.0.0 || ^20.0.0 || >=22.0.0', + }, + }, plugins: { n: pluginN, 'import-x': pluginImportX, @@ -213,17 +217,9 @@ export default tseslint.config( name: 'playground/test', files: ['playground/**/__tests__/**/*.?([cm])[jt]s?(x)'], rules: { - // engine field doesn't exist in playgrounds - 'n/no-unsupported-features/es-builtins': [ - 'error', - { - version: pkg.engines.node, - }, - ], 'n/no-unsupported-features/node-builtins': [ 'error', { - version: pkg.engines.node, // ideally we would like to allow all experimental features // https://github.com/eslint-community/eslint-plugin-n/issues/199 ignores: ['fetch'], diff --git a/package.json b/package.json index 434f88dbcbd8be..a6b316f1717d19 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "typescript": "~5.7.2", "typescript-eslint": "^8.20.0", "vite": "workspace:*", - "vitest": "^2.1.8" + "vitest": "^3.0.2" }, "simple-git-hooks": { "pre-commit": "pnpm exec lint-staged --concurrent false" diff --git a/packages/create-vite/src/index.ts b/packages/create-vite/src/index.ts index 1a0a628d6e2956..d3d827584cfcb5 100755 --- a/packages/create-vite/src/index.ts +++ b/packages/create-vite/src/index.ts @@ -10,6 +10,7 @@ const { blue, blueBright, cyan, + gray, green, greenBright, magenta, @@ -311,6 +312,7 @@ async function init() { } let targetDir = argTargetDir || defaultTargetDir + const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent) const getProjectName = () => path.basename(path.resolve(targetDir)) let result: prompts.Answers< @@ -402,9 +404,16 @@ async function init() { choices: (framework: Framework) => framework.variants.map((variant) => { const variantColor = variant.color + const command = variant.customCommand + ? getFullCustomCommand(variant.customCommand, pkgInfo).replace( + / TARGET_DIR$/, + '', + ) + : undefined return { title: variantColor(variant.display || variant.name), value: variant.name, + description: command ? gray(command) : undefined, } }), }, @@ -439,40 +448,13 @@ async function init() { template = template.replace('-swc', '') } - const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent) const pkgManager = pkgInfo ? pkgInfo.name : 'npm' - const isYarn1 = pkgManager === 'yarn' && pkgInfo?.version.startsWith('1.') const { customCommand } = FRAMEWORKS.flatMap((f) => f.variants).find((v) => v.name === template) ?? {} if (customCommand) { - const fullCustomCommand = customCommand - .replace(/^npm create /, () => { - // `bun create` uses it's own set of templates, - // the closest alternative is using `bun x` directly on the package - if (pkgManager === 'bun') { - return 'bun x create-' - } - return `${pkgManager} create ` - }) - // Only Yarn 1.x doesn't support `@version` in the `create` command - .replace('@latest', () => (isYarn1 ? '' : '@latest')) - .replace(/^npm exec/, () => { - // Prefer `pnpm dlx`, `yarn dlx`, or `bun x` - if (pkgManager === 'pnpm') { - return 'pnpm dlx' - } - if (pkgManager === 'yarn' && !isYarn1) { - return 'yarn dlx' - } - if (pkgManager === 'bun') { - return 'bun x' - } - // Use `npm exec` in all other cases, - // including Yarn 1.x and other custom npm clients. - return 'npm exec' - }) + const fullCustomCommand = getFullCustomCommand(customCommand, pkgInfo) const [command, ...args] = fullCustomCommand.split(' ') // we replace TARGET_DIR here because targetDir may include a space @@ -595,7 +577,12 @@ function emptyDir(dir: string) { } } -function pkgFromUserAgent(userAgent: string | undefined) { +interface PkgInfo { + name: string + version: string +} + +function pkgFromUserAgent(userAgent: string | undefined): PkgInfo | undefined { if (!userAgent) return undefined const pkgSpec = userAgent.split(' ')[0] const pkgSpecArr = pkgSpec.split('/') @@ -625,6 +612,40 @@ function editFile(file: string, callback: (content: string) => string) { fs.writeFileSync(file, callback(content), 'utf-8') } +function getFullCustomCommand(customCommand: string, pkgInfo?: PkgInfo) { + const pkgManager = pkgInfo ? pkgInfo.name : 'npm' + const isYarn1 = pkgManager === 'yarn' && pkgInfo?.version.startsWith('1.') + + return ( + customCommand + .replace(/^npm create /, () => { + // `bun create` uses it's own set of templates, + // the closest alternative is using `bun x` directly on the package + if (pkgManager === 'bun') { + return 'bun x create-' + } + return `${pkgManager} create ` + }) + // Only Yarn 1.x doesn't support `@version` in the `create` command + .replace('@latest', () => (isYarn1 ? '' : '@latest')) + .replace(/^npm exec/, () => { + // Prefer `pnpm dlx`, `yarn dlx`, or `bun x` + if (pkgManager === 'pnpm') { + return 'pnpm dlx' + } + if (pkgManager === 'yarn' && !isYarn1) { + return 'yarn dlx' + } + if (pkgManager === 'bun') { + return 'bun x' + } + // Use `npm exec` in all other cases, + // including Yarn 1.x and other custom npm clients. + return 'npm exec' + }) + ) +} + init().catch((e) => { console.error(e) }) diff --git a/packages/create-vite/template-react-ts/package.json b/packages/create-vite/template-react-ts/package.json index 5cc094715d5bb7..b8ca4911089707 100644 --- a/packages/create-vite/template-react-ts/package.json +++ b/packages/create-vite/template-react-ts/package.json @@ -10,13 +10,13 @@ "preview": "vite preview" }, "dependencies": { - "react": "^18.3.1", - "react-dom": "^18.3.1" + "react": "^19.0.0", + "react-dom": "^19.0.0" }, "devDependencies": { "@eslint/js": "^9.18.0", - "@types/react": "^18.3.18", - "@types/react-dom": "^18.3.5", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", "@vitejs/plugin-react": "^4.3.4", "eslint": "^9.18.0", "eslint-plugin-react-hooks": "^5.0.0", diff --git a/packages/create-vite/template-react/package.json b/packages/create-vite/template-react/package.json index ccc900f15e5b90..945ea56d243455 100644 --- a/packages/create-vite/template-react/package.json +++ b/packages/create-vite/template-react/package.json @@ -10,13 +10,13 @@ "preview": "vite preview" }, "dependencies": { - "react": "^18.3.1", - "react-dom": "^18.3.1" + "react": "^19.0.0", + "react-dom": "^19.0.0" }, "devDependencies": { "@eslint/js": "^9.18.0", - "@types/react": "^18.3.18", - "@types/react-dom": "^18.3.5", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", "@vitejs/plugin-react": "^4.3.4", "eslint": "^9.18.0", "eslint-plugin-react": "^7.37.4", diff --git a/packages/plugin-legacy/src/index.ts b/packages/plugin-legacy/src/index.ts index 21d69ad7a0796c..f8f271aa0f18d2 100644 --- a/packages/plugin-legacy/src/index.ts +++ b/packages/plugin-legacy/src/index.ts @@ -811,6 +811,7 @@ async function buildPolyfillChunk( }, output: { format, + hashCharacters: rollupOutputOptions.hashCharacters, entryFileNames: rollupOutputOptions.entryFileNames, }, }, diff --git a/packages/vite/CHANGELOG.md b/packages/vite/CHANGELOG.md index 4ac3e5c274f8f8..1fd878d1e19925 100644 --- a/packages/vite/CHANGELOG.md +++ b/packages/vite/CHANGELOG.md @@ -1,3 +1,40 @@ +## 6.0.11 (2025-01-21) + +* fix: `preview.allowedHosts` with specific values was not respected (#19246) ([aeb3ec8](https://github.com/vitejs/vite/commit/aeb3ec84a288d6be227a1284607f13428a4f14a1)), closes [#19246](https://github.com/vitejs/vite/issues/19246) +* fix: allow CORS from loopback addresses by default (#19249) ([3d03899](https://github.com/vitejs/vite/commit/3d038997377a30022b6a6b7916e0b4b5d8b9a363)), closes [#19249](https://github.com/vitejs/vite/issues/19249) + + + +## 6.0.10 (2025-01-20) + +* fix: try parse `server.origin` URL (#19241) ([2495022](https://github.com/vitejs/vite/commit/2495022420fda05ee389c2dcf26921b21e2aed3b)), closes [#19241](https://github.com/vitejs/vite/issues/19241) + + + +## 6.0.9 (2025-01-20) + +* fix!: check host header to prevent DNS rebinding attacks and introduce `server.allowedHosts` ([bd896fb](https://github.com/vitejs/vite/commit/bd896fb5f312fc0ff1730166d1d142fc0d34ba6d)) +* fix!: default `server.cors: false` to disallow fetching from untrusted origins ([b09572a](https://github.com/vitejs/vite/commit/b09572acc939351f4e4c50ddf793017a92c678b1)) +* fix: verify token for HMR WebSocket connection ([029dcd6](https://github.com/vitejs/vite/commit/029dcd6d77d3e3ef10bc38e9a0829784d9760fdb)) + + + +## 6.0.8 (2025-01-20) + +* fix: avoid SSR HMR for HTML files (#19193) ([3bd55bc](https://github.com/vitejs/vite/commit/3bd55bcb7e831d2c4f66c90d7bbb3e1fbf7a02b6)), closes [#19193](https://github.com/vitejs/vite/issues/19193) +* fix: build time display 7m 60s (#19108) ([cf0d2c8](https://github.com/vitejs/vite/commit/cf0d2c8e232a1af716c71cdd2218d180f7ecc02b)), closes [#19108](https://github.com/vitejs/vite/issues/19108) +* fix: don't resolve URL starting with double slash (#19059) ([35942cd](https://github.com/vitejs/vite/commit/35942cde11fd8a68fa89bf25f7aa1ddb87d775b2)), closes [#19059](https://github.com/vitejs/vite/issues/19059) +* fix: ensure `server.close()` only called once (#19204) ([db81c2d](https://github.com/vitejs/vite/commit/db81c2dada961f40c0882b5182adf2f34bb5c178)), closes [#19204](https://github.com/vitejs/vite/issues/19204) +* fix: resolve.conditions in ResolvedConfig was `defaultServerConditions` (#19174) ([ad75c56](https://github.com/vitejs/vite/commit/ad75c56dce5618a3a416e18f9a5c3880d437a107)), closes [#19174](https://github.com/vitejs/vite/issues/19174) +* fix: tree shake stringified JSON imports (#19189) ([f2aed62](https://github.com/vitejs/vite/commit/f2aed62d0bf1b66e870ee6b4aab80cd1702793ab)), closes [#19189](https://github.com/vitejs/vite/issues/19189) +* fix: use shared sigterm callback (#19203) ([47039f4](https://github.com/vitejs/vite/commit/47039f4643179be31a8d7c7fbff83c5c13deb787)), closes [#19203](https://github.com/vitejs/vite/issues/19203) +* fix(deps): update all non-major dependencies (#19098) ([8639538](https://github.com/vitejs/vite/commit/8639538e6498d1109da583ad942c1472098b5919)), closes [#19098](https://github.com/vitejs/vite/issues/19098) +* fix(optimizer): use correct default install state path for yarn PnP (#19119) ([e690d8b](https://github.com/vitejs/vite/commit/e690d8bb1e5741e81df5b7a6a5c8c3c1c971fa41)), closes [#19119](https://github.com/vitejs/vite/issues/19119) +* fix(types): improve `ESBuildOptions.include / exclude` type to allow `readonly (string | RegExp)[]` ([ea53e70](https://github.com/vitejs/vite/commit/ea53e7095297ea4192490fd58556414cc59a8975)), closes [#19146](https://github.com/vitejs/vite/issues/19146) +* chore(deps): update dependency pathe to v2 (#19139) ([71506f0](https://github.com/vitejs/vite/commit/71506f0a8deda5254cb49c743cd439dfe42859ce)), closes [#19139](https://github.com/vitejs/vite/issues/19139) + + + ## 6.0.7 (2025-01-02) * fix: fix `minify` when `builder.sharedPlugins: true` (#19025) ([f7b1964](https://github.com/vitejs/vite/commit/f7b1964d3a93a21f80b61638fa6ae9606d0a6f4f)), closes [#19025](https://github.com/vitejs/vite/issues/19025) diff --git a/packages/vite/client.d.ts b/packages/vite/client.d.ts index 16e2b167dec544..102e69c4bb3a25 100644 --- a/packages/vite/client.d.ts +++ b/packages/vite/client.d.ts @@ -106,6 +106,10 @@ declare module '*.cur' { const src: string export default src } +declare module '*.jxl' { + const src: string + export default src +} // media declare module '*.mp4' { diff --git a/packages/vite/index.cjs b/packages/vite/index.cjs index 70515aa90c7a8d..823b11bc167e97 100644 --- a/packages/vite/index.cjs +++ b/packages/vite/index.cjs @@ -21,6 +21,7 @@ const asyncFunctions = [ 'loadConfigFromFile', 'preprocessCSS', 'createBuilder', + 'runnerImport', ] asyncFunctions.forEach((name) => { module.exports[name] = (...args) => @@ -45,6 +46,7 @@ const disallowedVariables = [ // can be exposed, but doesn't make sense as it's Environment API related 'createServerHotChannel', 'createServerModuleRunner', + 'createServerModuleRunnerTransport', 'isRunnableDevEnvironment', ] disallowedVariables.forEach((name) => { diff --git a/packages/vite/package.json b/packages/vite/package.json index 8ce32d6cd80214..c5c06e30ddae5a 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -1,6 +1,6 @@ { "name": "vite", - "version": "6.0.7", + "version": "6.0.11", "type": "module", "license": "MIT", "author": "Evan You", @@ -144,7 +144,7 @@ "sass-embedded": "^1.83.4", "sirv": "^3.0.0", "source-map-support": "^0.5.21", - "strip-literal": "^2.1.1", + "strip-literal": "^3.0.0", "terser": "^5.37.0", "tinyglobby": "^0.2.10", "tsconfck": "^3.1.4", diff --git a/packages/vite/rollup.dts.config.ts b/packages/vite/rollup.dts.config.ts index 46306f0a22d6b5..d909755844ac1a 100644 --- a/packages/vite/rollup.dts.config.ts +++ b/packages/vite/rollup.dts.config.ts @@ -49,11 +49,12 @@ const identifierReplacements: Record> = { rollup: { Plugin$1: 'rollup.Plugin', PluginContext$1: 'rollup.PluginContext', + MinimalPluginContext$1: 'rollup.MinimalPluginContext', TransformPluginContext$1: 'rollup.TransformPluginContext', - TransformResult$2: 'rollup.TransformResult', + TransformResult$1: 'rollup.TransformResult', }, esbuild: { - TransformResult$1: 'esbuild_TransformResult', + TransformResult$2: 'esbuild_TransformResult', TransformOptions$1: 'esbuild_TransformOptions', BuildOptions$1: 'esbuild_BuildOptions', }, @@ -94,6 +95,8 @@ function patchTypes(): Plugin { renderChunk(code, chunk) { if ( chunk.fileName.startsWith('module-runner') || + // index and moduleRunner have a common chunk "moduleRunnerTransport" + chunk.fileName.startsWith('moduleRunnerTransport') || chunk.fileName.startsWith('types.d-') ) { validateRunnerChunk.call(this, chunk) @@ -116,6 +119,8 @@ function validateRunnerChunk(this: PluginContext, chunk: RenderedChunk) { if ( !id.startsWith('./') && !id.startsWith('../') && + // index and moduleRunner have a common chunk "moduleRunnerTransport" + !id.startsWith('moduleRunnerTransport.d') && !id.startsWith('types.d') ) { this.warn( @@ -138,6 +143,8 @@ function validateChunkImports(this: PluginContext, chunk: RenderedChunk) { !id.startsWith('node:') && !id.startsWith('types.d') && !id.startsWith('vite/') && + // index and moduleRunner have a common chunk "moduleRunnerTransport" + !id.startsWith('moduleRunnerTransport.d') && !deps.includes(id) && !deps.some((name) => id.startsWith(name + '/')) ) { diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index fdf13ded820b28..82f29a47f8b530 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -19,6 +19,7 @@ declare const __HMR_DIRECT_TARGET__: string declare const __HMR_BASE__: string declare const __HMR_TIMEOUT__: number declare const __HMR_ENABLE_OVERLAY__: boolean +declare const __WS_TOKEN__: string console.debug('[vite] connecting...') @@ -35,12 +36,16 @@ const socketHost = `${__HMR_HOSTNAME__ || importMetaUrl.hostname}:${ const directSocketHost = __HMR_DIRECT_TARGET__ const base = __BASE__ || '/' const hmrTimeout = __HMR_TIMEOUT__ +const wsToken = __WS_TOKEN__ const transport = normalizeModuleRunnerTransport( (() => { let wsTransport = createWebSocketModuleRunnerTransport({ createConnection: () => - new WebSocket(`${socketProtocol}://${socketHost}`, 'vite-hmr'), + new WebSocket( + `${socketProtocol}://${socketHost}?token=${wsToken}`, + 'vite-hmr', + ), pingInterval: hmrTimeout, }) @@ -54,7 +59,7 @@ const transport = normalizeModuleRunnerTransport( wsTransport = createWebSocketModuleRunnerTransport({ createConnection: () => new WebSocket( - `${socketProtocol}://${directSocketHost}`, + `${socketProtocol}://${directSocketHost}?token=${wsToken}`, 'vite-hmr', ), pingInterval: hmrTimeout, @@ -241,7 +246,9 @@ async function handleMessage(payload: HotPayload) { if (hasDocument && !willUnload) { console.log(`[vite] server connection lost. Polling for restart...`) const socket = payload.data.webSocket as WebSocket - await waitForSuccessfulPing(socket.url) + const url = new URL(socket.url) + url.search = '' // remove query string including `token` + await waitForSuccessfulPing(url.href) location.reload() } } diff --git a/packages/vite/src/module-runner/esmEvaluator.ts b/packages/vite/src/module-runner/esmEvaluator.ts index f7f8c8ab52de80..003d6b2d242b88 100644 --- a/packages/vite/src/module-runner/esmEvaluator.ts +++ b/packages/vite/src/module-runner/esmEvaluator.ts @@ -12,7 +12,7 @@ import { import type { ModuleEvaluator, ModuleRunnerContext } from './types' export class ESModulesEvaluator implements ModuleEvaluator { - startOffset = getAsyncFunctionDeclarationPaddingLineCount() + public readonly startOffset = getAsyncFunctionDeclarationPaddingLineCount() async runInlinedModule( context: ModuleRunnerContext, diff --git a/packages/vite/src/node/__tests__/build.spec.ts b/packages/vite/src/node/__tests__/build.spec.ts index 3c4be301e6d528..8f4e8f5455056c 100644 --- a/packages/vite/src/node/__tests__/build.spec.ts +++ b/packages/vite/src/node/__tests__/build.spec.ts @@ -1,17 +1,27 @@ import { basename, resolve } from 'node:path' import { fileURLToPath } from 'node:url' +import { stripVTControlCharacters } from 'node:util' import colors from 'picocolors' -import { describe, expect, test, vi } from 'vitest' -import type { OutputChunk, OutputOptions, RollupOutput } from 'rollup' +import { afterEach, describe, expect, test, vi } from 'vitest' +import type { + LogLevel, + OutputChunk, + OutputOptions, + RollupLog, + RollupOptions, + RollupOutput, +} from 'rollup' import type { LibraryFormats, LibraryOptions } from '../build' import { build, createBuilder, + onRollupLog, resolveBuildOutputs, resolveLibFilename, } from '../build' import type { Logger } from '../logger' import { createLogger } from '../logger' +import { BuildEnvironment, resolveConfig } from '..' const __dirname = resolve(fileURLToPath(import.meta.url), '..') @@ -134,6 +144,69 @@ describe('build', () => { assertOutputHashContentChange(result[0], result[1]) }) + test.for([ + [true, true], + [true, false], + [false, true], + [false, false], + ['auto', true], + ['auto', false], + ] as const)( + 'large json object files should have tree-shaking (json.stringify: %s, json.namedExports: %s)', + async ([stringify, namedExports]) => { + const esBundle = (await build({ + mode: 'development', + root: resolve(__dirname, 'packages/build-project'), + logLevel: 'silent', + json: { stringify, namedExports }, + build: { + minify: false, + modulePreload: { polyfill: false }, + write: false, + }, + plugins: [ + { + name: 'test', + resolveId(id) { + if ( + id === 'entry.js' || + id === 'object.json' || + id === 'array.json' + ) { + return '\0' + id + } + }, + load(id) { + if (id === '\0entry.js') { + return ` + import object from 'object.json'; + import array from 'array.json'; + console.log(); + ` + } + if (id === '\0object.json') { + return ` + {"value": {"${stringify}_${namedExports}":"JSON_OBJ${'_'.repeat(10_000)}"}} + ` + } + if (id === '\0array.json') { + return ` + ["${stringify}_${namedExports}","JSON_ARR${'_'.repeat(10_000)}"] + ` + } + }, + }, + ], + })) as RollupOutput + + const foo = esBundle.output.find( + (chunk) => chunk.type === 'chunk' && chunk.isEntry, + ) as OutputChunk + expect(foo.code).not.contains('JSON_ARR') + expect(foo.code).not.contains('JSON_OBJ') + }, + ) + test('external modules should not be hoisted in library build', async () => { const [esBundle] = (await build({ logLevel: 'silent', @@ -813,6 +886,158 @@ test('adjust worker build error for worker.format', async () => { expect.unreachable() }) +describe('onRollupLog', () => { + const pluginName = 'rollup-plugin-test' + const msgInfo = 'This is the INFO message.' + const msgWarn = 'This is the WARN message.' + const buildProject = async ( + level: LogLevel | 'error', + message: string | RollupLog, + logger: Logger, + options?: Pick, + ) => { + await build({ + root: resolve(__dirname, 'packages/build-project'), + logLevel: 'info', + build: { + write: false, + rollupOptions: { + ...options, + logLevel: 'debug', + }, + }, + customLogger: logger, + plugins: [ + { + name: pluginName, + resolveId(id) { + this[level](message) + if (id === 'entry.js') { + return '\0' + id + } + }, + load(id) { + if (id === '\0entry.js') { + return `export default "This is test module";` + } + }, + }, + ], + }) + } + + const callOnRollupLog = async ( + logger: Logger, + level: LogLevel, + log: RollupLog, + ) => { + const config = await resolveConfig( + { customLogger: logger }, + 'build', + 'production', + 'production', + ) + const buildEnvironment = new BuildEnvironment('client', config) + onRollupLog(level, log, buildEnvironment) + } + + afterEach(() => { + vi.restoreAllMocks() + }) + + test('Rollup logs of info should be handled by vite', async () => { + const logger = createLogger() + const loggerSpy = vi.spyOn(logger, 'info').mockImplementation(() => {}) + + await buildProject('info', msgInfo, logger) + const logs = loggerSpy.mock.calls.map((args) => + stripVTControlCharacters(args[0]), + ) + expect(logs).contain(`[plugin ${pluginName}] ${msgInfo}`) + }) + + test('Rollup logs of warn should be handled by vite', async () => { + const logger = createLogger('silent') + const loggerSpy = vi.spyOn(logger, 'warn').mockImplementation(() => {}) + + await buildProject('warn', msgWarn, logger) + const logs = loggerSpy.mock.calls.map((args) => + stripVTControlCharacters(args[0]), + ) + expect(logs).contain(`[plugin ${pluginName}] ${msgWarn}`) + }) + + test('onLog passed by user is called', async () => { + const logger = createLogger('silent') + + const onLogInfo = vi.fn((_log: RollupLog) => {}) + await buildProject('info', msgInfo, logger, { + onLog(level, log) { + if (level === 'info') { + onLogInfo(log) + } + }, + }) + expect(onLogInfo).toBeCalledWith( + expect.objectContaining({ message: `[plugin ${pluginName}] ${msgInfo}` }), + ) + }) + + test('onwarn passed by user is called', async () => { + const logger = createLogger('silent') + + const onWarn = vi.fn((_log: RollupLog) => {}) + await buildProject('warn', msgWarn, logger, { + onwarn(warning) { + onWarn(warning) + }, + }) + expect(onWarn).toBeCalledWith( + expect.objectContaining({ message: `[plugin ${pluginName}] ${msgWarn}` }), + ) + }) + + test('should throw error when warning contains UNRESOLVED_IMPORT', async () => { + const logger = createLogger() + await expect(() => + callOnRollupLog(logger, 'warn', { + code: 'UNRESOLVED_IMPORT', + message: 'test', + }), + ).rejects.toThrowError(/Rollup failed to resolve import/) + }) + + test.each([[`Unsupported expression`], [`statically analyzed`]])( + 'should ignore dynamic import warnings (%s)', + async (message: string) => { + const logger = createLogger() + const loggerSpy = vi.spyOn(logger, 'warn').mockImplementation(() => {}) + + await callOnRollupLog(logger, 'warn', { + code: 'PLUGIN_WARNING', + message: message, + plugin: 'rollup-plugin-dynamic-import-variables', + }) + expect(loggerSpy).toBeCalledTimes(0) + }, + ) + + test.each([[`CIRCULAR_DEPENDENCY`], [`THIS_IS_UNDEFINED`]])( + 'should ignore some warnings (%s)', + async (code: string) => { + const logger = createLogger() + const loggerSpy = vi.spyOn(logger, 'warn').mockImplementation(() => {}) + + await callOnRollupLog(logger, 'warn', { + code: code, + message: 'test message', + plugin: pluginName, + }) + expect(loggerSpy).toBeCalledTimes(0) + }, + ) +}) + /** * for each chunks in output1, if there's a chunk in output2 with the same fileName, * ensure that the chunk code is the same. if not, the chunk hash should have changed. diff --git a/packages/vite/src/node/__tests__/config.spec.ts b/packages/vite/src/node/__tests__/config.spec.ts index f249a06fa23e56..e2966f262add71 100644 --- a/packages/vite/src/node/__tests__/config.spec.ts +++ b/packages/vite/src/node/__tests__/config.spec.ts @@ -249,7 +249,7 @@ describe('preview config', () => { 'Cache-Control': 'no-store', }, proxy: { '/foo': 'http://localhost:4567' }, - cors: false, + cors: true, }) test('preview inherits server config with default port', async () => { @@ -285,7 +285,7 @@ describe('preview config', () => { open: false, host: false, proxy: { '/bar': 'http://localhost:3010' }, - cors: true, + cors: false, }) test('preview overrides server config', async () => { diff --git a/packages/vite/src/node/__tests__/constants.spec.ts b/packages/vite/src/node/__tests__/constants.spec.ts new file mode 100644 index 00000000000000..c7015f60104280 --- /dev/null +++ b/packages/vite/src/node/__tests__/constants.spec.ts @@ -0,0 +1,32 @@ +import { expect, test } from 'vitest' +import { defaultAllowedOrigins } from '../constants' + +test('defaultAllowedOrigins', () => { + const allowed = [ + 'http://localhost', + 'http://foo.localhost', + 'http://localhost:3000', + 'https://localhost:3000', + 'http://127.0.0.1', + 'http://[::1]', + 'http://[::1]:3000', + ] + const denied = [ + 'file:///foo', + 'http://localhost.example.com', + 'http://foo.example.com:localhost', + 'http://', + 'http://192.0.2', + 'http://[2001:db8::1]', + 'http://vite', + 'http://vite:3000', + ] + + for (const origin of allowed) { + expect(defaultAllowedOrigins.test(origin), origin).toBe(true) + } + + for (const origin of denied) { + expect(defaultAllowedOrigins.test(origin), origin).toBe(false) + } +}) diff --git a/packages/vite/src/node/__tests__/fixtures/runner-import/basic.ts b/packages/vite/src/node/__tests__/fixtures/runner-import/basic.ts new file mode 100644 index 00000000000000..2fee8764679469 --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/runner-import/basic.ts @@ -0,0 +1,7 @@ +interface Test { + field: true +} + +export const test: Test = { + field: true, +} diff --git a/packages/vite/src/node/__tests__/fixtures/runner-import/cjs.js b/packages/vite/src/node/__tests__/fixtures/runner-import/cjs.js new file mode 100644 index 00000000000000..4ba52ba2c8df67 --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/runner-import/cjs.js @@ -0,0 +1 @@ +module.exports = {} diff --git a/packages/vite/src/node/__tests__/fixtures/runner-import/dynamic-import-dep.ts b/packages/vite/src/node/__tests__/fixtures/runner-import/dynamic-import-dep.ts new file mode 100644 index 00000000000000..60c71f346d9a3e --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/runner-import/dynamic-import-dep.ts @@ -0,0 +1 @@ +export default 'ok' diff --git a/packages/vite/src/node/__tests__/fixtures/runner-import/dynamic-import.ts b/packages/vite/src/node/__tests__/fixtures/runner-import/dynamic-import.ts new file mode 100644 index 00000000000000..b406cfff6e5ac2 --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/runner-import/dynamic-import.ts @@ -0,0 +1 @@ +export default () => import('./dynamic-import-dep') diff --git a/packages/vite/src/node/__tests__/fixtures/runner-import/plugin.ts b/packages/vite/src/node/__tests__/fixtures/runner-import/plugin.ts new file mode 100644 index 00000000000000..173c7d928c4f17 --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/runner-import/plugin.ts @@ -0,0 +1,7 @@ +import { Plugin } from 'vite' + +export default function testPlugin(): Plugin { + return { + name: 'test', + } +} diff --git a/packages/vite/src/node/__tests__/fixtures/runner-import/vite.config.outside-pkg-import.mts b/packages/vite/src/node/__tests__/fixtures/runner-import/vite.config.outside-pkg-import.mts new file mode 100644 index 00000000000000..86fea8ffe713f7 --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/runner-import/vite.config.outside-pkg-import.mts @@ -0,0 +1,5 @@ +import parent from '@vitejs/parent' + +export default { + __injected: parent.child, +} diff --git a/packages/vite/src/node/__tests__/fixtures/runner-import/vite.config.ts b/packages/vite/src/node/__tests__/fixtures/runner-import/vite.config.ts new file mode 100644 index 00000000000000..2cbd562498124b --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/runner-import/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import plugin from './plugin' + +export default defineConfig({ + root: './test', + plugins: [plugin()], +}) diff --git a/packages/vite/src/node/__tests__/package.json b/packages/vite/src/node/__tests__/package.json index 90a5d7c132c1ee..4f6f029f385542 100644 --- a/packages/vite/src/node/__tests__/package.json +++ b/packages/vite/src/node/__tests__/package.json @@ -3,6 +3,7 @@ "private": true, "version": "0.0.0", "dependencies": { + "@vitejs/parent": "link:./packages/parent", "@vitejs/cjs-ssr-dep": "link:./fixtures/cjs-ssr-dep", "@vitejs/test-dep-conditions": "file:./fixtures/test-dep-conditions" } diff --git a/packages/vite/src/node/__tests__/packages/child/index.js b/packages/vite/src/node/__tests__/packages/child/index.js new file mode 100644 index 00000000000000..186b120756be19 --- /dev/null +++ b/packages/vite/src/node/__tests__/packages/child/index.js @@ -0,0 +1 @@ +export default true diff --git a/packages/vite/src/node/__tests__/packages/child/package.json b/packages/vite/src/node/__tests__/packages/child/package.json new file mode 100644 index 00000000000000..77e2aa64615b63 --- /dev/null +++ b/packages/vite/src/node/__tests__/packages/child/package.json @@ -0,0 +1,5 @@ +{ + "name": "@vitejs/child", + "type": "module", + "main": "./index.js" +} diff --git a/packages/vite/src/node/__tests__/packages/parent/index.ts b/packages/vite/src/node/__tests__/packages/parent/index.ts new file mode 100644 index 00000000000000..747305283cadb2 --- /dev/null +++ b/packages/vite/src/node/__tests__/packages/parent/index.ts @@ -0,0 +1,6 @@ +// @ts-expect-error not typed +import child from '@vitejs/child' + +export default { + child, +} diff --git a/packages/vite/src/node/__tests__/packages/parent/package.json b/packages/vite/src/node/__tests__/packages/parent/package.json new file mode 100644 index 00000000000000..d966448a0560a8 --- /dev/null +++ b/packages/vite/src/node/__tests__/packages/parent/package.json @@ -0,0 +1,8 @@ +{ + "name": "@vitejs/parent", + "type": "module", + "main": "./index.ts", + "dependencies": { + "@vitejs/child": "link:../child" + } +} diff --git a/packages/vite/src/node/__tests__/plugins/json.spec.ts b/packages/vite/src/node/__tests__/plugins/json.spec.ts index 3e114fd3dd145b..e90bcb39c22737 100644 --- a/packages/vite/src/node/__tests__/plugins/json.spec.ts +++ b/packages/vite/src/node/__tests__/plugins/json.spec.ts @@ -62,7 +62,7 @@ describe('transform', () => { false, ) expect(actualSmall).toMatchInlineSnapshot( - `"export default JSON.parse("[{\\"a\\":1,\\"b\\":2}]")"`, + `"export default /* #__PURE__ */ JSON.parse("[{\\"a\\":1,\\"b\\":2}]")"`, ) }) @@ -122,7 +122,7 @@ describe('transform', () => { false, ) expect(actualDev).toMatchInlineSnapshot( - `"export default JSON.parse("{\\"a\\":1,\\n\\"🫠\\": \\"\\",\\n\\"const\\": false}")"`, + `"export default /* #__PURE__ */ JSON.parse("{\\"a\\":1,\\n\\"🫠\\": \\"\\",\\n\\"const\\": false}")"`, ) const actualBuild = transform( @@ -131,7 +131,7 @@ describe('transform', () => { true, ) expect(actualBuild).toMatchInlineSnapshot( - `"export default JSON.parse("{\\"a\\":1,\\"🫠\\":\\"\\",\\"const\\":false}")"`, + `"export default /* #__PURE__ */ JSON.parse("{\\"a\\":1,\\"🫠\\":\\"\\",\\"const\\":false}")"`, ) }) diff --git a/packages/vite/src/node/__tests__/plugins/workerImportMetaUrl.spec.ts b/packages/vite/src/node/__tests__/plugins/workerImportMetaUrl.spec.ts new file mode 100644 index 00000000000000..201946b99207c0 --- /dev/null +++ b/packages/vite/src/node/__tests__/plugins/workerImportMetaUrl.spec.ts @@ -0,0 +1,163 @@ +import { describe, expect, test } from 'vitest' +import { parseAst } from 'rollup/parseAst' +import { workerImportMetaUrlPlugin } from '../../plugins/workerImportMetaUrl' +import { resolveConfig } from '../../config' +import { PartialEnvironment } from '../../baseEnvironment' + +async function createWorkerImportMetaUrlPluginTransform() { + const config = await resolveConfig({ configFile: false }, 'serve') + const instance = workerImportMetaUrlPlugin(config) + const environment = new PartialEnvironment('client', config) + + return async (code: string) => { + // @ts-expect-error transform should exist + const result = await instance.transform.call( + { environment, parse: parseAst }, + code, + 'foo.ts', + ) + return result?.code || result + } +} + +describe('workerImportMetaUrlPlugin', async () => { + const transform = await createWorkerImportMetaUrlPluginTransform() + + test('without worker options', async () => { + expect( + await transform('new Worker(new URL("./worker.js", import.meta.url))'), + ).toMatchInlineSnapshot( + `"new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=classic", import.meta.url))"`, + ) + }) + + test('with shared worker', async () => { + expect( + await transform( + 'new SharedWorker(new URL("./worker.js", import.meta.url))', + ), + ).toMatchInlineSnapshot( + `"new SharedWorker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=classic", import.meta.url))"`, + ) + }) + + test('with static worker options and identifier properties', async () => { + expect( + await transform( + 'new Worker(new URL("./worker.js", import.meta.url), { type: "module", name: "worker1" })', + ), + ).toMatchInlineSnapshot( + `"new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=module", import.meta.url), { type: "module", name: "worker1" })"`, + ) + }) + + test('with static worker options and literal properties', async () => { + expect( + await transform( + 'new Worker(new URL("./worker.js", import.meta.url), { "type": "module", "name": "worker1" })', + ), + ).toMatchInlineSnapshot( + `"new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=module", import.meta.url), { "type": "module", "name": "worker1" })"`, + ) + }) + + test('with dynamic name field in worker options', async () => { + expect( + await transform( + 'const id = 1; new Worker(new URL("./worker.js", import.meta.url), { name: "worker" + id })', + ), + ).toMatchInlineSnapshot( + `"const id = 1; new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=classic", import.meta.url), { name: "worker" + id })"`, + ) + }) + + test('with dynamic name field and static type in worker options', async () => { + expect( + await transform( + 'const id = 1; new Worker(new URL("./worker.js", import.meta.url), { name: "worker" + id, type: "module" })', + ), + ).toMatchInlineSnapshot( + `"const id = 1; new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=module", import.meta.url), { name: "worker" + id, type: "module" })"`, + ) + }) + + test('with parenthesis inside of worker options', async () => { + expect( + await transform( + 'const worker = new Worker(new URL("./worker.js", import.meta.url), { name: genName(), type: "module"})', + ), + ).toMatchInlineSnapshot( + `"const worker = new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=module", import.meta.url), { name: genName(), type: "module"})"`, + ) + }) + + test('with multi-line code and worker options', async () => { + expect( + await transform(` +const worker = new Worker(new URL("./worker.js", import.meta.url), { + name: genName(), + type: "module", + }, +) + +worker.addEventListener('message', (ev) => text('.simple-worker-url', JSON.stringify(ev.data))) +`), + ).toMatchInlineSnapshot(`" +const worker = new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=module", import.meta.url), { + name: genName(), + type: "module", + }, +) + +worker.addEventListener('message', (ev) => text('.simple-worker-url', JSON.stringify(ev.data))) +"`) + }) + + test('throws an error when non-static worker options are provided', async () => { + await expect( + transform( + 'new Worker(new URL("./worker.js", import.meta.url), myWorkerOptions)', + ), + ).rejects.toThrow( + 'Vite is unable to parse the worker options as the value is not static. To ignore this error, please use /* @vite-ignore */ in the worker options.', + ) + }) + + test('throws an error when worker options are not an object', async () => { + await expect( + transform( + 'new Worker(new URL("./worker.js", import.meta.url), "notAnObject")', + ), + ).rejects.toThrow('Expected worker options to be an object, got string') + }) + + test('throws an error when non-literal type field in worker options', async () => { + await expect( + transform( + 'const type = "module"; new Worker(new URL("./worker.js", import.meta.url), { type })', + ), + ).rejects.toThrow( + 'Expected worker options type property to be a literal value.', + ) + }) + + test('throws an error when spread operator used without the type field', async () => { + await expect( + transform( + 'const options = { name: "worker1" }; new Worker(new URL("./worker.js", import.meta.url), { ...options })', + ), + ).rejects.toThrow( + 'Expected object spread to be used before the definition of the type property. Vite needs a static value for the type property to correctly infer it.', + ) + }) + + test('throws an error when spread operator used after definition of type field', async () => { + await expect( + transform( + 'const options = { name: "worker1" }; new Worker(new URL("./worker.js", import.meta.url), { type: "module", ...options })', + ), + ).rejects.toThrow( + 'Expected object spread to be used before the definition of the type property. Vite needs a static value for the type property to correctly infer it.', + ) + }) +}) diff --git a/packages/vite/src/node/__tests__/resolve.spec.ts b/packages/vite/src/node/__tests__/resolve.spec.ts index 111cdb99a6b1a0..ad15dcdd73a547 100644 --- a/packages/vite/src/node/__tests__/resolve.spec.ts +++ b/packages/vite/src/node/__tests__/resolve.spec.ts @@ -2,7 +2,7 @@ import { join } from 'node:path' import { describe, expect, onTestFinished, test } from 'vitest' import { createServer } from '../server' import { createServerModuleRunner } from '../ssr/runtime/serverModuleRunner' -import type { InlineConfig } from '../config' +import type { EnvironmentOptions, InlineConfig } from '../config' import { build } from '../build' describe('import and resolveId', () => { @@ -116,6 +116,137 @@ describe('file url', () => { expect(mod4.default).toBe(mod) }) + describe('environment builtins', () => { + function getConfig( + targetEnv: 'client' | 'ssr' | string, + builtins: NonNullable['builtins'], + ): InlineConfig { + return { + configFile: false, + root: join(import.meta.dirname, 'fixtures/file-url'), + logLevel: 'error', + server: { + middlewareMode: true, + }, + environments: { + [targetEnv]: { + resolve: { + builtins, + }, + }, + }, + } + } + + async function run({ + builtins, + targetEnv = 'custom', + testEnv = 'custom', + idToResolve, + }: { + builtins?: NonNullable['builtins'] + targetEnv?: 'client' | 'ssr' | string + testEnv?: 'client' | 'ssr' | string + idToResolve: string + }) { + const server = await createServer(getConfig(targetEnv, builtins)) + onTestFinished(() => server.close()) + + return server.environments[testEnv]?.pluginContainer.resolveId( + idToResolve, + ) + } + + test('declared builtin string', async () => { + const resolved = await run({ + builtins: ['my-env:custom-builtin'], + idToResolve: 'my-env:custom-builtin', + }) + expect(resolved?.external).toBe(true) + }) + + test('declared builtin regexp', async () => { + const resolved = await run({ + builtins: [/^my-env:\w/], + idToResolve: 'my-env:custom-builtin', + }) + expect(resolved?.external).toBe(true) + }) + + test('non declared builtin', async () => { + const resolved = await run({ + builtins: [ + /* empty */ + ], + idToResolve: 'my-env:custom-builtin', + }) + expect(resolved).toBeNull() + }) + + test('non declared node builtin', async () => { + await expect( + run({ + builtins: [ + /* empty */ + ], + idToResolve: 'node:fs', + }), + ).rejects.toThrowError( + /Automatically externalized node built-in module "node:fs"/, + ) + }) + + test('default to node-like builtins', async () => { + const resolved = await run({ + idToResolve: 'node:fs', + }) + expect(resolved?.external).toBe(true) + }) + + test('default to node-like builtins for ssr environment', async () => { + const resolved = await run({ + idToResolve: 'node:fs', + testEnv: 'ssr', + }) + expect(resolved?.external).toBe(true) + }) + + test('no default to node-like builtins for client environment', async () => { + const resolved = await run({ + idToResolve: 'node:fs', + testEnv: 'client', + }) + expect(resolved?.id).toEqual('__vite-browser-external:node:fs') + }) + + test('no builtins overriding for client environment', async () => { + const resolved = await run({ + idToResolve: 'node:fs', + testEnv: 'client', + targetEnv: 'client', + }) + expect(resolved?.id).toEqual('__vite-browser-external:node:fs') + }) + + test('declared node builtin', async () => { + const resolved = await run({ + builtins: [/^node:/], + idToResolve: 'node:fs', + }) + expect(resolved?.external).toBe(true) + }) + + test('declared builtin string in different environment', async () => { + const resolved = await run({ + builtins: ['my-env:custom-builtin'], + idToResolve: 'my-env:custom-builtin', + targetEnv: 'custom', + testEnv: 'ssr', + }) + expect(resolved).toBe(null) + }) + }) + test('build', async () => { await build({ ...getConfig(), diff --git a/packages/vite/src/node/__tests__/runnerImport.spec.ts b/packages/vite/src/node/__tests__/runnerImport.spec.ts new file mode 100644 index 00000000000000..d6084b84bbbf30 --- /dev/null +++ b/packages/vite/src/node/__tests__/runnerImport.spec.ts @@ -0,0 +1,73 @@ +import { resolve } from 'node:path' +import { describe, expect, test } from 'vitest' +import { loadConfigFromFile } from 'vite' +import { runnerImport } from '../ssr/runnerImport' +import { slash } from '../../shared/utils' + +describe('importing files using inlined environment', () => { + const fixture = (name: string) => + resolve(import.meta.dirname, './fixtures/runner-import', name) + + test('importing a basic file works', async () => { + const { module } = await runnerImport< + typeof import('./fixtures/runner-import/basic') + >(fixture('basic')) + expect(module.test).toEqual({ + field: true, + }) + }) + + test("cannot import cjs, 'runnerImport' doesn't support CJS syntax at all", async () => { + await expect(() => + runnerImport( + fixture('cjs.js'), + ), + ).rejects.toThrow('module is not defined') + }) + + test('can import vite config', async () => { + const { module, dependencies } = await runnerImport< + typeof import('./fixtures/runner-import/vite.config') + >(fixture('vite.config')) + expect(module.default).toEqual({ + root: './test', + plugins: [ + { + name: 'test', + }, + ], + }) + expect(dependencies).toEqual([slash(fixture('plugin.ts'))]) + }) + + test('can import vite config that imports a TS external module', async () => { + const { module, dependencies } = await runnerImport< + typeof import('./fixtures/runner-import/vite.config.outside-pkg-import.mjs') + >(fixture('vite.config.outside-pkg-import.mts')) + + expect(module.default.__injected).toBe(true) + expect(dependencies).toEqual([ + slash(resolve(import.meta.dirname, './packages/parent/index.ts')), + ]) + + // confirm that it fails with a bundle approach + await expect(async () => { + const root = resolve(import.meta.dirname, './fixtures/runner-import') + await loadConfigFromFile( + { mode: 'production', command: 'serve' }, + resolve(root, './vite.config.outside-pkg-import.mts'), + root, + 'silent', + ) + }).rejects.toThrow('Unknown file extension ".ts"') + }) + + test('dynamic import', async () => { + const { module } = await runnerImport(fixture('dynamic-import.ts')) + await expect(() => module.default()).rejects.toMatchInlineSnapshot( + `[Error: Vite module runner has been closed.]`, + ) + // const dep = await module.default(); + // expect(dep.default).toMatchInlineSnapshot(`"ok"`) + }) +}) diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index ea19a35d95bf55..53f979655e9885 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -5,7 +5,8 @@ import type { ExternalOption, InputOption, InternalModuleFormat, - LoggingFunction, + LogLevel, + LogOrStringHandler, ModuleFormat, OutputOptions, RollupBuild, @@ -14,6 +15,7 @@ import type { RollupOptions, RollupOutput, RollupWatcher, + WarningHandlerWithDefault, WatcherOptions, } from 'rollup' import commonjsPlugin from '@rollup/plugin-commonjs' @@ -42,6 +44,7 @@ import { arraify, asyncFlatten, copyDir, + createDebugger, displayTime, emptyDir, getPkgName, @@ -596,8 +599,8 @@ async function buildEnvironment( input, plugins, external: options.rollupOptions.external, - onwarn(warning, warn) { - onRollupWarning(warning, warn, environment) + onLog(level, log) { + onRollupLog(level, log, environment) }, } @@ -635,7 +638,12 @@ async function buildEnvironment( const stackOnly = extractStack(e) let msg = colors.red((e.plugin ? `[${e.plugin}] ` : '') + e.message) - if (e.id) { + if (e.loc && e.loc.file && e.loc.file !== e.id) { + msg += `\nfile: ${colors.cyan( + `${e.loc.file}:${e.loc.line}:${e.loc.column}` + + (e.id ? ` (${e.id})` : ''), + )}` + } else if (e.id) { msg += `\nfile: ${colors.cyan( e.id + (e.loc ? `:${e.loc.line}:${e.loc.column}` : ''), )}` @@ -1001,70 +1009,102 @@ function clearLine() { } } -export function onRollupWarning( - warning: RollupLog, - warn: LoggingFunction, +export function onRollupLog( + level: LogLevel, + log: RollupLog, environment: BuildEnvironment, ): void { - const viteWarn: LoggingFunction = (warnLog) => { - let warning: string | RollupLog - - if (typeof warnLog === 'function') { - warning = warnLog() - } else { - warning = warnLog - } - - if (typeof warning === 'object') { - if (warning.code === 'UNRESOLVED_IMPORT') { - const id = warning.id - const exporter = warning.exporter - // throw unless it's commonjs external... - if (!id || !id.endsWith('?commonjs-external')) { - throw new Error( - `[vite]: Rollup failed to resolve import "${exporter}" from "${id}".\n` + - `This is most likely unintended because it can break your application at runtime.\n` + - `If you do want to externalize this module explicitly add it to\n` + - `\`build.rollupOptions.external\``, - ) - } + const debugLogger = createDebugger('vite:build') + const viteLog: LogOrStringHandler = (logLeveling, rawLogging) => { + const logging = + typeof rawLogging === 'object' ? rawLogging : { message: rawLogging } + + if (logging.code === 'UNRESOLVED_IMPORT') { + const id = logging.id + const exporter = logging.exporter + // throw unless it's commonjs external... + if (!id || !id.endsWith('?commonjs-external')) { + throw new Error( + `[vite]: Rollup failed to resolve import "${exporter}" from "${id}".\n` + + `This is most likely unintended because it can break your application at runtime.\n` + + `If you do want to externalize this module explicitly add it to\n` + + `\`build.rollupOptions.external\``, + ) } + } + if (logLeveling === 'warn') { if ( - warning.plugin === 'rollup-plugin-dynamic-import-variables' && + logging.plugin === 'rollup-plugin-dynamic-import-variables' && dynamicImportWarningIgnoreList.some((msg) => - warning.message.includes(msg), + logging.message.includes(msg), ) ) { return } - if (warningIgnoreList.includes(warning.code!)) { + if (warningIgnoreList.includes(logging.code!)) { return } + } - if (warning.code === 'PLUGIN_WARNING') { - environment.logger.warn( - `${colors.bold( - colors.yellow(`[plugin:${warning.plugin}]`), - )} ${colors.yellow(warning.message)}`, - ) + switch (logLeveling) { + case 'info': + environment.logger.info(logging.message) + return + case 'warn': + environment.logger.warn(colors.yellow(logging.message)) + return + case 'error': + environment.logger.error(colors.red(logging.message)) + return + case 'debug': + debugLogger?.(logging.message) + return + default: + logLeveling satisfies never + // fallback to info if a unknown log level is passed + environment.logger.info(logging.message) return - } } - - warn(warnLog) } clearLine() - const userOnWarn = environment.config.build.rollupOptions.onwarn - if (userOnWarn) { - userOnWarn(warning, viteWarn) + const userOnLog = environment.config.build.rollupOptions?.onLog + const userOnWarn = environment.config.build.rollupOptions?.onwarn + if (userOnLog) { + if (userOnWarn) { + const normalizedUserOnWarn = normalizeUserOnWarn(userOnWarn, viteLog) + userOnLog(level, log, normalizedUserOnWarn) + } else { + userOnLog(level, log, viteLog) + } + } else if (userOnWarn) { + const normalizedUserOnWarn = normalizeUserOnWarn(userOnWarn, viteLog) + normalizedUserOnWarn(level, log) } else { - viteWarn(warning) + viteLog(level, log) } } +function normalizeUserOnWarn( + userOnWarn: WarningHandlerWithDefault, + defaultHandler: LogOrStringHandler, +): LogOrStringHandler { + return (logLevel, logging) => { + if (logLevel === 'warn') { + userOnWarn(normalizeLog(logging), (log) => + defaultHandler('warn', typeof log === 'function' ? log() : log), + ) + } else { + defaultHandler(logLevel, logging) + } + } +} + +const normalizeLog = (log: RollupLog | string): RollupLog => + typeof log === 'string' ? { message: log } : log + export function resolveUserExternal( user: ExternalOption, id: string, diff --git a/packages/vite/src/node/cli.ts b/packages/vite/src/node/cli.ts index b520b9b5144b46..aeaaafb697b3b3 100644 --- a/packages/vite/src/node/cli.ts +++ b/packages/vite/src/node/cli.ts @@ -23,6 +23,7 @@ interface GlobalCLIOptions { l?: LogLevel logLevel?: LogLevel clearScreen?: boolean + configLoader?: 'bundle' | 'runner' d?: boolean | string debug?: boolean | string f?: string @@ -87,6 +88,7 @@ function cleanGlobalCLIOptions( delete ret.l delete ret.logLevel delete ret.clearScreen + delete ret.configLoader delete ret.d delete ret.debug delete ret.f @@ -151,6 +153,10 @@ cli }) .option('-l, --logLevel ', `[string] info | warn | error | silent`) .option('--clearScreen', `[boolean] allow/disable clear screen when logging`) + .option( + '--configLoader ', + `[string] use 'bundle' to bundle the config with esbuild or 'runner' (experimental) to process it on the fly (default: bundle)`, + ) .option('-d, --debug [feat]', `[string | boolean] show debug logs`) .option('-f, --filter ', `[string] filter debug logs`) .option('-m, --mode ', `[string] set env mode`) @@ -180,10 +186,11 @@ cli base: options.base, mode: options.mode, configFile: options.config, + configLoader: options.configLoader, logLevel: options.logLevel, clearScreen: options.clearScreen, - optimizeDeps: { force: options.force }, server: cleanGlobalCLIOptions(options), + forceOptimizeDeps: options.force, }) if (!server.httpServer) { @@ -304,6 +311,7 @@ cli base: options.base, mode: options.mode, configFile: options.config, + configLoader: options.configLoader, logLevel: options.logLevel, clearScreen: options.clearScreen, build: buildOptions, @@ -340,6 +348,7 @@ cli root, base: options.base, configFile: options.config, + configLoader: options.configLoader, logLevel: options.logLevel, mode: options.mode, }, @@ -382,6 +391,7 @@ cli root, base: options.base, configFile: options.config, + configLoader: options.configLoader, logLevel: options.logLevel, mode: options.mode, build: { diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 507c580800325a..00444570cbfdea 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -1,15 +1,16 @@ import fs from 'node:fs' -import fsp from 'node:fs/promises' import path from 'node:path' +import fsp from 'node:fs/promises' import { pathToFileURL } from 'node:url' import { promisify } from 'node:util' import { performance } from 'node:perf_hooks' import { createRequire } from 'node:module' +import crypto from 'node:crypto' import colors from 'picocolors' import type { Alias, AliasOptions } from 'dep-types/alias' -import { build } from 'esbuild' import type { RollupOptions } from 'rollup' import picomatch from 'picomatch' +import { build } from 'esbuild' import type { AnymatchFn } from '../types/anymatch' import { withTrailingSlash } from '../shared/utils' import { @@ -62,16 +63,17 @@ import { asyncFlatten, createDebugger, createFilter, - isBuiltin, isExternalUrl, isFilePathESM, isInNodeModules, isNodeBuiltin, + isNodeLikeBuiltin, isObject, isParentDirectory, mergeAlias, mergeConfig, mergeWithDefaults, + nodeLikeBuiltins, normalizeAlias, normalizePath, } from './utils' @@ -82,12 +84,12 @@ import { resolvePlugins, } from './plugins' import type { ESBuildOptions } from './plugins/esbuild' -import type { - EnvironmentResolveOptions, - InternalResolveOptions, - ResolveOptions, +import { + type EnvironmentResolveOptions, + type InternalResolveOptions, + type ResolveOptions, + tryNodeResolve, } from './plugins/resolve' -import { tryNodeResolve } from './plugins/resolve' import type { LogLevel, Logger } from './logger' import { createLogger } from './logger' import type { DepOptimizationOptions } from './optimizer' @@ -99,6 +101,8 @@ import type { ResolvedSSROptions, SSROptions } from './ssr' import { resolveSSROptions, ssrConfigDefaults } from './ssr' import { PartialEnvironment } from './baseEnvironment' import { createIdResolver } from './idResolver' +import { runnerImport } from './ssr/runnerImport' +import { getAdditionalAllowedHosts } from './server/middlewares/hostCheck' const debug = createDebugger('vite:config', { depth: 10 }) const promisifiedRealpath = promisify(fs.realpath) @@ -519,6 +523,18 @@ export interface LegacyOptions { * https://github.com/vitejs/vite/discussions/14697. */ proxySsrExternalModules?: boolean + /** + * In Vite 6.0.8 and below, WebSocket server was able to connect from any web pages. However, + * that could be exploited by a malicious web page. + * + * In Vite 6.0.9+, the WebSocket server now requires a token to connect from a web page. + * But this may break some plugins and frameworks that connects to the WebSocket server + * on their own. Enabling this option will make Vite skip the token check. + * + * **We do not recommend enabling this option unless you are sure that you are fine with + * that security weakness.** + */ + skipWebSocketTokenCheck?: boolean } export interface ResolvedWorkerOptions { @@ -529,76 +545,93 @@ export interface ResolvedWorkerOptions { export interface InlineConfig extends UserConfig { configFile?: string | false + /** @experimental */ + configLoader?: 'bundle' | 'runner' envFile?: false + forceOptimizeDeps?: boolean } -export type ResolvedConfig = Readonly< - Omit< - UserConfig, - | 'plugins' - | 'css' - | 'json' - | 'assetsInclude' - | 'optimizeDeps' - | 'worker' - | 'build' - | 'dev' - | 'environments' - | 'server' - | 'preview' - > & { - configFile: string | undefined - configFileDependencies: string[] - inlineConfig: InlineConfig - root: string - base: string - /** @internal */ - decodedBase: string - /** @internal */ - rawBase: string - publicDir: string - cacheDir: string - command: 'build' | 'serve' - mode: string - isWorker: boolean - // in nested worker bundle to find the main config - /** @internal */ - mainConfig: ResolvedConfig | null - /** @internal list of bundle entry id. used to detect recursive worker bundle. */ - bundleChain: string[] - isProduction: boolean - envDir: string - env: Record - resolve: Required & { - alias: Alias[] - } - plugins: readonly Plugin[] - css: ResolvedCSSOptions - json: Required - esbuild: ESBuildOptions | false - server: ResolvedServerOptions - dev: ResolvedDevEnvironmentOptions - /** @experimental */ - builder: ResolvedBuilderOptions | undefined - build: ResolvedBuildOptions - preview: ResolvedPreviewOptions - ssr: ResolvedSSROptions - assetsInclude: (file: string) => boolean - logger: Logger - createResolver: (options?: Partial) => ResolveFn - optimizeDeps: DepOptimizationOptions - /** @internal */ - packageCache: PackageCache - worker: ResolvedWorkerOptions - appType: AppType - experimental: ExperimentalOptions - environments: Record - /** @internal */ - fsDenyGlob: AnymatchFn - /** @internal */ - safeModulePaths: Set - } & PluginHookUtils -> +export interface ResolvedConfig + extends Readonly< + Omit< + UserConfig, + | 'plugins' + | 'css' + | 'json' + | 'assetsInclude' + | 'optimizeDeps' + | 'worker' + | 'build' + | 'dev' + | 'environments' + | 'server' + | 'preview' + > & { + configFile: string | undefined + configFileDependencies: string[] + inlineConfig: InlineConfig + root: string + base: string + /** @internal */ + decodedBase: string + /** @internal */ + rawBase: string + publicDir: string + cacheDir: string + command: 'build' | 'serve' + mode: string + isWorker: boolean + // in nested worker bundle to find the main config + /** @internal */ + mainConfig: ResolvedConfig | null + /** @internal list of bundle entry id. used to detect recursive worker bundle. */ + bundleChain: string[] + isProduction: boolean + envDir: string + env: Record + resolve: Required & { + alias: Alias[] + } + plugins: readonly Plugin[] + css: ResolvedCSSOptions + json: Required + esbuild: ESBuildOptions | false + server: ResolvedServerOptions + dev: ResolvedDevEnvironmentOptions + /** @experimental */ + builder: ResolvedBuilderOptions | undefined + build: ResolvedBuildOptions + preview: ResolvedPreviewOptions + ssr: ResolvedSSROptions + assetsInclude: (file: string) => boolean + logger: Logger + createResolver: (options?: Partial) => ResolveFn + optimizeDeps: DepOptimizationOptions + /** @internal */ + packageCache: PackageCache + worker: ResolvedWorkerOptions + appType: AppType + experimental: ExperimentalOptions + environments: Record + /** + * The token to connect to the WebSocket server from browsers. + * + * We recommend using `import.meta.hot` rather than connecting + * to the WebSocket server directly. + * If you have a usecase that requires connecting to the WebSocket + * server, please create an issue so that we can discuss. + * + * @deprecated + */ + webSocketToken: string + /** @internal */ + fsDenyGlob: AnymatchFn + /** @internal */ + safeModulePaths: Set + /** @internal */ + additionalAllowedHosts: string[] + } & PluginHookUtils + > {} // inferred ones are omitted export const configDefaults = Object.freeze({ @@ -673,6 +706,7 @@ export const configDefaults = Object.freeze({ }, legacy: { proxySsrExternalModules: false, + skipWebSocketTokenCheck: false, }, logLevel: 'info', customLogger: undefined, @@ -742,6 +776,7 @@ function resolveEnvironmentOptions( options: EnvironmentOptions, alias: Alias[], preserveSymlinks: boolean, + forceOptimizeDeps: boolean | undefined, logger: Logger, environmentName: string, // Backward compatibility @@ -771,6 +806,7 @@ function resolveEnvironmentOptions( optimizeDeps: resolveDepOptimizationOptions( options.optimizeDeps, resolve.preserveSymlinks, + forceOptimizeDeps, consumer, ), dev: resolveDevEnvironmentOptions( @@ -887,7 +923,11 @@ function resolveEnvironmentResolveOptions( isSsrTargetWebworkerEnvironment ? DEFAULT_CLIENT_CONDITIONS : DEFAULT_SERVER_CONDITIONS.filter((c) => c !== 'browser'), - enableBuiltinNoExternalCheck: !!isSsrTargetWebworkerEnvironment, + builtins: + resolve?.builtins ?? + (consumer === 'server' && !isSsrTargetWebworkerEnvironment + ? nodeLikeBuiltins + : []), }, resolve ?? {}, ) @@ -943,6 +983,7 @@ function resolveResolveOptions( function resolveDepOptimizationOptions( optimizeDeps: DepOptimizationOptions | undefined, preserveSymlinks: boolean, + forceOptimizeDeps: boolean | undefined, consumer: 'client' | 'server' | undefined, ): DepOptimizationOptions { return mergeWithDefaults( @@ -953,6 +994,7 @@ function resolveDepOptimizationOptions( esbuildOptions: { preserveSymlinks, }, + force: forceOptimizeDeps ?? configDefaults.optimizeDeps.force, }, optimizeDeps ?? {}, ) @@ -996,6 +1038,7 @@ export async function resolveConfig( config.root, config.logLevel, config.customLogger, + config.configLoader, ) if (loadResult) { config = mergeConfig(loadResult.config, config) @@ -1164,6 +1207,7 @@ export async function resolveConfig( config.environments[environmentName], resolvedDefaultResolve.alias, resolvedDefaultResolve.preserveSymlinks, + inlineConfig.forceOptimizeDeps, logger, environmentName, config.experimental?.skipSsrTransform, @@ -1358,6 +1402,8 @@ export async function resolveConfig( const base = withTrailingSlash(resolvedBase) + const preview = resolvePreviewOptions(config.preview, server) + resolved = { configFile: configFile ? normalizePath(configFile) : undefined, configFileDependencies: configFileDependencies.map((name) => @@ -1388,7 +1434,7 @@ export async function resolveConfig( }, server, builder, - preview: resolvePreviewOptions(config.preview, server), + preview, envDir, env: { ...userEnv, @@ -1420,6 +1466,13 @@ export async function resolveConfig( environments: resolvedEnvironments, + // random 72 bits (12 base64 chars) + // at least 64bits is recommended + // https://owasp.org/www-community/vulnerabilities/Insufficient_Session-ID_Length + webSocketToken: Buffer.from( + crypto.getRandomValues(new Uint8Array(9)), + ).toString('base64url'), + getSortedPlugins: undefined!, getSortedPluginHooks: undefined!, @@ -1460,6 +1513,7 @@ export async function resolveConfig( }, ), safeModulePaths: new Set(), + additionalAllowedHosts: getAdditionalAllowedHosts(server, preview), } resolved = { ...config, @@ -1642,11 +1696,18 @@ export async function loadConfigFromFile( configRoot: string = process.cwd(), logLevel?: LogLevel, customLogger?: Logger, + configLoader: 'bundle' | 'runner' = 'bundle', ): Promise<{ path: string config: UserConfig dependencies: string[] } | null> { + if (configLoader !== 'bundle' && configLoader !== 'runner') { + throw new Error( + `Unsupported configLoader: ${configLoader}. Accepted values are 'bundle' and 'runner'.`, + ) + } + const start = performance.now() const getTime = () => `${(performance.now() - start).toFixed(2)}ms` @@ -1672,28 +1733,23 @@ export async function loadConfigFromFile( return null } - const isESM = - typeof process.versions.deno === 'string' || isFilePathESM(resolvedPath) - try { - const bundled = await bundleConfigFile(resolvedPath, isESM) - const userConfig = await loadConfigFromBundledFile( - resolvedPath, - bundled.code, - isESM, - ) - debug?.(`bundled config file loaded in ${getTime()}`) - - const config = await (typeof userConfig === 'function' - ? userConfig(configEnv) - : userConfig) + const resolver = + configLoader === 'bundle' ? bundleAndLoadConfigFile : importConfigFile + const { configExport, dependencies } = await resolver(resolvedPath) + debug?.(`config file loaded in ${getTime()}`) + + const config = await (typeof configExport === 'function' + ? configExport(configEnv) + : configExport) if (!isObject(config)) { throw new Error(`config must export or return an object.`) } + return { path: normalizePath(resolvedPath), config, - dependencies: bundled.dependencies, + dependencies, } } catch (e) { createLogger(logLevel, { customLogger }).error( @@ -1706,6 +1762,33 @@ export async function loadConfigFromFile( } } +async function importConfigFile(resolvedPath: string) { + const { module, dependencies } = await runnerImport<{ + default: UserConfigExport + }>(resolvedPath) + return { + configExport: module.default, + dependencies, + } +} + +async function bundleAndLoadConfigFile(resolvedPath: string) { + const isESM = + typeof process.versions.deno === 'string' || isFilePathESM(resolvedPath) + + const bundled = await bundleConfigFile(resolvedPath, isESM) + const userConfig = await loadConfigFromBundledFile( + resolvedPath, + bundled.code, + isESM, + ) + + return { + configExport: userConfig, + dependencies: bundled.dependencies, + } +} + async function bundleConfigFile( fileName: string, isESM: boolean, @@ -1765,6 +1848,7 @@ async function bundleConfigFile( preserveSymlinks: false, packageCache, isRequire, + builtins: nodeLikeBuiltins, })?.id } @@ -1783,7 +1867,7 @@ async function bundleConfigFile( // With the `isNodeBuiltin` check above, this check captures if the builtin is a // non-node built-in, which esbuild doesn't know how to handle. In that case, we // externalize it so the non-node runtime handles it instead. - if (isBuiltin(id)) { + if (isNodeLikeBuiltin(id)) { return { external: true } } diff --git a/packages/vite/src/node/constants.ts b/packages/vite/src/node/constants.ts index 0f865742c4cc1a..5a9dc7f935df26 100644 --- a/packages/vite/src/node/constants.ts +++ b/packages/vite/src/node/constants.ts @@ -133,6 +133,7 @@ export const KNOWN_ASSET_TYPES = [ 'webp', 'avif', 'cur', + 'jxl', // media 'mp4', @@ -183,6 +184,13 @@ export const DEFAULT_PREVIEW_PORT = 4173 export const DEFAULT_ASSETS_INLINE_LIMIT = 4096 +// the regex to allow loopback address origins: +// - localhost domains (which will always resolve to the loopback address by RFC 6761 section 6.3) +// - 127.0.0.1 +// - ::1 +export const defaultAllowedOrigins = + /^https?:\/\/(?:(?:[^:]+\.)?localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/ + export const METADATA_FILENAME = '_metadata.json' export const ERR_OPTIMIZE_DEPS_PROCESSING_ERROR = diff --git a/packages/vite/src/node/external.ts b/packages/vite/src/node/external.ts index 7b95d6aa92b959..40e0459cc42e1b 100644 --- a/packages/vite/src/node/external.ts +++ b/packages/vite/src/node/external.ts @@ -155,7 +155,9 @@ function createIsExternal( } let isExternal = false if (id[0] !== '.' && !path.isAbsolute(id)) { - isExternal = isBuiltin(id) || isConfiguredAsExternal(id, importer) + isExternal = + isBuiltin(environment.config.resolve.builtins, id) || + isConfiguredAsExternal(id, importer) } processedIds.set(id, isExternal) return isExternal diff --git a/packages/vite/src/node/http.ts b/packages/vite/src/node/http.ts index ec1bf5e645641d..32c47461f7fd56 100644 --- a/packages/vite/src/node/http.ts +++ b/packages/vite/src/node/http.ts @@ -24,6 +24,18 @@ export interface CommonServerOptions { * Set to 0.0.0.0 to listen on all addresses, including LAN and public addresses. */ host?: string | boolean + /** + * The hostnames that Vite is allowed to respond to. + * `localhost` and subdomains under `.localhost` and all IP addresses are allowed by default. + * When using HTTPS, this check is skipped. + * + * If a string starts with `.`, it will allow that hostname without the `.` and all subdomains under the hostname. + * For example, `.example.com` will allow `example.com`, `foo.example.com`, and `foo.bar.example.com`. + * + * If set to `true`, the server is allowed to respond to requests for any hosts. + * This is not recommended as it will be vulnerable to DNS rebinding attacks. + */ + allowedHosts?: string[] | true /** * Enable TLS + HTTP/2. * Note: this downgrades to TLS only when the proxy option is also used. @@ -59,8 +71,14 @@ export interface CommonServerOptions { /** * Configure CORS for the dev server. * Uses https://github.com/expressjs/cors. + * + * When enabling this option, **we recommend setting a specific value + * rather than `true`** to avoid exposing the source code to untrusted origins. + * * Set to `true` to allow all methods from any origin, or configure separately * using an object. + * + * @default false */ cors?: CorsOptions | boolean /** @@ -73,6 +91,12 @@ export interface CommonServerOptions { * https://github.com/expressjs/cors#configuration-options */ export interface CorsOptions { + /** + * Configures the Access-Control-Allow-Origin CORS header. + * + * **We recommend setting a specific value rather than + * `true`** to avoid exposing the source code to untrusted origins. + */ origin?: | CorsOrigin | (( diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index ae52db468474f1..ee5e4e9c820d28 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -31,10 +31,14 @@ export { DevEnvironment, type DevEnvironmentContext, } from './server/environment' +export { runnerImport } from './ssr/runnerImport' export { BuildEnvironment } from './build' export { fetchModule, type FetchModuleOptions } from './ssr/fetchModule' -export { createServerModuleRunner } from './ssr/runtime/serverModuleRunner' +export { + createServerModuleRunner, + createServerModuleRunnerTransport, +} from './ssr/runtime/serverModuleRunner' export { createServerHotChannel } from './server/hmr' export { ssrTransform as moduleRunnerTransform } from './ssr/ssrTransform' export type { ModuleRunnerTransformOptions } from './ssr/ssrTransform' @@ -169,6 +173,9 @@ export type { HotChannel, ServerHotChannel, HotChannelClient, + NormalizedHotChannel, + NormalizedHotChannelClient, + NormalizedServerHotChannel, } from './server/hmr' export type { FetchFunction, FetchResult } from 'vite/module-runner' diff --git a/packages/vite/src/node/optimizer/esbuildDepPlugin.ts b/packages/vite/src/node/optimizer/esbuildDepPlugin.ts index 1bdb2d2125d539..7b065cbb0a8ea0 100644 --- a/packages/vite/src/node/optimizer/esbuildDepPlugin.ts +++ b/packages/vite/src/node/optimizer/esbuildDepPlugin.ts @@ -115,7 +115,7 @@ export function esbuildDepPlugin( namespace: 'optional-peer-dep', } } - if (environment.config.consumer === 'server' && isBuiltin(resolved)) { + if (isBuiltin(environment.config.resolve.builtins, resolved)) { return } if (isExternalUrl(resolved)) { diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 1ea15cd165e6d3..8dcf3da90675af 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -373,24 +373,36 @@ export async function loadCachedDepOptimizationMetadata( if (cachedMetadata.lockfileHash !== getLockfileHash(environment)) { environment.logger.info( 'Re-optimizing dependencies because lockfile has changed', + { + timestamp: true, + }, ) } else if (cachedMetadata.configHash !== getConfigHash(environment)) { environment.logger.info( 'Re-optimizing dependencies because vite config has changed', + { + timestamp: true, + }, ) } else { - log?.('Hash is consistent. Skipping. Use --force to override.') + log?.( + `(${environment.name}) Hash is consistent. Skipping. Use --force to override.`, + ) // Nothing to commit or cancel as we are using the cache, we only // need to resolve the processing promise so requests can move on return cachedMetadata } } } else { - environment.logger.info('Forced re-optimization of dependencies') + environment.logger.info('Forced re-optimization of dependencies', { + timestamp: true, + }) } // Start with a fresh cache - debug?.(colors.green(`removing old cache dir ${depsCacheDir}`)) + debug?.( + `(${environment.name}) ${colors.green(`removing old cache dir ${depsCacheDir}`)}`, + ) await fsp.rm(depsCacheDir, { recursive: true, force: true }) } @@ -1206,7 +1218,11 @@ const lockfileFormats = [ manager: 'pnpm', }, { - name: 'bun.lockb', + path: 'bun.lock', + checkPatchesDir: 'patches', + manager: 'bun', + }, + { path: 'bun.lockb', checkPatchesDir: 'patches', manager: 'bun', diff --git a/packages/vite/src/node/optimizer/scan.ts b/packages/vite/src/node/optimizer/scan.ts index 5c7562c5ba8f12..55b0111d57a093 100644 --- a/packages/vite/src/node/optimizer/scan.ts +++ b/packages/vite/src/node/optimizer/scan.ts @@ -105,11 +105,6 @@ export function devToScanEnvironment( } as unknown as ScanEnvironment } -type ResolveIdOptions = Omit< - Parameters[2], - 'environment' -> - const debug = createDebugger('vite:deps') const htmlTypesRE = /\.(html|vue|svelte|astro|imba)$/ @@ -383,27 +378,19 @@ function esbuildScanPlugin( async function resolveId( id: string, importer?: string, - options?: ResolveIdOptions, ): Promise { return environment.pluginContainer.resolveId( id, importer && normalizePath(importer), - { - ...options, - scan: true, - }, + { scan: true }, ) } - const resolve = async ( - id: string, - importer?: string, - options?: ResolveIdOptions, - ) => { + const resolve = async (id: string, importer?: string) => { const key = id + (importer && path.dirname(importer)) if (seen.has(key)) { return seen.get(key) } - const resolved = await resolveId(id, importer, options) + const resolved = await resolveId(id, importer) const res = resolved?.id seen.set(key, res) return res @@ -633,18 +620,14 @@ function esbuildScanPlugin( // avoid matching windows volume filter: /^[\w@][^:]/, }, - async ({ path: id, importer, pluginData }) => { + async ({ path: id, importer }) => { if (moduleListContains(exclude, id)) { return externalUnlessEntry({ path: id }) } if (depImports[id]) { return externalUnlessEntry({ path: id }) } - const resolved = await resolve(id, importer, { - custom: { - depScan: { loader: pluginData?.htmlType?.loader }, - }, - }) + const resolved = await resolve(id, importer) if (resolved) { if (shouldExternalizeDep(resolved, id)) { return externalUnlessEntry({ path: id }) @@ -706,13 +689,9 @@ function esbuildScanPlugin( { filter: /.*/, }, - async ({ path: id, importer, pluginData }) => { + async ({ path: id, importer }) => { // use vite resolver to support urls and omitted extensions - const resolved = await resolve(id, importer, { - custom: { - depScan: { loader: pluginData?.htmlType?.loader }, - }, - }) + const resolved = await resolve(id, importer) if (resolved) { if ( shouldExternalizeDep(resolved, id) || diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index b9447d9cf17f68..ac284835b34af8 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -57,6 +57,8 @@ export function registerCustomMime(): void { mrmime.mimes['flac'] = 'audio/flac' // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types mrmime.mimes['eot'] = 'application/vnd.ms-fontobject' + // https://github.com/lukeed/mrmime/issues/10 + mrmime.mimes['jxl'] = 'image/jxl' } export function renderAssetUrlInJS( diff --git a/packages/vite/src/node/plugins/clientInjections.ts b/packages/vite/src/node/plugins/clientInjections.ts index b64668d5ec2e21..f59f7a77e9acee 100644 --- a/packages/vite/src/node/plugins/clientInjections.ts +++ b/packages/vite/src/node/plugins/clientInjections.ts @@ -76,6 +76,7 @@ export function clientInjectionsPlugin(config: ResolvedConfig): Plugin { const hmrTimeoutReplacement = escapeReplacement(timeout) const hmrEnableOverlayReplacement = escapeReplacement(overlay) const hmrConfigNameReplacement = escapeReplacement(hmrConfigName) + const wsTokenReplacement = escapeReplacement(config.webSocketToken) injectConfigValues = (code: string) => { return code @@ -90,6 +91,7 @@ export function clientInjectionsPlugin(config: ResolvedConfig): Plugin { .replace(`__HMR_TIMEOUT__`, hmrTimeoutReplacement) .replace(`__HMR_ENABLE_OVERLAY__`, hmrEnableOverlayReplacement) .replace(`__HMR_CONFIG_NAME__`, hmrConfigNameReplacement) + .replace(`__WS_TOKEN__`, wsTokenReplacement) } }, async transform(code, id, options) { diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 12207db233b406..3f1f6e528650ff 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -3243,6 +3243,25 @@ async function compileLightningCSS( line: e.loc.line, column: e.loc.column - 1, // 1-based } + // add friendly error for https://github.com/parcel-bundler/lightningcss/issues/39 + try { + const code = fs.readFileSync(e.fileName, 'utf-8') + const commonIeMessage = + ', which was used in the past to support old Internet Explorer versions.' + + ' This is not a valid CSS syntax and will be ignored by modern browsers. ' + + '\nWhile this is not supported by LightningCSS, you can set `css.lightningcss.errorRecovery: true` to strip these codes.' + if (/[\s;{]\*[a-zA-Z-][\w-]+\s*:/.test(code)) { + // https://stackoverflow.com/a/1667560 + e.message += + '.\nThis file contains star property hack (e.g. `*zoom`)' + + commonIeMessage + } else if (/min-width:\s*0\\0/.test(code)) { + // https://stackoverflow.com/a/14585820 + e.message += + '.\nThis file contains @media zero hack (e.g. `@media (min-width: 0\\0)`)' + + commonIeMessage + } + } catch {} } throw e } diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index a5e960c8e3bb68..4697454db613a3 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -321,7 +321,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { url: string, pos: number, forceSkipImportAnalysis: boolean = false, - ): Promise<[string, string]> => { + ): Promise<[string, string | null]> => { url = stripBase(url, base) let importerFile = importer @@ -355,7 +355,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { if (!resolved || resolved.meta?.['vite:alias']?.noResolved) { // in ssr, we should let node handle the missing modules if (ssr) { - return [url, url] + return [url, null] } // fix#9534, prevent the importerModuleNode being stopped from propagating updates importerModule.isSelfAccepting = false @@ -396,32 +396,35 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { url = injectQuery(url, versionMatch[1]) } } + } + try { + // delay setting `isSelfAccepting` until the file is actually used (#7870) + // We use an internal function to avoid resolving the url again + const depModule = await moduleGraph._ensureEntryFromUrl( + unwrapId(url), + canSkipImportAnalysis(url) || forceSkipImportAnalysis, + resolved, + ) // check if the dep has been hmr updated. If yes, we need to attach // its last updated timestamp to force the browser to fetch the most // up-to-date version of this module. - try { - // delay setting `isSelfAccepting` until the file is actually used (#7870) - // We use an internal function to avoid resolving the url again - const depModule = await moduleGraph._ensureEntryFromUrl( - unwrapId(url), - canSkipImportAnalysis(url) || forceSkipImportAnalysis, - resolved, - ) - if (depModule.lastHMRTimestamp > 0) { - url = injectQuery(url, `t=${depModule.lastHMRTimestamp}`) - } - } catch (e: any) { - // it's possible that the dep fails to resolve (non-existent import) - // attach location to the missing import - e.pos = pos - throw e + if ( + environment.config.consumer === 'client' && + depModule.lastHMRTimestamp > 0 + ) { + url = injectQuery(url, `t=${depModule.lastHMRTimestamp}`) } - - // prepend base - if (!ssr) url = joinUrlSegments(base, url) + } catch (e: any) { + // it's possible that the dep fails to resolve (non-existent import) + // attach location to the missing import + e.pos = pos + throw e } + // prepend base + if (!ssr) url = joinUrlSegments(base, url) + return [url, resolved.id] } @@ -517,7 +520,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { if (shouldExternalize(environment, specifier, importer)) { return } - if (isBuiltin(specifier)) { + if (isBuiltin(environment.config.resolve.builtins, specifier)) { return } } @@ -547,7 +550,8 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { } // normalize - const [url, resolvedId] = await normalizeUrl(specifier, start) + let [url, resolvedId] = await normalizeUrl(specifier, start) + resolvedId = resolvedId || url // record as safe modules // safeModulesPath should not include the base prefix. @@ -751,9 +755,40 @@ 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)) + let [normalized, resolvedId] = await normalizeUrl(url, start).catch( + () => [], + ) + if (resolvedId) { + const mod = moduleGraph.getModuleById(resolvedId) + if (!mod) { + this.error( + `module was not found for ${JSON.stringify(resolvedId)}`, + start, + ) + return + } + normalized = mod.url + } else { + try { + // this fallback is for backward compat and will be removed in Vite 7 + const [resolved] = await moduleGraph.resolveUrl(toAbsoluteUrl(url)) + normalized = resolved + if (resolved) { + this.warn({ + message: + `Failed to resolve ${JSON.stringify(url)} from ${importer}.` + + ' An id should be written. Did you pass a URL?', + pos: start, + }) + } + } catch { + this.error(`Failed to resolve ${JSON.stringify(url)}`, start) + return + } + } normalizedAcceptedUrls.add(normalized) - str().overwrite(start, end, JSON.stringify(normalized), { + const hmrAccept = normalizeHmrUrl(normalized) + str().overwrite(start, end, JSON.stringify(hmrAccept), { contentOnly: true, }) } diff --git a/packages/vite/src/node/plugins/json.ts b/packages/vite/src/node/plugins/json.ts index 33c1c0d27aeaac..0234d471d39243 100644 --- a/packages/vite/src/node/plugins/json.ts +++ b/packages/vite/src/node/plugins/json.ts @@ -87,7 +87,7 @@ export function jsonPlugin( } return { - code: `export default JSON.parse(${JSON.stringify(json)})`, + code: `export default /* #__PURE__ */ JSON.parse(${JSON.stringify(json)})`, map: { mappings: '' }, } } @@ -120,7 +120,7 @@ function serializeValue(value: unknown): string { value != null && valueAsString.length > 10 * 1000 ) { - return `JSON.parse(${JSON.stringify(valueAsString)})` + return `/* #__PURE__ */ JSON.parse(${JSON.stringify(valueAsString)})` } return valueAsString } diff --git a/packages/vite/src/node/plugins/reporter.ts b/packages/vite/src/node/plugins/reporter.ts index 4f97184dc0522e..10b9334d398585 100644 --- a/packages/vite/src/node/plugins/reporter.ts +++ b/packages/vite/src/node/plugins/reporter.ts @@ -24,7 +24,7 @@ type LogEntry = { mapSize: number | null } -const COMPRESSIBLE_ASSETS_RE = /\.(?:html|json|svg|txt|xml|xhtml)$/ +const COMPRESSIBLE_ASSETS_RE = /\.(?:html|json|svg|txt|xml|xhtml|wasm)$/ export function buildReporterPlugin(config: ResolvedConfig): Plugin { const compress = promisify(gzip) diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index 2306a2600a1c50..6c93f41c12b381 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -25,10 +25,10 @@ import { isDataUrl, isExternalUrl, isInNodeModules, + isNodeLikeBuiltin, isNonDriveRelativeAbsolutePath, isObject, isOptimizable, - isTsRequest, normalizePath, safeRealpathSync, tryStatSync, @@ -97,9 +97,9 @@ export interface EnvironmentResolveOptions { */ external?: string[] | true /** - * @internal + * Array of strings or regular expressions that indicate what modules are builtin for the environment. */ - enableBuiltinNoExternalCheck?: boolean + builtins?: (string | RegExp)[] } export interface ResolveOptions extends EnvironmentResolveOptions { @@ -124,10 +124,7 @@ interface ResolvePluginOptions { tryPrefix?: string preferRelative?: boolean isRequire?: boolean - // #3040 - // when the importer is a ts module, - // if the specifier requests a non-existent `.js/jsx/mjs/cjs` file, - // should also try import from `.ts/tsx/mts/cts` source file as fallback. + /** @deprecated */ isFromTsImporter?: boolean // True when resolving during the scan phase to discover dependencies scan?: boolean @@ -173,11 +170,8 @@ interface ResolvePluginOptions { } export interface InternalResolveOptions - extends Required>, - ResolvePluginOptions { - /** @internal this is always optional for backward compat */ - enableBuiltinNoExternalCheck?: boolean -} + 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) @@ -243,18 +237,6 @@ export function resolvePlugin( } } - if (importer) { - if ( - isTsRequest(importer) || - resolveOpts.custom?.depScan?.loader?.startsWith('ts') - ) { - options.isFromTsImporter = true - } else { - const moduleLang = this.getModuleInfo(importer)?.meta.vite?.lang - options.isFromTsImporter = moduleLang && isTsRequest(`.${moduleLang}`) - } - } - let res: string | PartialResolvedId | undefined // resolve pre-bundled deps requests, these could be resolved by @@ -422,47 +404,67 @@ export function resolvePlugin( return res } - // node built-ins. - // externalize if building for a node compatible environment, otherwise redirect to empty module - if (isBuiltin(id)) { - if (currentEnvironmentOptions.consumer === 'server') { - if ( - options.enableBuiltinNoExternalCheck && - 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. - (options.external === true || !options.external.includes(id)) - ) { - let message = `Cannot bundle Node.js built-in "${id}"` - if (importer) { - message += ` imported from "${path.relative( - process.cwd(), - importer, - )}"` - } - message += `. Consider disabling environments.${this.environment.name}.noExternal or remove the built-in dependency.` - this.error(message) + // built-ins + // externalize if building for a server environment, otherwise redirect to an empty module + if ( + currentEnvironmentOptions.consumer === 'server' && + isBuiltin(options.builtins, id) + ) { + return options.idOnly + ? id + : { id, external: true, moduleSideEffects: false } + } else if ( + currentEnvironmentOptions.consumer === 'server' && + isNodeLikeBuiltin(id) + ) { + if (!(options.external === true || options.external.includes(id))) { + let message = `Automatically externalized node built-in module "${id}"` + if (importer) { + message += ` imported from "${path.relative( + process.cwd(), + importer, + )}"` } + message += `. Consider adding it to environments.${this.environment.name}.external if it is intended.` + this.error(message) + } - return options.idOnly - ? id - : { id, external: true, moduleSideEffects: false } - } else { - if (!asSrc) { - debug?.( - `externalized node built-in "${id}" to empty module. ` + - `(imported by: ${colors.white(colors.dim(importer))})`, - ) - } else if (isProduction) { - this.warn( - `Module "${id}" has been externalized for browser compatibility, imported by "${importer}". ` + - `See https://vite.dev/guide/troubleshooting.html#module-externalized-for-browser-compatibility for more details.`, - ) + return options.idOnly + ? id + : { id, external: true, moduleSideEffects: false } + } else if ( + currentEnvironmentOptions.consumer === 'client' && + isNodeLikeBuiltin(id) + ) { + if ( + 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. + (options.external === true || !options.external.includes(id)) + ) { + let message = `Cannot bundle built-in module "${id}"` + if (importer) { + message += ` imported from "${path.relative( + process.cwd(), + importer, + )}"` } - return isProduction - ? browserExternalId - : `${browserExternalId}:${id}` + message += `. Consider disabling environments.${this.environment.name}.noExternal or remove the built-in dependency.` + this.error(message) } + + if (!asSrc) { + debug?.( + `externalized node built-in "${id}" to empty module. ` + + `(imported by: ${colors.white(colors.dim(importer))})`, + ) + } else if (isProduction) { + this.warn( + `Module "${id}" has been externalized for browser compatibility, imported by "${importer}". ` + + `See https://vite.dev/guide/troubleshooting.html#module-externalized-for-browser-compatibility for more details.`, + ) + } + return isProduction ? browserExternalId : `${browserExternalId}:${id}` } } @@ -598,7 +600,7 @@ function tryCleanFsResolve( let res: string | undefined // If path.dirname is a valid directory, try extensions and ts resolution logic - const possibleJsToTs = options.isFromTsImporter && isPossibleTsOutput(file) + const possibleJsToTs = isPossibleTsOutput(file) if (possibleJsToTs || options.extensions.length || tryPrefix) { const dirPath = path.dirname(file) if (isDirectory(dirPath)) { @@ -720,8 +722,10 @@ export function tryNodeResolve( basedir = root } + const isModuleBuiltin = (id: string) => isBuiltin(options.builtins, id) + let selfPkg = null - if (!isBuiltin(id) && !id.includes('\0') && bareImportRE.test(id)) { + if (!isModuleBuiltin(id) && !id.includes('\0') && bareImportRE.test(id)) { // check if it's a self reference dep. const selfPackageData = findNearestPackageData(basedir, packageCache) selfPkg = @@ -738,7 +742,7 @@ export function tryNodeResolve( // if so, we can resolve to a special id that errors only when imported. if ( basedir !== root && // root has no peer dep - !isBuiltin(id) && + !isModuleBuiltin(id) && !id.includes('\0') && bareImportRE.test(id) ) { diff --git a/packages/vite/src/node/plugins/worker.ts b/packages/vite/src/node/plugins/worker.ts index b7eea41aac0a6a..15dd48879b7479 100644 --- a/packages/vite/src/node/plugins/worker.ts +++ b/packages/vite/src/node/plugins/worker.ts @@ -15,7 +15,7 @@ import { BuildEnvironment, createToImportMetaURLBasedRelativeRuntime, injectEnvironmentToHooks, - onRollupWarning, + onRollupLog, toOutputFilePathInJS, } from '../build' import { cleanUrl } from '../../shared/utils' @@ -85,8 +85,8 @@ async function bundleWorkerEntry( plugins: workerEnvironment.plugins.map((p) => injectEnvironmentToHooks(workerEnvironment, p), ), - onwarn(warning, warn) { - onRollupWarning(warning, warn, workerEnvironment) + onLog(level, log) { + onRollupLog(level, log, workerEnvironment) }, preserveEntrySignatures: false, }) diff --git a/packages/vite/src/node/plugins/workerImportMetaUrl.ts b/packages/vite/src/node/plugins/workerImportMetaUrl.ts index 8d8d316a4ec214..9c85e458ef961c 100644 --- a/packages/vite/src/node/plugins/workerImportMetaUrl.ts +++ b/packages/vite/src/node/plugins/workerImportMetaUrl.ts @@ -1,7 +1,9 @@ import path from 'node:path' import MagicString from 'magic-string' -import type { RollupError } from 'rollup' +import type { RollupAstNode, RollupError } from 'rollup' +import { parseAstAsync } from 'rollup/parseAst' import { stripLiteral } from 'strip-literal' +import type { Expression, ExpressionStatement } from 'estree' import type { ResolvedConfig } from '../config' import type { Plugin } from '../plugin' import { evalValue, injectQuery, transformStableResult } from '../utils' @@ -25,16 +27,92 @@ function err(e: string, pos: number) { return error } -function parseWorkerOptions( +function findClosingParen(input: string, fromIndex: number) { + let count = 1 + + for (let i = fromIndex + 1; i < input.length; i++) { + if (input[i] === '(') count++ + if (input[i] === ')') count-- + if (count === 0) return i + } + + return -1 +} + +function extractWorkerTypeFromAst( + expression: Expression, + optsStartIndex: number, +): 'classic' | 'module' | undefined { + if (expression.type !== 'ObjectExpression') { + return + } + + let lastSpreadElementIndex = -1 + let typeProperty = null + let typePropertyIndex = -1 + + for (let i = 0; i < expression.properties.length; i++) { + const property = expression.properties[i] + + if (property.type === 'SpreadElement') { + lastSpreadElementIndex = i + continue + } + + if ( + property.type === 'Property' && + ((property.key.type === 'Identifier' && property.key.name === 'type') || + (property.key.type === 'Literal' && property.key.value === 'type')) + ) { + typeProperty = property + typePropertyIndex = i + } + } + + if (typePropertyIndex === -1 && lastSpreadElementIndex === -1) { + // No type property or spread element in use. Assume safe usage and default to classic + return 'classic' + } + + if (typePropertyIndex < lastSpreadElementIndex) { + throw err( + 'Expected object spread to be used before the definition of the type property. ' + + 'Vite needs a static value for the type property to correctly infer it.', + optsStartIndex, + ) + } + + if (typeProperty?.value.type !== 'Literal') { + throw err( + 'Expected worker options type property to be a literal value.', + optsStartIndex, + ) + } + + // Silently default to classic type like the getWorkerType method + return typeProperty?.value.value === 'module' ? 'module' : 'classic' +} + +async function parseWorkerOptions( rawOpts: string, optsStartIndex: number, -): WorkerOptions { +): Promise { let opts: WorkerOptions = {} try { opts = evalValue(rawOpts) } catch { + const optsNode = ( + (await parseAstAsync(`(${rawOpts})`)) + .body[0] as RollupAstNode + ).expression + + const type = extractWorkerTypeFromAst(optsNode, optsStartIndex) + if (type) { + return { type } + } + throw err( - 'Vite is unable to parse the worker options as the value is not static.' + + 'Vite is unable to parse the worker options as the value is not static. ' + 'To ignore this error, please use /* @vite-ignore */ in the worker options.', optsStartIndex, ) @@ -54,12 +132,16 @@ function parseWorkerOptions( return opts } -function getWorkerType(raw: string, clean: string, i: number): WorkerType { +async function getWorkerType( + raw: string, + clean: string, + i: number, +): Promise { const commaIndex = clean.indexOf(',', i) if (commaIndex === -1) { return 'classic' } - const endIndex = clean.indexOf(')', i) + const endIndex = findClosingParen(clean, i) // case: ') ... ,' mean no worker options params if (commaIndex > endIndex) { @@ -82,7 +164,7 @@ function getWorkerType(raw: string, clean: string, i: number): WorkerType { return 'classic' } - const workerOpts = parseWorkerOptions(workerOptString, commaIndex + 1) + const workerOpts = await parseWorkerOptions(workerOptString, commaIndex + 1) if ( workerOpts.type && (workerOpts.type === 'module' || workerOpts.type === 'classic') @@ -152,12 +234,12 @@ export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin { } s ||= new MagicString(code) - const workerType = getWorkerType(code, cleanString, endIndex) + const workerType = await getWorkerType(code, cleanString, endIndex) const url = rawUrl.slice(1, -1) let file: string | undefined if (url[0] === '.') { file = path.resolve(path.dirname(id), url) - file = tryFsResolve(file, fsResolveOptions) ?? file + file = slash(tryFsResolve(file, fsResolveOptions) ?? file) } else { workerResolver ??= createBackCompatIdResolver(config, { extensions: [], diff --git a/packages/vite/src/node/preview.ts b/packages/vite/src/node/preview.ts index 614c16f8aaac06..5b92a8d7addc42 100644 --- a/packages/vite/src/node/preview.ts +++ b/packages/vite/src/node/preview.ts @@ -38,6 +38,7 @@ import { resolveConfig } from './config' import type { InlineConfig, ResolvedConfig } from './config' import { DEFAULT_PREVIEW_PORT } from './constants' import type { RequiredExceptFor } from './typeUtils' +import { hostCheckMiddleware } from './server/middlewares/hostCheck' export interface PreviewOptions extends CommonServerOptions {} @@ -55,6 +56,7 @@ export function resolvePreviewOptions( port: preview?.port ?? DEFAULT_PREVIEW_PORT, strictPort: preview?.strictPort ?? server.strictPort, host: preview?.host ?? server.host, + allowedHosts: preview?.allowedHosts ?? server.allowedHosts, https: preview?.https ?? server.https, open: preview?.open ?? server.open, proxy: preview?.proxy ?? server.proxy, @@ -202,6 +204,13 @@ export async function preview( app.use(corsMiddleware(typeof cors === 'boolean' ? {} : cors)) } + // host check (to prevent DNS rebinding attacks) + const { allowedHosts } = config.preview + // no need to check for HTTPS as HTTPS is not vulnerable to DNS rebinding attacks + if (allowedHosts !== true && !config.preview.https) { + app.use(hostCheckMiddleware(config, true)) + } + // proxy const { proxy } = config.preview if (proxy) { diff --git a/packages/vite/src/node/publicUtils.ts b/packages/vite/src/node/publicUtils.ts index d27fed085e0890..0f05a225b9dcce 100644 --- a/packages/vite/src/node/publicUtils.ts +++ b/packages/vite/src/node/publicUtils.ts @@ -9,6 +9,7 @@ export { DEFAULT_CLIENT_MAIN_FIELDS as defaultClientMainFields, DEFAULT_SERVER_CONDITIONS as defaultServerConditions, DEFAULT_SERVER_MAIN_FIELDS as defaultServerMainFields, + defaultAllowedOrigins, } from './constants' export { version as esbuildVersion } from 'esbuild' export { diff --git a/packages/vite/src/node/server/__tests__/pluginContainer.spec.ts b/packages/vite/src/node/server/__tests__/pluginContainer.spec.ts index acfcb1a29f578e..0967a1a0e3c96d 100644 --- a/packages/vite/src/node/server/__tests__/pluginContainer.spec.ts +++ b/packages/vite/src/node/server/__tests__/pluginContainer.spec.ts @@ -1,8 +1,10 @@ -import { describe, expect, it } from 'vitest' +import { stripVTControlCharacters } from 'node:util' +import { describe, expect, it, vi } from 'vitest' import type { UserConfig } from '../../config' import { resolveConfig } from '../../config' import type { Plugin } from '../../plugin' import { DevEnvironment } from '../environment' +import { createLogger } from '../../logger' describe('plugin container', () => { describe('getModuleInfo', () => { @@ -142,6 +144,55 @@ describe('plugin container', () => { }) }) + describe('options', () => { + it('should not throw errors when this.debug is called', async () => { + const plugin: Plugin = { + name: 'p1', + options() { + this.debug('test') + }, + } + await getDevEnvironment({ + plugins: [plugin], + }) + }) + + const logFunctions = ['info', 'warn'] as const + for (const logFunction of logFunctions) { + it(`should support this.${logFunction}`, async () => { + const logger = createLogger() + const mockedFn = vi + .spyOn(logger, logFunction) + .mockImplementation(() => {}) + const plugin: Plugin = { + name: 'p1', + options() { + this[logFunction]('test') + }, + } + await getDevEnvironment({ + plugins: [plugin], + customLogger: logger, + }) + expect(mockedFn).toHaveBeenCalledOnce() + }) + } + + it('should support this.error', async () => { + const plugin: Plugin = { + name: 'p1', + options() { + this.error('test') + }, + } + await expect(() => + getDevEnvironment({ + plugins: [plugin], + }), + ).rejects.toThrowError('test') + }) + }) + describe('load', () => { it('can resolve a secondary module', async () => { const entryUrl = '/x.js' @@ -212,6 +263,65 @@ describe('plugin container', () => { ) expect(result.code).equals('3') }) + + it('should not throw errors when this.debug is called', async () => { + const plugin: Plugin = { + name: 'p1', + load() { + this.debug({ message: 'test', pos: 12 }) + }, + } + const environment = await getDevEnvironment({ + plugins: [plugin], + }) + await environment.pluginContainer.load('foo') + }) + + const logFunctions = ['info', 'warn'] as const + for (const logFunction of logFunctions) { + it(`should support this.${logFunction}`, async () => { + const logger = createLogger() + const mockedFn = vi + .spyOn(logger, logFunction) + .mockImplementation(() => {}) + const plugin: Plugin = { + name: 'p1', + load() { + this[logFunction]({ message: 'test', pos: 12 }) + }, + } + const environment = await getDevEnvironment({ + plugins: [plugin], + customLogger: logger, + }) + await environment.pluginContainer.load('foo') + expect(mockedFn).toHaveBeenCalledOnce() + expect(stripVTControlCharacters(mockedFn.mock.calls[0][0])).toBe( + `${logFunction === 'warn' ? 'warning' : logFunction}: test\n` + + ' Plugin: p1', + ) + }) + } + + it('should support this.error', async () => { + const plugin: Plugin = { + name: 'p1', + load() { + this.error({ message: 'test', pos: 12 }) + }, + } + const environment = await getDevEnvironment({ + plugins: [plugin], + }) + await expect(() => environment.pluginContainer.load('foo')).rejects + .toThrowErrorMatchingInlineSnapshot(` + { + "message": "test", + "plugin": "p1", + "pos": 12, + } + `) + }) }) describe('resolveId', () => { diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 486b245d9c71de..b3c10bccf92456 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -44,7 +44,11 @@ import { reloadOnTsconfigChange } from '../plugins/esbuild' import { bindCLIShortcuts } from '../shortcuts' import type { BindCLIShortcutsOptions } from '../shortcuts' import { ERR_OUTDATED_OPTIMIZED_DEP } from '../../shared/constants' -import { CLIENT_DIR, DEFAULT_DEV_PORT } from '../constants' +import { + CLIENT_DIR, + DEFAULT_DEV_PORT, + defaultAllowedOrigins, +} from '../constants' import type { Logger } from '../logger' import { printServerUrls } from '../logger' import { warnFutureDeprecation } from '../deprecations' @@ -93,6 +97,7 @@ import type { TransformOptions, TransformResult } from './transformRequest' import { transformRequest } from './transformRequest' import { searchForPackageRoot, searchForWorkspaceRoot } from './searchRoot' import type { DevEnvironment } from './environment' +import { hostCheckMiddleware } from './middlewares/hostCheck' export interface ServerOptions extends CommonServerOptions { /** @@ -851,12 +856,19 @@ export async function _createServer( middlewares.use(timeMiddleware(root)) } - // cors (enabled by default) + // cors const { cors } = serverConfig if (cors !== false) { middlewares.use(corsMiddleware(typeof cors === 'boolean' ? {} : cors)) } + // host check (to prevent DNS rebinding attacks) + const { allowedHosts } = serverConfig + // no need to check for HTTPS as HTTPS is not vulnerable to DNS rebinding attacks + if (allowedHosts !== true && !serverConfig.https) { + middlewares.use(hostCheckMiddleware(config, false)) + } + middlewares.use(cachedTransformMiddleware(server)) // proxy @@ -1043,10 +1055,11 @@ export const serverConfigDefaults = Object.freeze({ port: DEFAULT_DEV_PORT, strictPort: false, host: 'localhost', + allowedHosts: [], https: undefined, open: false, proxy: undefined, - cors: true, + cors: { origin: defaultAllowedOrigins }, headers: {}, // hmr // ws @@ -1151,9 +1164,7 @@ async function restartServer(server: ViteDevServer) { let inlineConfig = server.config.inlineConfig if (server._forceOptimizeOnRestart) { inlineConfig = mergeConfig(inlineConfig, { - optimizeDeps: { - force: true, - }, + forceOptimizeDeps: true, }) } diff --git a/packages/vite/src/node/server/middlewares/__tests__/hostCheck.spec.ts b/packages/vite/src/node/server/middlewares/__tests__/hostCheck.spec.ts new file mode 100644 index 00000000000000..7186ba85f2644a --- /dev/null +++ b/packages/vite/src/node/server/middlewares/__tests__/hostCheck.spec.ts @@ -0,0 +1,112 @@ +import { describe, expect, test } from 'vitest' +import { + getAdditionalAllowedHosts, + isHostAllowedWithoutCache, +} from '../hostCheck' + +test('getAdditionalAllowedHosts', async () => { + const actual = getAdditionalAllowedHosts( + { + host: 'vite.host.example.com', + hmr: { + host: 'vite.hmr-host.example.com', + }, + origin: 'http://vite.origin.example.com:5173', + }, + { + host: 'vite.preview-host.example.com', + }, + ).sort() + expect(actual).toStrictEqual( + [ + 'vite.host.example.com', + 'vite.hmr-host.example.com', + 'vite.origin.example.com', + 'vite.preview-host.example.com', + ].sort(), + ) +}) + +describe('isHostAllowedWithoutCache', () => { + const allowCases = { + 'IP address': [ + '192.168.0.0', + '[::1]', + '127.0.0.1:5173', + '[2001:db8:0:0:1:0:0:1]:5173', + ], + localhost: [ + 'localhost', + 'localhost:5173', + 'foo.localhost', + 'foo.bar.localhost', + ], + specialProtocols: [ + // for electron browser window (https://github.com/webpack/webpack-dev-server/issues/3821) + 'file:///path/to/file.html', + // for browser extensions (https://github.com/webpack/webpack-dev-server/issues/3807) + 'chrome-extension://foo', + ], + } + + const disallowCases = { + 'IP address': ['255.255.255.256', '[:', '[::z]'], + localhost: ['localhos', 'localhost.foo'], + specialProtocols: ['mailto:foo@bar.com'], + others: [''], + } + + for (const [name, inputList] of Object.entries(allowCases)) { + test.each(inputList)(`allows ${name} (%s)`, (input) => { + const actual = isHostAllowedWithoutCache([], [], input) + expect(actual).toBe(true) + }) + } + + for (const [name, inputList] of Object.entries(disallowCases)) { + test.each(inputList)(`disallows ${name} (%s)`, (input) => { + const actual = isHostAllowedWithoutCache([], [], input) + expect(actual).toBe(false) + }) + } + + test('allows additionalAlloweHosts option', () => { + const additionalAllowedHosts = ['vite.example.com'] + const actual = isHostAllowedWithoutCache( + [], + additionalAllowedHosts, + 'vite.example.com', + ) + expect(actual).toBe(true) + }) + + test('allows single allowedHosts', () => { + const cases = { + allowed: ['example.com'], + disallowed: ['vite.dev'], + } + for (const c of cases.allowed) { + const actual = isHostAllowedWithoutCache(['example.com'], [], c) + expect(actual, c).toBe(true) + } + for (const c of cases.disallowed) { + const actual = isHostAllowedWithoutCache(['example.com'], [], c) + expect(actual, c).toBe(false) + } + }) + + test('allows all subdomain allowedHosts', () => { + const cases = { + allowed: ['example.com', 'foo.example.com', 'foo.bar.example.com'], + disallowed: ['vite.dev'], + } + for (const c of cases.allowed) { + const actual = isHostAllowedWithoutCache(['.example.com'], [], c) + expect(actual, c).toBe(true) + } + for (const c of cases.disallowed) { + const actual = isHostAllowedWithoutCache(['.example.com'], [], c) + expect(actual, c).toBe(false) + } + }) +}) diff --git a/packages/vite/src/node/server/middlewares/hostCheck.ts b/packages/vite/src/node/server/middlewares/hostCheck.ts new file mode 100644 index 00000000000000..755c38036cfa0e --- /dev/null +++ b/packages/vite/src/node/server/middlewares/hostCheck.ts @@ -0,0 +1,180 @@ +import net from 'node:net' +import type { Connect } from 'dep-types/connect' +import type { ResolvedConfig } from '../../config' +import type { ResolvedPreviewOptions, ResolvedServerOptions } from '../..' + +const allowedHostsServerCache = new WeakMap>() +const allowedHostsPreviewCache = new WeakMap>() + +const isFileOrExtensionProtocolRE = /^(?:file|.+-extension):/i + +export function getAdditionalAllowedHosts( + resolvedServerOptions: Pick, + resolvedPreviewOptions: Pick, +): string[] { + const list = [] + + // allow host option by default as that indicates that the user is + // expecting Vite to respond on that host + if ( + typeof resolvedServerOptions.host === 'string' && + resolvedServerOptions.host + ) { + list.push(resolvedServerOptions.host) + } + if ( + typeof resolvedServerOptions.hmr === 'object' && + resolvedServerOptions.hmr.host + ) { + list.push(resolvedServerOptions.hmr.host) + } + if ( + typeof resolvedPreviewOptions.host === 'string' && + resolvedPreviewOptions.host + ) { + list.push(resolvedPreviewOptions.host) + } + + // allow server origin by default as that indicates that the user is + // expecting Vite to respond on that host + if (resolvedServerOptions.origin) { + // some frameworks may pass the origin as a placeholder, so it's not + // possible to parse as URL, so use a try-catch here as a best effort + try { + const serverOriginUrl = new URL(resolvedServerOptions.origin) + list.push(serverOriginUrl.hostname) + } catch {} + } + + return list +} + +// Based on webpack-dev-server's `checkHeader` function: https://github.com/webpack/webpack-dev-server/blob/v5.2.0/lib/Server.js#L3086 +// https://github.com/webpack/webpack-dev-server/blob/v5.2.0/LICENSE +export function isHostAllowedWithoutCache( + allowedHosts: string[], + additionalAllowedHosts: string[], + host: string, +): boolean { + if (isFileOrExtensionProtocolRE.test(host)) { + return true + } + + // We don't care about malformed Host headers, + // because we only need to consider browser requests. + // Non-browser clients can send any value they want anyway. + // + // `Host = uri-host [ ":" port ]` + const trimmedHost = host.trim() + + // IPv6 + if (trimmedHost[0] === '[') { + const endIpv6 = trimmedHost.indexOf(']') + if (endIpv6 < 0) { + return false + } + // DNS rebinding attacks does not happen with IP addresses + return net.isIP(trimmedHost.slice(1, endIpv6)) === 6 + } + + // uri-host does not include ":" unless IPv6 address + const colonPos = trimmedHost.indexOf(':') + const hostname = + colonPos === -1 ? trimmedHost : trimmedHost.slice(0, colonPos) + + // DNS rebinding attacks does not happen with IP addresses + if (net.isIP(hostname) === 4) { + return true + } + + // allow localhost and .localhost by default as they always resolve to the loopback address + // https://datatracker.ietf.org/doc/html/rfc6761#section-6.3 + if (hostname === 'localhost' || hostname.endsWith('.localhost')) { + return true + } + + for (const additionalAllowedHost of additionalAllowedHosts) { + if (additionalAllowedHost === hostname) { + return true + } + } + + for (const allowedHost of allowedHosts) { + if (allowedHost === hostname) { + return true + } + + // allow all subdomains of it + // e.g. `.foo.example` will allow `foo.example`, `*.foo.example`, `*.*.foo.example`, etc + if ( + allowedHost[0] === '.' && + (allowedHost.slice(1) === hostname || hostname.endsWith(allowedHost)) + ) { + return true + } + } + + return false +} + +/** + * @param config resolved config + * @param isPreview whether it's for the preview server or not + * @param host the value of host header. See [RFC 9110 7.2](https://datatracker.ietf.org/doc/html/rfc9110#name-host-and-authority). + */ +export function isHostAllowed( + config: ResolvedConfig, + isPreview: boolean, + host: string, +): boolean { + const allowedHosts = isPreview + ? config.preview.allowedHosts + : config.server.allowedHosts + if (allowedHosts === true) { + return true + } + + const cache = isPreview ? allowedHostsPreviewCache : allowedHostsServerCache + if (!cache.has(config)) { + cache.set(config, new Set()) + } + + const cachedAllowedHosts = cache.get(config)! + if (cachedAllowedHosts.has(host)) { + return true + } + + const result = isHostAllowedWithoutCache( + allowedHosts, + config.additionalAllowedHosts, + host, + ) + if (result) { + cachedAllowedHosts.add(host) + } + return result +} + +export function hostCheckMiddleware( + config: ResolvedConfig, + isPreview: boolean, +): Connect.NextHandleFunction { + // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...` + return function viteHostCheckMiddleware(req, res, next) { + const hostHeader = req.headers.host + if (!hostHeader || !isHostAllowed(config, isPreview, hostHeader)) { + const hostname = hostHeader?.replace(/:\d+$/, '') + const hostnameWithQuotes = JSON.stringify(hostname) + const optionName = `${isPreview ? 'preview' : 'server'}.allowedHosts` + res.writeHead(403, { + 'Content-Type': 'text/plain', + }) + res.end( + `Blocked request. This host (${hostnameWithQuotes}) is not allowed.\n` + + `To allow this host, add ${hostnameWithQuotes} to \`${optionName}\` in vite.config.js.`, + ) + return + } + return next() + } +} diff --git a/packages/vite/src/node/server/middlewares/proxy.ts b/packages/vite/src/node/server/middlewares/proxy.ts index 5f7c5b04c975fa..b226a323d6a7b9 100644 --- a/packages/vite/src/node/server/middlewares/proxy.ts +++ b/packages/vite/src/node/server/middlewares/proxy.ts @@ -27,7 +27,13 @@ export interface ProxyOptions extends HttpProxy.ServerOptions { /** undefined for WebSocket upgrade requests */ res: http.ServerResponse | undefined, options: ProxyOptions, - ) => void | null | undefined | false | string + ) => + | void + | null + | undefined + | false + | string + | Promise /** * rewrite the Origin header of a WebSocket request to match the target * @@ -158,7 +164,7 @@ export function proxyMiddleware( }) if (httpServer) { - httpServer.on('upgrade', (req, socket, head) => { + httpServer.on('upgrade', async (req, socket, head) => { const url = req.url! for (const context in proxies) { if (doesProxyContextMatchUrl(context, url)) { @@ -169,14 +175,26 @@ export function proxyMiddleware( opts.target?.toString().startsWith('wss:') ) { if (opts.bypass) { - const bypassResult = opts.bypass(req, undefined, opts) - if (typeof bypassResult === 'string') { - req.url = bypassResult - debug?.(`bypass: ${req.url} -> ${bypassResult}`) - return - } else if (bypassResult === false) { - debug?.(`bypass: ${req.url} -> 404`) - socket.end('HTTP/1.1 404 Not Found\r\n\r\n', '') + try { + const bypassResult = await opts.bypass(req, undefined, opts) + if (typeof bypassResult === 'string') { + debug?.(`bypass: ${req.url} -> ${bypassResult}`) + req.url = bypassResult + return + } + if (bypassResult === false) { + debug?.(`bypass: ${req.url} -> 404`) + socket.end('HTTP/1.1 404 Not Found\r\n\r\n', '') + return + } + } catch (err) { + config.logger.error( + `${colors.red(`ws proxy bypass error:`)}\n${err.stack}`, + { + timestamp: true, + error: err, + }, + ) return } } @@ -194,7 +212,7 @@ export function proxyMiddleware( } // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...` - return function viteProxyMiddleware(req, res, next) { + return async function viteProxyMiddleware(req, res, next) { const url = req.url! for (const context in proxies) { if (doesProxyContextMatchUrl(context, url)) { @@ -202,15 +220,21 @@ export function proxyMiddleware( const options: HttpProxy.ServerOptions = {} if (opts.bypass) { - const bypassResult = opts.bypass(req, res, opts) - if (typeof bypassResult === 'string') { - req.url = bypassResult - debug?.(`bypass: ${req.url} -> ${bypassResult}`) - return next() - } else if (bypassResult === false) { - debug?.(`bypass: ${req.url} -> 404`) - res.statusCode = 404 - return res.end() + try { + const bypassResult = await opts.bypass(req, res, opts) + if (typeof bypassResult === 'string') { + debug?.(`bypass: ${req.url} -> ${bypassResult}`) + req.url = bypassResult + return next() + } + if (bypassResult === false) { + debug?.(`bypass: ${req.url} -> 404`) + res.statusCode = 404 + return res.end() + } + } catch (e) { + debug?.(`bypass: ${req.url} -> ${e}`) + return next(e) } } diff --git a/packages/vite/src/node/server/pluginContainer.ts b/packages/vite/src/node/server/pluginContainer.ts index c77e105fa2a969..e767a1f684296e 100644 --- a/packages/vite/src/node/server/pluginContainer.ts +++ b/packages/vite/src/node/server/pluginContainer.ts @@ -40,7 +40,6 @@ import type { FunctionPluginHooks, InputOptions, LoadResult, - MinimalPluginContext, ModuleInfo, ModuleOptions, NormalizedInputOptions, @@ -48,9 +47,11 @@ import type { ParallelPluginHooks, PartialNull, PartialResolvedId, + PluginContextMeta, ResolvedId, RollupError, RollupLog, + MinimalPluginContext as RollupMinimalPluginContext, PluginContext as RollupPluginContext, TransformPluginContext as RollupTransformPluginContext, SourceDescription, @@ -88,8 +89,6 @@ import type { EnvironmentModuleNode, } from './moduleGraph' -const noop = () => {} - // same default value of "moduleInfo.meta" as in Rollup const EMPTY_OBJECT = Object.freeze({}) @@ -105,6 +104,9 @@ const debugPluginResolve = createDebugger('vite:plugin-resolve', { const debugPluginTransform = createDebugger('vite:plugin-transform', { onlyWhenFocused: 'vite:plugin', }) +const debugPluginContainerContext = createDebugger( + 'vite:plugin-container-context', +) export const ERR_CLOSED_SERVER = 'ERR_CLOSED_SERVER' @@ -182,18 +184,10 @@ class EnvironmentPluginContainer { public plugins: Plugin[], public watcher?: FSWatcher, ) { - this.minimalContext = { - meta: { - rollupVersion, - watchMode: true, - }, - debug: noop, - info: noop, - warn: noop, - // @ts-expect-error noop - error: noop, + this.minimalContext = new MinimalPluginContext( + { rollupVersion, watchMode: true }, environment, - } + ) const utils = createPluginHookUtils(plugins) this.getSortedPlugins = utils.getSortedPlugins this.getSortedPluginHooks = utils.getSortedPluginHooks @@ -545,22 +539,63 @@ class EnvironmentPluginContainer { } } -class PluginContext implements Omit { +class MinimalPluginContext implements RollupMinimalPluginContext { + constructor( + public meta: PluginContextMeta, + public environment: Environment, + ) {} + + debug(rawLog: string | RollupLog | (() => string | RollupLog)): void { + const log = this._normalizeRawLog(rawLog) + const msg = buildErrorMessage(log, [`debug: ${log.message}`], false) + debugPluginContainerContext?.(msg) + } + + info(rawLog: string | RollupLog | (() => string | RollupLog)): void { + const log = this._normalizeRawLog(rawLog) + const msg = buildErrorMessage(log, [`info: ${log.message}`], false) + this.environment.logger.info(msg, { clear: true, timestamp: true }) + } + + warn(rawLog: string | RollupLog | (() => string | RollupLog)): void { + const log = this._normalizeRawLog(rawLog) + const msg = buildErrorMessage( + log, + [colors.yellow(`warning: ${log.message}`)], + false, + ) + this.environment.logger.warn(msg, { clear: true, timestamp: true }) + } + + error(e: string | RollupError): never { + const err = (typeof e === 'string' ? new Error(e) : e) as RollupError + throw err + } + + private _normalizeRawLog( + rawLog: string | RollupLog | (() => string | RollupLog), + ): RollupLog { + const logValue = typeof rawLog === 'function' ? rawLog() : rawLog + return typeof logValue === 'string' ? new Error(logValue) : logValue + } +} + +class PluginContext + extends MinimalPluginContext + implements Omit +{ ssr = false _scan = false _activeId: string | null = null _activeCode: string | null = null _resolveSkips?: Set _resolveSkipCalls?: readonly SkipInformation[] - meta: RollupPluginContext['meta'] - environment: Environment constructor( public _plugin: Plugin, public _container: EnvironmentPluginContainer, ) { - this.environment = this._container.environment - this.meta = this._container.minimalContext.meta + super(_container.minimalContext.meta, _container.environment) } parse(code: string, opts: any) { @@ -684,39 +719,41 @@ class PluginContext implements Omit { return '' } - warn( - e: string | RollupLog | (() => string | RollupLog), + override debug(log: string | RollupLog | (() => string | RollupLog)): void { + const err = this._formatLog(typeof log === 'function' ? log() : log) + super.debug(err) + } + + override info(log: string | RollupLog | (() => string | RollupLog)): void { + const err = this._formatLog(typeof log === 'function' ? log() : log) + super.info(err) + } + + override warn( + log: 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, + const err = this._formatLog( + typeof log === 'function' ? log() : log, + position, ) - this.environment.logger.warn(msg, { - clear: true, - timestamp: true, - }) + super.warn(err) } - error( + override 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) + throw this._formatLog(e, position) } - debug = noop - info = noop - - private _formatError( - e: string | RollupError, - position: number | { column: number; line: number } | undefined, - ): RollupError { - const err = (typeof e === 'string' ? new Error(e) : e) as RollupError + private _formatLog( + e: string | E, + position?: number | { column: number; line: number } | undefined, + ): E { + const err = (typeof e === 'string' ? new Error(e) : e) as E if (err.pluginCode) { return err // The plugin likely called `this.error` } diff --git a/packages/vite/src/node/server/ws.ts b/packages/vite/src/node/server/ws.ts index 6e7ca804fbae72..330b922110462f 100644 --- a/packages/vite/src/node/server/ws.ts +++ b/packages/vite/src/node/server/ws.ts @@ -5,6 +5,7 @@ import type { ServerOptions as HttpsServerOptions } from 'node:https' import { createServer as createHttpsServer } from 'node:https' import type { Socket } from 'node:net' import type { Duplex } from 'node:stream' +import crypto from 'node:crypto' import colors from 'picocolors' import type { WebSocket as WebSocketRaw } from 'ws' import { WebSocketServer as WebSocketServerRaw_ } from 'ws' @@ -15,6 +16,7 @@ import type { ResolvedConfig } from '..' import { isObject } from '../utils' import type { NormalizedHotChannel, NormalizedHotChannelClient } from './hmr' import { normalizeHotChannel } from './hmr' +import { isHostAllowed } from './middlewares/hostCheck' import type { HttpServer } from '.' /* In Bun, the `ws` module is overridden to hook into the native code. Using the bundled `js` version @@ -87,6 +89,29 @@ function noop() { // noop } +// we only allow websockets to be connected if it has a valid token +// this is to prevent untrusted origins to connect to the server +// for example, Cross-site WebSocket hijacking +// +// we should check the token before calling wss.handleUpgrade +// otherwise untrusted ws clients will be included in wss.clients +// +// using the query params means the token might be logged out in server or middleware logs +// but we assume that is not an issue since the token is regenerated for each process +function hasValidToken(config: ResolvedConfig, url: URL) { + const token = url.searchParams.get('token') + if (!token) return false + + try { + const isValidToken = crypto.timingSafeEqual( + Buffer.from(token), + Buffer.from(config.webSocketToken), + ) + return isValidToken + } catch {} // an error is thrown when the length is incorrect + return false +} + export function createWebSocketServer( server: HttpServer | null, config: ResolvedConfig, @@ -116,7 +141,6 @@ export function createWebSocketServer( } } - let wss: WebSocketServerRaw_ let wsHttpServer: Server | undefined = undefined const hmr = isObject(config.server.hmr) && config.server.hmr @@ -135,23 +159,69 @@ export function createWebSocketServer( const port = hmrPort || 24678 const host = (hmr && hmr.host) || undefined + const shouldHandle = (req: IncomingMessage) => { + const protocol = req.headers['sec-websocket-protocol']! + // vite-ping is allowed to connect from anywhere + // because it needs to be connected before the client fetches the new `/@vite/client` + // this is fine because vite-ping does not receive / send any meaningful data + if (protocol === 'vite-ping') return true + + const hostHeader = req.headers.host + if (!hostHeader || !isHostAllowed(config, false, hostHeader)) { + return false + } + + if (config.legacy?.skipWebSocketTokenCheck) { + return true + } + + // If the Origin header is set, this request might be coming from a browser. + // Browsers always sets the Origin header for WebSocket connections. + if (req.headers.origin) { + const parsedUrl = new URL(`http://example.com${req.url!}`) + return hasValidToken(config, parsedUrl) + } + + // We allow non-browser requests to connect without a token + // for backward compat and convenience + // This is fine because if you can sent a request without the SOP limitation, + // you can also send a normal HTTP request to the server. + return true + } + const handleUpgrade = ( + req: IncomingMessage, + socket: Duplex, + head: Buffer, + isPing: boolean, + ) => { + wss.handleUpgrade(req, socket as Socket, head, (ws) => { + // vite-ping is allowed to connect from anywhere + // we close the connection immediately without connection event + // so that the client does not get included in `wss.clients` + if (isPing) { + ws.close(/* Normal Closure */ 1000) + return + } + wss.emit('connection', ws, req) + }) + } + const wss: WebSocketServerRaw_ = new WebSocketServerRaw({ noServer: true }) + wss.shouldHandle = shouldHandle + if (wsServer) { let hmrBase = config.base const hmrPath = hmr ? hmr.path : undefined if (hmrPath) { hmrBase = path.posix.join(hmrBase, hmrPath) } - wss = new WebSocketServerRaw({ noServer: true }) hmrServerWsListener = (req, socket, head) => { + const protocol = req.headers['sec-websocket-protocol']! + const parsedUrl = new URL(`http://example.com${req.url!}`) if ( - [HMR_HEADER, 'vite-ping'].includes( - req.headers['sec-websocket-protocol']!, - ) && - req.url === hmrBase + [HMR_HEADER, 'vite-ping'].includes(protocol) && + parsedUrl.pathname === hmrBase ) { - wss.handleUpgrade(req, socket as Socket, head, (ws) => { - wss.emit('connection', ws, req) - }) + handleUpgrade(req, socket as Socket, head, protocol === 'vite-ping') } } wsServer.on('upgrade', hmrServerWsListener) @@ -177,7 +247,6 @@ export function createWebSocketServer( } else { wsHttpServer = createHttpServer(route) } - wss = new WebSocketServerRaw({ noServer: true }) wsHttpServer.on('upgrade', (req, socket, head) => { const protocol = req.headers['sec-websocket-protocol']! if (protocol === 'vite-ping' && server && !server.listening) { @@ -187,9 +256,7 @@ export function createWebSocketServer( req.destroy() return } - wss.handleUpgrade(req, socket as Socket, head, (ws) => { - wss.emit('connection', ws, req) - }) + handleUpgrade(req, socket as Socket, head, protocol === 'vite-ping') }) wsHttpServer.on('error', (e: Error & { code: string }) => { if (e.code === 'EADDRINUSE') { @@ -207,9 +274,6 @@ export function createWebSocketServer( } wss.on('connection', (socket) => { - if (socket.protocol === 'vite-ping') { - return - } socket.on('message', (raw) => { if (!customListeners.size) return let parsed: any diff --git a/packages/vite/src/node/ssr/__tests__/ssrTransform.spec.ts b/packages/vite/src/node/ssr/__tests__/ssrTransform.spec.ts index 226e0ef176278e..2e0768ef0a2f94 100644 --- a/packages/vite/src/node/ssr/__tests__/ssrTransform.spec.ts +++ b/packages/vite/src/node/ssr/__tests__/ssrTransform.spec.ts @@ -5,6 +5,7 @@ import type { SourceMap } from 'rollup' import { TraceMap, originalPositionFor } from '@jridgewell/trace-mapping' import { transformWithEsbuild } from '../../plugins/esbuild' import { ssrTransform } from '../ssrTransform' +import { createServer } from '../..' const ssrTransformSimple = async (code: string, url = '') => ssrTransform(code, null, url, code) @@ -1385,3 +1386,76 @@ const c = () => { `, ) }) + +test('combine mappings', async () => { + const server = await createServer({ + configFile: false, + envFile: false, + logLevel: 'error', + plugins: [ + { + name: 'test-mappings', + resolveId(source) { + if (source.startsWith('virtual:test-mappings')) { + return '\0' + source + } + }, + load(id) { + if (id.startsWith('\0virtual:test-mappings')) { + const code = `export default "test";\n` + if (id === '\0virtual:test-mappings:empty') { + return { code, map: { mappings: '' } } + } + if (id === '\0virtual:test-mappings:null') { + return { code, map: null } + } + } + }, + }, + ], + }) + + { + const result = await server.environments.ssr.transformRequest( + 'virtual:test-mappings:empty', + ) + expect(result?.map).toMatchInlineSnapshot(` + { + "mappings": "", + } + `) + const mod = await server.ssrLoadModule('virtual:test-mappings:empty') + expect(mod).toMatchInlineSnapshot(` + { + "default": "test", + } + `) + } + + { + const result = await server.environments.ssr.transformRequest( + 'virtual:test-mappings:null', + ) + expect(result?.map).toMatchInlineSnapshot(` + SourceMap { + "file": undefined, + "mappings": "AAAA,8BAAc,CAAC,CAAC,IAAI,CAAC;", + "names": [], + "sources": [ + "virtual:test-mappings:null", + ], + "sourcesContent": [ + "export default "test"; + ", + ], + "version": 3, + } + `) + const mod = await server.ssrLoadModule('virtual:test-mappings:null') + expect(mod).toMatchInlineSnapshot(` + { + "default": "test", + } + `) + } +}) diff --git a/packages/vite/src/node/ssr/fetchModule.ts b/packages/vite/src/node/ssr/fetchModule.ts index a658fe6c3e31bd..a012167a2a1720 100644 --- a/packages/vite/src/node/ssr/fetchModule.ts +++ b/packages/vite/src/node/ssr/fetchModule.ts @@ -28,8 +28,10 @@ export async function fetchModule( importer?: string, options: FetchModuleOptions = {}, ): Promise { - // builtins should always be externalized - if (url.startsWith('data:') || isBuiltin(url)) { + if ( + url.startsWith('data:') || + isBuiltin(environment.config.resolve.builtins, url) + ) { return { externalize: url, type: 'builtin' } } @@ -60,6 +62,7 @@ export async function fetchModule( isProduction, root, packageCache: environment.config.packageCache, + builtins: environment.config.resolve.builtins, }) if (!resolved) { const err: any = new Error( diff --git a/packages/vite/src/node/ssr/runnerImport.ts b/packages/vite/src/node/ssr/runnerImport.ts new file mode 100644 index 00000000000000..d24f9e58a55368 --- /dev/null +++ b/packages/vite/src/node/ssr/runnerImport.ts @@ -0,0 +1,76 @@ +import type { InlineConfig } from '../config' +import { resolveConfig } from '../config' +import { createRunnableDevEnvironment } from '../server/environments/runnableEnvironment' +import { mergeConfig } from '../utils' + +interface RunnerImportResult { + module: T + dependencies: string[] +} + +/** + * Import any file using the default Vite environment. + * @experimental + */ +export async function runnerImport( + moduleId: string, + inlineConfig?: InlineConfig, +): Promise> { + const isModuleSyncConditionEnabled = (await import('#module-sync-enabled')) + .default + const config = await resolveConfig( + mergeConfig(inlineConfig || {}, { + configFile: false, + envFile: false, + cacheDir: process.cwd(), + environments: { + inline: { + consumer: 'server', + dev: { + moduleRunnerTransform: true, + }, + resolve: { + external: true, + mainFields: [], + conditions: [ + 'node', + ...(isModuleSyncConditionEnabled ? ['module-sync'] : []), + ], + }, + }, + }, + } satisfies InlineConfig), + 'serve', + ) + const environment = createRunnableDevEnvironment('inline', config, { + runnerOptions: { + hmr: { + logger: false, + }, + }, + hot: false, + }) + await environment.init() + try { + const module = await environment.runner.import(moduleId) + const modules = [ + ...environment.runner.evaluatedModules.urlToIdModuleMap.values(), + ] + const dependencies = modules + .filter((m) => { + // ignore all externalized modules + if (!m.meta || 'externalize' in m.meta) { + return false + } + // ignore the current module + return m.exports !== module + }) + .map((m) => m.file) + return { + module, + dependencies, + } + } finally { + await environment.close() + } +} diff --git a/packages/vite/src/node/ssr/ssrTransform.ts b/packages/vite/src/node/ssr/ssrTransform.ts index 8c44859bc49129..25735fd33b0756 100644 --- a/packages/vite/src/node/ssr/ssrTransform.ts +++ b/packages/vite/src/node/ssr/ssrTransform.ts @@ -419,21 +419,26 @@ async function ssrTransformScript( }, }) - let map = s.generateMap({ hires: 'boundary' }) - map.sources = [path.basename(url)] - // needs to use originalCode instead of code - // because code might be already transformed even if map is null - map.sourcesContent = [originalCode] - if ( - inMap && - inMap.mappings && - 'sources' in inMap && - inMap.sources.length > 0 - ) { - map = combineSourcemaps(url, [ - map as RawSourceMap, - inMap as RawSourceMap, - ]) as SourceMap + let map: TransformResult['map'] + if (inMap?.mappings === '') { + map = inMap + } else { + map = s.generateMap({ hires: 'boundary' }) + map.sources = [path.basename(url)] + // needs to use originalCode instead of code + // because code might be already transformed even if map is null + map.sourcesContent = [originalCode] + if ( + inMap && + inMap.mappings && + 'sources' in inMap && + inMap.sources.length > 0 + ) { + map = combineSourcemaps(url, [ + map as RawSourceMap, + inMap as RawSourceMap, + ]) as SourceMap + } } return { diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index 582a3c2c6e43ef..0883f2e5da4fa7 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -102,11 +102,43 @@ const BUN_BUILTIN_NAMESPACE = 'bun:' // Some runtimes like Bun injects namespaced modules here, which is not a node builtin const nodeBuiltins = builtinModules.filter((id) => !id.includes(':')) -// TODO: Use `isBuiltin` from `node:module`, but Deno doesn't support it -export function isBuiltin(id: string): boolean { - if (process.versions.deno && id.startsWith(NPM_BUILTIN_NAMESPACE)) return true - if (process.versions.bun && id.startsWith(BUN_BUILTIN_NAMESPACE)) return true - return isNodeBuiltin(id) +const isBuiltinCache = new WeakMap< + (string | RegExp)[], + (id: string, importer?: string) => boolean +>() + +export function isBuiltin(builtins: (string | RegExp)[], id: string): boolean { + let isBuiltin = isBuiltinCache.get(builtins) + if (!isBuiltin) { + isBuiltin = createIsBuiltin(builtins) + isBuiltinCache.set(builtins, isBuiltin) + } + return isBuiltin(id) +} + +export function createIsBuiltin( + builtins: (string | RegExp)[], +): (id: string) => boolean { + const plainBuiltinsSet = new Set( + builtins.filter((builtin) => typeof builtin === 'string'), + ) + const regexBuiltins = builtins.filter( + (builtin) => typeof builtin !== 'string', + ) + + return (id) => + plainBuiltinsSet.has(id) || regexBuiltins.some((regexp) => regexp.test(id)) +} + +export const nodeLikeBuiltins = [ + ...nodeBuiltins, + new RegExp(`^${NODE_BUILTIN_NAMESPACE}`), + new RegExp(`^${NPM_BUILTIN_NAMESPACE}`), + new RegExp(`^${BUN_BUILTIN_NAMESPACE}`), +] + +export function isNodeLikeBuiltin(id: string): boolean { + return isBuiltin(nodeLikeBuiltins, id) } export function isNodeBuiltin(id: string): boolean { @@ -295,9 +327,6 @@ export const isJSRequest = (url: string): boolean => { return false } -const knownTsRE = /\.(?:ts|mts|cts|tsx)(?:$|\?)/ -export const isTsRequest = (url: string): boolean => knownTsRE.test(url) - const importQueryRE = /(\?|&)import=?(?:&|$)/ const directRequestRE = /(\?|&)direct=?(?:&|$)/ const internalPrefixes = [ diff --git a/playground/environment-react-ssr/package.json b/playground/environment-react-ssr/package.json index f29131c589a685..5f7de1d416efdf 100644 --- a/playground/environment-react-ssr/package.json +++ b/playground/environment-react-ssr/package.json @@ -9,11 +9,11 @@ "preview": "vite preview" }, "devDependencies": { - "@types/react": "^18.3.18", - "@types/react-dom": "^18.3.5", - "react": "^18.3.1", - "react-fake-client": "npm:react@^18.3.1", - "react-fake-server": "npm:react@^18.3.1", - "react-dom": "^18.3.1" + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "react": "^19.0.0", + "react-fake-client": "npm:react@^19.0.0", + "react-fake-server": "npm:react@^19.0.0", + "react-dom": "^19.0.0" } } diff --git a/playground/fs-serve/__tests__/fs-serve.spec.ts b/playground/fs-serve/__tests__/fs-serve.spec.ts index 16ecc0b78dc295..deeb5153b8dfaa 100644 --- a/playground/fs-serve/__tests__/fs-serve.spec.ts +++ b/playground/fs-serve/__tests__/fs-serve.spec.ts @@ -1,14 +1,28 @@ import fetch from 'node-fetch' -import { beforeAll, describe, expect, test } from 'vitest' +import { + afterEach, + beforeAll, + beforeEach, + describe, + expect, + test, +} from 'vitest' +import type { Page } from 'playwright-chromium' +import WebSocket from 'ws' import testJSON from '../safe.json' -import { isServe, page, viteTestUrl } from '~utils' +import { browser, isServe, page, viteServer, viteTestUrl } from '~utils' + +const getViteTestIndexHtmlUrl = () => { + const srcPrefix = viteTestUrl.endsWith('/') ? '' : '/' + // NOTE: viteTestUrl is set lazily + return viteTestUrl + srcPrefix + 'src/' +} const stringified = JSON.stringify(testJSON) describe.runIf(isServe)('main', () => { beforeAll(async () => { - const srcPrefix = viteTestUrl.endsWith('/') ? '' : '/' - await page.goto(viteTestUrl + srcPrefix + 'src/') + await page.goto(getViteTestIndexHtmlUrl()) }) test('default import', async () => { @@ -113,3 +127,205 @@ describe('fetch', () => { expect(res.headers.get('x-served-by')).toBe('vite') }) }) + +describe('cross origin', () => { + const fetchStatusFromPage = async (page: Page, url: string) => { + return await page.evaluate(async (url: string) => { + try { + const res = await globalThis.fetch(url) + return res.status + } catch { + return -1 + } + }, url) + } + + const connectWebSocketFromPage = async (page: Page, url: string) => { + return await page.evaluate(async (url: string) => { + try { + const ws = new globalThis.WebSocket(url, ['vite-hmr']) + await new Promise((resolve, reject) => { + ws.addEventListener('open', () => { + resolve() + ws.close() + }) + ws.addEventListener('error', () => { + reject() + }) + }) + return true + } catch { + return false + } + }, url) + } + + const connectWebSocketFromServer = async ( + url: string, + host: string, + origin: string | undefined, + ) => { + try { + const ws = new WebSocket(url, ['vite-hmr'], { + headers: { + Host: host, + ...(origin ? { Origin: origin } : undefined), + }, + }) + await new Promise((resolve, reject) => { + ws.addEventListener('open', () => { + resolve() + ws.close() + }) + ws.addEventListener('error', () => { + reject() + }) + }) + return true + } catch { + return false + } + } + + describe('allowed for same origin', () => { + beforeEach(async () => { + await page.goto(getViteTestIndexHtmlUrl()) + }) + + test('fetch HTML file', async () => { + const status = await fetchStatusFromPage(page, viteTestUrl + '/src/') + expect(status).toBe(200) + }) + + test.runIf(isServe)('fetch JS file', async () => { + const status = await fetchStatusFromPage( + page, + viteTestUrl + '/src/code.js', + ) + expect(status).toBe(200) + }) + + test.runIf(isServe)('connect WebSocket with valid token', async () => { + const token = viteServer.config.webSocketToken + const result = await connectWebSocketFromPage( + page, + `${viteTestUrl}?token=${token}`, + ) + expect(result).toBe(true) + }) + + test('fetch with allowed hosts', async () => { + const viteTestUrlUrl = new URL(viteTestUrl) + const res = await fetch(viteTestUrl + '/src/index.html', { + headers: { Host: viteTestUrlUrl.host }, + }) + expect(res.status).toBe(200) + }) + + test.runIf(isServe)( + 'connect WebSocket with valid token with allowed hosts', + async () => { + const viteTestUrlUrl = new URL(viteTestUrl) + const token = viteServer.config.webSocketToken + const result = await connectWebSocketFromServer( + `${viteTestUrl}?token=${token}`, + viteTestUrlUrl.host, + viteTestUrlUrl.origin, + ) + expect(result).toBe(true) + }, + ) + + test.runIf(isServe)( + 'connect WebSocket without a token without the origin header', + async () => { + const viteTestUrlUrl = new URL(viteTestUrl) + const result = await connectWebSocketFromServer( + viteTestUrl, + viteTestUrlUrl.host, + undefined, + ) + expect(result).toBe(true) + }, + ) + }) + + describe('denied for different origin', async () => { + let page2: Page + beforeEach(async () => { + page2 = await browser.newPage() + await page2.goto('http://vite.dev/404') + }) + afterEach(async () => { + await page2.close() + }) + + test('fetch HTML file', async () => { + const status = await fetchStatusFromPage(page2, viteTestUrl + '/src/') + expect(status).not.toBe(200) + }) + + test.runIf(isServe)('fetch JS file', async () => { + const status = await fetchStatusFromPage( + page2, + viteTestUrl + '/src/code.js', + ) + expect(status).not.toBe(200) + }) + + test.runIf(isServe)('connect WebSocket without token', async () => { + const result = await connectWebSocketFromPage(page, viteTestUrl) + expect(result).toBe(false) + + const result2 = await connectWebSocketFromPage( + page, + `${viteTestUrl}?token=`, + ) + expect(result2).toBe(false) + }) + + test.runIf(isServe)('connect WebSocket with invalid token', async () => { + const token = viteServer.config.webSocketToken + const result = await connectWebSocketFromPage( + page, + `${viteTestUrl}?token=${'t'.repeat(token.length)}`, + ) + expect(result).toBe(false) + + const result2 = await connectWebSocketFromPage( + page, + `${viteTestUrl}?token=${'t'.repeat(token.length)}t`, // different length + ) + expect(result2).toBe(false) + }) + + test('fetch with non-allowed hosts', async () => { + const res = await fetch(viteTestUrl + '/src/index.html', { + headers: { + Host: 'vite.dev', + }, + }) + expect(res.status).toBe(403) + }) + + test.runIf(isServe)( + 'connect WebSocket with valid token with non-allowed hosts', + async () => { + const token = viteServer.config.webSocketToken + const result = await connectWebSocketFromServer( + `${viteTestUrl}?token=${token}`, + 'vite.dev', + 'http://vite.dev', + ) + expect(result).toBe(false) + + const result2 = await connectWebSocketFromServer( + `${viteTestUrl}?token=${token}`, + 'vite.dev', + undefined, + ) + expect(result2).toBe(false) + }, + ) + }) +}) diff --git a/playground/fs-serve/package.json b/playground/fs-serve/package.json index f71a082b890c6a..6fae0fb1a56ef2 100644 --- a/playground/fs-serve/package.json +++ b/playground/fs-serve/package.json @@ -14,5 +14,8 @@ "dev:deny": "vite root --config ./root/vite.config-deny.js", "build:deny": "vite build root --config ./root/vite.config-deny.js", "preview:deny": "vite preview root --config ./root/vite.config-deny.js" + }, + "devDependencies": { + "ws": "^8.18.0" } } diff --git a/playground/fs-serve/root/src/code.js b/playground/fs-serve/root/src/code.js new file mode 100644 index 00000000000000..33fd8df878207b --- /dev/null +++ b/playground/fs-serve/root/src/code.js @@ -0,0 +1 @@ +// code.js diff --git a/playground/fs-serve/root/src/index.html b/playground/fs-serve/root/src/index.html index fb1276d79fea22..9f56c8831c12d9 100644 --- a/playground/fs-serve/root/src/index.html +++ b/playground/fs-serve/root/src/index.html @@ -52,6 +52,7 @@

Denied

@@ -24,6 +25,7 @@
+
diff --git a/playground/hmr/modules.d.ts b/playground/hmr/modules.d.ts index 122559a692ef20..e880082076b638 100644 --- a/playground/hmr/modules.d.ts +++ b/playground/hmr/modules.d.ts @@ -1,3 +1,7 @@ declare module 'virtual:file' { export const virtual: string } + +declare module 'virtual:file-dep' { + export const virtual: string +} diff --git a/playground/hmr/vite.config.ts b/playground/hmr/vite.config.ts index 94b07092b58f15..9ee8024ee2bf44 100644 --- a/playground/hmr/vite.config.ts +++ b/playground/hmr/vite.config.ts @@ -45,23 +45,22 @@ function virtualPlugin(): Plugin { return { name: 'virtual-file', resolveId(id) { - if (id === 'virtual:file') { - return '\0virtual:file' + if (id.startsWith('virtual:file')) { + return '\0' + id } }, load(id) { - if (id === '\0virtual:file') { + if (id.startsWith('\0virtual:file')) { return `\ import { virtual as _virtual } from "/importedVirtual.js"; export const virtual = _virtual + '${num}';` } }, configureServer(server) { - server.environments.client.hot.on('virtual:increment', async () => { - const mod = - await server.environments.client.moduleGraph.getModuleByUrl( - '\0virtual:file', - ) + server.environments.client.hot.on('virtual:increment', async (suffix) => { + const mod = await server.environments.client.moduleGraph.getModuleById( + '\0virtual:file' + (suffix || ''), + ) if (mod) { num++ server.environments.client.reloadModule(mod) diff --git a/playground/optimize-deps/dep-linked-include/package.json b/playground/optimize-deps/dep-linked-include/package.json index 1759c52daafb5a..14783d8c69df64 100644 --- a/playground/optimize-deps/dep-linked-include/package.json +++ b/playground/optimize-deps/dep-linked-include/package.json @@ -4,6 +4,6 @@ "version": "0.0.0", "main": "index.mjs", "dependencies": { - "react": "18.3.1" + "react": "19.0.0" } } diff --git a/playground/optimize-deps/package.json b/playground/optimize-deps/package.json index 2f4c67cc93968f..23c8f9f4268705 100644 --- a/playground/optimize-deps/package.json +++ b/playground/optimize-deps/package.json @@ -43,8 +43,8 @@ "lodash-es": "^4.17.21", "@vitejs/test-nested-exclude": "file:./nested-exclude", "phoenix": "^1.7.18", - "react": "^18.3.1", - "react-dom": "^18.3.1", + "react": "^19.0.0", + "react-dom": "^19.0.0", "@vitejs/test-resolve-linked": "workspace:0.0.0", "url": "^0.11.4", "vue": "^3.5.13", diff --git a/playground/proxy-bypass/__tests__/proxy-bypass.spec.ts b/playground/proxy-bypass/__tests__/proxy-bypass.spec.ts index 093179072a4118..f438dd286184e8 100644 --- a/playground/proxy-bypass/__tests__/proxy-bypass.spec.ts +++ b/playground/proxy-bypass/__tests__/proxy-bypass.spec.ts @@ -1,8 +1,19 @@ import { expect, test, vi } from 'vitest' -import { browserLogs } from '~utils' +import { browserLogs, isServe, page, serverLogs } from '~utils' test('proxy-bypass', async () => { await vi.waitFor(() => { expect(browserLogs.join('\n')).toContain('status of 404 (Not Found)') }) }) + +test('async-proxy-bypass', async () => { + const content = await page.frame('async-response').content() + expect(content).toContain('Hello after 4 ms (async timeout)') +}) + +test.runIf(isServe)('async-proxy-bypass-with-error', async () => { + await vi.waitFor(() => { + expect(serverLogs.join('\n')).toContain('bypass error') + }) +}) diff --git a/playground/proxy-bypass/index.html b/playground/proxy-bypass/index.html index 366aaebb5651ae..e41b3708cfa0e3 100644 --- a/playground/proxy-bypass/index.html +++ b/playground/proxy-bypass/index.html @@ -1,2 +1,4 @@ root app
+ + diff --git a/playground/proxy-bypass/vite.config.js b/playground/proxy-bypass/vite.config.js index 0873583a5bfa56..8d055a63212e1f 100644 --- a/playground/proxy-bypass/vite.config.js +++ b/playground/proxy-bypass/vite.config.js @@ -1,5 +1,7 @@ import { defineConfig } from 'vite' +const timeout = (ms) => new Promise((r) => setTimeout(r, ms)) + export default defineConfig({ server: { port: 9606, @@ -10,6 +12,22 @@ export default defineConfig({ return false }, }, + '/asyncResponse': { + bypass: async (_, res) => { + await timeout(4) + res.writeHead(200, { + 'Content-Type': 'text/plain', + }) + res.end('Hello after 4 ms (async timeout)') + return '/asyncResponse' + }, + }, + '/asyncThrowingError': { + bypass: async () => { + await timeout(4) + throw new Error('bypass error') + }, + }, }, }, }) diff --git a/playground/resolve/__tests__/resolve.spec.ts b/playground/resolve/__tests__/resolve.spec.ts index ba5e6441dc7edb..d5d11f4a7b08ce 100644 --- a/playground/resolve/__tests__/resolve.spec.ts +++ b/playground/resolve/__tests__/resolve.spec.ts @@ -111,6 +111,10 @@ test('a ts module can import another ts module using its corresponding js file n expect(await page.textContent('.ts-extension')).toMatch('[success]') }) +test('a js module can import another ts module using its corresponding js file name', async () => { + expect(await page.textContent('.js-ts-extension')).toMatch('[success]') +}) + test('filename with dot', async () => { expect(await page.textContent('.dot')).toMatch('[success]') }) diff --git a/playground/resolve/index.html b/playground/resolve/index.html index f1480b0a9b3e52..1b5cd5ae76a3fd 100644 --- a/playground/resolve/index.html +++ b/playground/resolve/index.html @@ -113,6 +113,9 @@

fail

+

A js module can import TS modules using its corresponding js file name

+

fail

+

Resolve file name containing dot

fail

@@ -304,6 +307,9 @@

utf8-bom-package

import { msgMjs as tsMjsExtensionWithQueryMsg } from './ts-extension?query=1' text('.mjs-extension-with-query', tsMjsExtensionWithQueryMsg) + import { msg as jsTsExtensionMsg } from './ts-extension/index-js.js' + text('.js-ts-extension', jsTsExtensionMsg) + // filename with dot import { bar } from './util/bar.util' text('.dot', bar()) diff --git a/playground/resolve/ts-extension/index-js.js b/playground/resolve/ts-extension/index-js.js new file mode 100644 index 00000000000000..0a1d17d4b10ea1 --- /dev/null +++ b/playground/resolve/ts-extension/index-js.js @@ -0,0 +1,10 @@ +import { msg as msgJs } from './hello.js' +import { msgJsx } from './hellojsx.jsx' +import { msgTsx } from './hellotsx.js' +import { msgCjs } from './hellocjs.cjs' +import { msgMjs } from './hellomjs.mjs' + +export const msg = + msgJs && msgJsx && msgTsx && msgCjs && msgMjs + ? '[success] use .js / .jsx / .cjs / .mjs extension to import a TS modules' + : '[fail]' diff --git a/playground/ssr-webworker/package.json b/playground/ssr-webworker/package.json index 5f36252ee158ef..83c46bc5286a80 100644 --- a/playground/ssr-webworker/package.json +++ b/playground/ssr-webworker/package.json @@ -8,7 +8,7 @@ "build:worker": "vite build --ssr src/entry-worker.jsx --outDir dist/worker" }, "dependencies": { - "react": "^18.3.1", + "react": "^19.0.0", "@vitejs/test-browser-exports": "file:./browser-exports", "@vitejs/test-worker-exports": "file:./worker-exports" }, diff --git a/playground/vitestGlobalSetup.ts b/playground/vitestGlobalSetup.ts index 95a3e21273baa1..3ae5d53fcdda9b 100644 --- a/playground/vitestGlobalSetup.ts +++ b/playground/vitestGlobalSetup.ts @@ -1,12 +1,12 @@ import fs from 'node:fs/promises' import path from 'node:path' -import type { GlobalSetupContext } from 'vitest/node' +import type { TestProject } from 'vitest/node' import type { BrowserServer } from 'playwright-chromium' import { chromium } from 'playwright-chromium' let browserServer: BrowserServer | undefined -export async function setup({ provide }: GlobalSetupContext): Promise { +export async function setup({ provide }: TestProject): Promise { process.env.NODE_ENV = process.env.VITE_TEST_BUILD ? 'production' : 'development' diff --git a/playground/worker/worker/main-module.js b/playground/worker/worker/main-module.js index ed694895296f0e..a659aa438fd5ca 100644 --- a/playground/worker/worker/main-module.js +++ b/playground/worker/worker/main-module.js @@ -140,7 +140,6 @@ const genWorkerName = () => 'module' const w2 = new SharedWorker( new URL('../url-shared-worker.js', import.meta.url), { - /* @vite-ignore */ name: genWorkerName(), type: 'module', }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad6c6194f90a05..dd58a32d53e6a9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -133,8 +133,8 @@ importers: specifier: workspace:* version: link:packages/vite vitest: - specifier: ^2.1.8 - version: 2.1.8(@types/node@22.10.6) + specifier: ^3.0.2 + version: 3.0.2(@types/node@22.10.6) docs: devDependencies: @@ -149,7 +149,7 @@ importers: version: 4.2.2 vitepress: specifier: ^1.5.0 - version: 1.5.0(@algolia/client-search@5.13.0)(@types/react@18.3.18)(axios@1.7.9)(postcss@8.5.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2) + version: 1.5.0(@algolia/client-search@5.13.0)(axios@1.7.9)(postcss@8.5.1)(typescript@5.7.2) vitepress-plugin-group-icons: specifier: ^1.3.4 version: 1.3.4 @@ -391,8 +391,8 @@ importers: specifier: ^0.5.21 version: 0.5.21 strip-literal: - specifier: ^2.1.1 - version: 2.1.1 + specifier: ^3.0.0 + version: 3.0.0 terser: specifier: ^5.37.0 version: 5.37.0 @@ -420,6 +420,9 @@ importers: '@vitejs/cjs-ssr-dep': specifier: link:./fixtures/cjs-ssr-dep version: link:fixtures/cjs-ssr-dep + '@vitejs/parent': + specifier: link:./packages/parent + version: link:packages/parent '@vitejs/test-dep-conditions': specifier: file:./fixtures/test-dep-conditions version: file:packages/vite/src/node/__tests__/fixtures/test-dep-conditions @@ -430,12 +433,20 @@ importers: packages/vite/src/node/__tests__/fixtures/test-dep-conditions: {} + packages/vite/src/node/__tests__/packages/child: {} + packages/vite/src/node/__tests__/packages/module: {} packages/vite/src/node/__tests__/packages/name: {} packages/vite/src/node/__tests__/packages/noname: {} + packages/vite/src/node/__tests__/packages/parent: + dependencies: + '@vitejs/child': + specifier: link:../child + version: link:../child + packages/vite/src/node/server/__tests__/fixtures/lerna/nested: {} packages/vite/src/node/server/__tests__/fixtures/none/nested: {} @@ -687,23 +698,23 @@ importers: playground/environment-react-ssr: devDependencies: '@types/react': - specifier: ^18.3.18 - version: 18.3.18 + specifier: ^19.0.8 + version: 19.0.8 '@types/react-dom': - specifier: ^18.3.5 - version: 18.3.5(@types/react@18.3.18) + specifier: ^19.0.3 + version: 19.0.3(@types/react@19.0.8) react: - specifier: ^18.3.1 - version: 18.3.1 + specifier: ^19.0.0 + version: 19.0.0 react-dom: - specifier: ^18.3.1 - version: 18.3.1(react@18.3.1) + specifier: ^19.0.0 + version: 19.0.0(react@19.0.0) react-fake-client: - specifier: npm:react@^18.3.1 - version: react@18.3.1 + specifier: npm:react@^19.0.0 + version: react@19.0.0 react-fake-server: - specifier: npm:react@^18.3.1 - version: react@18.3.1 + specifier: npm:react@^19.0.0 + version: react@19.0.0 playground/extensions: dependencies: @@ -760,7 +771,11 @@ importers: specifier: ^3.5.13 version: 3.5.13(typescript@5.7.2) - playground/fs-serve: {} + playground/fs-serve: + devDependencies: + ws: + specifier: ^8.18.0 + version: 8.18.0 playground/glob-import: dependencies: @@ -1022,11 +1037,11 @@ importers: specifier: ^1.7.18 version: 1.7.18 react: - specifier: ^18.3.1 - version: 18.3.1 + specifier: ^19.0.0 + version: 19.0.0 react-dom: - specifier: ^18.3.1 - version: 18.3.1(react@18.3.1) + specifier: ^19.0.0 + version: 19.0.0(react@19.0.0) url: specifier: ^0.11.4 version: 0.11.4 @@ -1084,8 +1099,8 @@ importers: playground/optimize-deps/dep-linked-include: dependencies: react: - specifier: 18.3.1 - version: 18.3.1 + specifier: 19.0.0 + version: 19.0.0 playground/optimize-deps/dep-node-env: {} @@ -1585,8 +1600,8 @@ importers: specifier: file:./worker-exports version: file:playground/ssr-webworker/worker-exports react: - specifier: ^18.3.1 - version: 18.3.1 + specifier: ^19.0.0 + version: 19.0.0 devDependencies: '@vitejs/test-resolve-linked': specifier: workspace:* @@ -3159,22 +3174,19 @@ packages: '@types/prompts@2.4.9': resolution: {integrity: sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==} - '@types/prop-types@15.7.13': - resolution: {integrity: sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==} - '@types/qs@6.9.17': resolution: {integrity: sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==} '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - '@types/react-dom@18.3.5': - resolution: {integrity: sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==} + '@types/react-dom@19.0.3': + resolution: {integrity: sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==} peerDependencies: - '@types/react': ^18.0.0 + '@types/react': ^19.0.0 - '@types/react@18.3.18': - resolution: {integrity: sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==} + '@types/react@19.0.8': + resolution: {integrity: sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw==} '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} @@ -3524,11 +3536,11 @@ packages: '@vitejs/test-worker-exports@file:playground/ssr-webworker/worker-exports': resolution: {directory: playground/ssr-webworker/worker-exports, type: directory} - '@vitest/expect@2.1.8': - resolution: {integrity: sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw==} + '@vitest/expect@3.0.2': + resolution: {integrity: sha512-dKSHLBcoZI+3pmP5hiZ7I5grNru2HRtEW8Z5Zp4IXog8QYcxhlox7JUPyIIFWfN53+3HW3KPLIl6nSzUGgKSuQ==} - '@vitest/mocker@2.1.8': - resolution: {integrity: sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==} + '@vitest/mocker@3.0.2': + resolution: {integrity: sha512-Hr09FoBf0jlwwSyzIF4Xw31OntpO3XtZjkccpcBf8FeVW3tpiyKlkeUzxS/txzHqpUCNIX157NaTySxedyZLvA==} peerDependencies: msw: ^2.4.9 vite: workspace:* @@ -3538,20 +3550,20 @@ packages: vite: optional: true - '@vitest/pretty-format@2.1.8': - resolution: {integrity: sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==} + '@vitest/pretty-format@3.0.2': + resolution: {integrity: sha512-yBohcBw/T/p0/JRgYD+IYcjCmuHzjC3WLAKsVE4/LwiubzZkE8N49/xIQ/KGQwDRA8PaviF8IRO8JMWMngdVVQ==} - '@vitest/runner@2.1.8': - resolution: {integrity: sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg==} + '@vitest/runner@3.0.2': + resolution: {integrity: sha512-GHEsWoncrGxWuW8s405fVoDfSLk6RF2LCXp6XhevbtDjdDme1WV/eNmUueDfpY1IX3MJaCRelVCEXsT9cArfEg==} - '@vitest/snapshot@2.1.8': - resolution: {integrity: sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==} + '@vitest/snapshot@3.0.2': + resolution: {integrity: sha512-h9s67yD4+g+JoYG0zPCo/cLTabpDqzqNdzMawmNPzDStTiwxwkyYM1v5lWE8gmGv3SVJ2DcxA2NpQJZJv9ym3g==} - '@vitest/spy@2.1.8': - resolution: {integrity: sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg==} + '@vitest/spy@3.0.2': + resolution: {integrity: sha512-8mI2iUn+PJFMT44e3ISA1R+K6ALVs47W6eriDTfXe6lFqlflID05MB4+rIFhmDSLBj8iBsZkzBYlgSkinxLzSQ==} - '@vitest/utils@2.1.8': - resolution: {integrity: sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==} + '@vitest/utils@3.0.2': + resolution: {integrity: sha512-Qu01ZYZlgHvDP02JnMBRpX43nRaZtNpIzw3C1clDXmn8eakgX6iQVGzTQ/NjkIr64WD8ioqOjkaYRVvHQI5qiw==} '@volar/language-core@2.4.9': resolution: {integrity: sha512-t++GIrUeQnKCieZdY9e+Uar2VmTqOE4Z9KcEcdSHKmKZPuqpbbWow1YKe1i3HpU2s1JqLRVM8y/n87WKXyxJAg==} @@ -5253,10 +5265,6 @@ packages: longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} - loose-envify@1.4.0: - resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} - hasBin: true - loupe@3.1.2: resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} @@ -5758,6 +5766,9 @@ packages: pathe@2.0.1: resolution: {integrity: sha512-6jpjMpOth5S9ITVu5clZ7NOgHNsv5vRQdheL9ztp2vZmM6fRbLvyua1tiBIL4lk8SAe3ARzeXEly6siXCjDHDw==} + pathe@2.0.2: + resolution: {integrity: sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==} + pathval@2.0.0: resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} engines: {node: '>= 14.16'} @@ -6189,13 +6200,13 @@ packages: resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} engines: {node: '>= 0.8'} - react-dom@18.3.1: - resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + react-dom@19.0.0: + resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==} peerDependencies: - react: ^18.3.1 + react: ^19.0.0 - react@18.3.1: - resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + react@19.0.0: + resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} engines: {node: '>=0.10.0'} read-cache@1.0.0: @@ -6486,8 +6497,8 @@ packages: sax@1.4.1: resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} - scheduler@0.23.2: - resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + scheduler@0.25.0: + resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} scslre@0.3.0: resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} @@ -6710,8 +6721,8 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - strip-literal@2.1.1: - resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==} + strip-literal@3.0.0: + resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} stylehacks@7.0.4: resolution: {integrity: sha512-i4zfNrGMt9SB4xRK9L83rlsFCgdGANfeDAYacO1pkqcE7cRHPdWHwnKZVz7WY17Veq/FvyYsRAU++Ga+qDFIww==} @@ -6812,16 +6823,19 @@ packages: tinyexec@0.3.1: resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyglobby@0.2.10: resolution: {integrity: sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==} engines: {node: '>=12.0.0'} - tinypool@1.0.1: - resolution: {integrity: sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==} + tinypool@1.0.2: + resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} engines: {node: ^18.0.0 || >=20.0.0} - tinyrainbow@1.2.0: - resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} tinyspy@2.2.0: @@ -7031,9 +7045,9 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - vite-node@2.1.8: - resolution: {integrity: sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==} - engines: {node: ^18.0.0 || >=20.0.0} + vite-node@3.0.2: + resolution: {integrity: sha512-hsEQerBAHvVAbv40m3TFQe/lTEbOp7yDpyqMJqr2Tnd+W58+DEYOt+fluQgekOePcsNBmR77lpVAnIU2Xu4SvQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true vitepress-plugin-group-icons@1.3.4: @@ -7051,15 +7065,15 @@ packages: postcss: optional: true - vitest@2.1.8: - resolution: {integrity: sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==} - engines: {node: ^18.0.0 || >=20.0.0} + vitest@3.0.2: + resolution: {integrity: sha512-5bzaHakQ0hmVVKLhfh/jXf6oETDBtgPo8tQCHYB+wftNgFJ+Hah67IsWc8ivx4vFL025Ow8UiuTf4W57z4izvQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' - '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 2.1.8 - '@vitest/ui': 2.1.8 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.0.2 + '@vitest/ui': 3.0.2 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -8030,9 +8044,9 @@ snapshots: '@docsearch/css@3.7.0': {} - '@docsearch/js@3.7.0(@algolia/client-search@5.13.0)(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@docsearch/js@3.7.0(@algolia/client-search@5.13.0)': dependencies: - '@docsearch/react': 3.7.0(@algolia/client-search@5.13.0)(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docsearch/react': 3.7.0(@algolia/client-search@5.13.0) preact: 10.24.3 transitivePeerDependencies: - '@algolia/client-search' @@ -8041,16 +8055,12 @@ snapshots: - react-dom - search-insights - '@docsearch/react@3.7.0(@algolia/client-search@5.13.0)(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@docsearch/react@3.7.0(@algolia/client-search@5.13.0)': dependencies: '@algolia/autocomplete-core': 1.17.6(@algolia/client-search@5.13.0)(algoliasearch@5.13.0) '@algolia/autocomplete-preset-algolia': 1.17.6(@algolia/client-search@5.13.0)(algoliasearch@5.13.0) '@docsearch/css': 3.7.0 algoliasearch: 5.13.0 - optionalDependencies: - '@types/react': 18.3.18 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) transitivePeerDependencies: - '@algolia/client-search' @@ -8779,19 +8789,16 @@ snapshots: '@types/node': 22.10.6 kleur: 3.0.3 - '@types/prop-types@15.7.13': {} - '@types/qs@6.9.17': {} '@types/range-parser@1.2.7': {} - '@types/react-dom@18.3.5(@types/react@18.3.18)': + '@types/react-dom@19.0.3(@types/react@19.0.8)': dependencies: - '@types/react': 18.3.18 + '@types/react': 19.0.8 - '@types/react@18.3.18': + '@types/react@19.0.8': dependencies: - '@types/prop-types': 15.7.13 csstype: 3.1.3 '@types/resolve@1.20.2': {} @@ -9123,45 +9130,45 @@ snapshots: '@vitejs/test-worker-exports@file:playground/ssr-webworker/worker-exports': {} - '@vitest/expect@2.1.8': + '@vitest/expect@3.0.2': dependencies: - '@vitest/spy': 2.1.8 - '@vitest/utils': 2.1.8 + '@vitest/spy': 3.0.2 + '@vitest/utils': 3.0.2 chai: 5.1.2 - tinyrainbow: 1.2.0 + tinyrainbow: 2.0.0 - '@vitest/mocker@2.1.8(vite@packages+vite)': + '@vitest/mocker@3.0.2(vite@packages+vite)': dependencies: - '@vitest/spy': 2.1.8 + '@vitest/spy': 3.0.2 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: vite: link:packages/vite - '@vitest/pretty-format@2.1.8': + '@vitest/pretty-format@3.0.2': dependencies: - tinyrainbow: 1.2.0 + tinyrainbow: 2.0.0 - '@vitest/runner@2.1.8': + '@vitest/runner@3.0.2': dependencies: - '@vitest/utils': 2.1.8 - pathe: 1.1.2 + '@vitest/utils': 3.0.2 + pathe: 2.0.2 - '@vitest/snapshot@2.1.8': + '@vitest/snapshot@3.0.2': dependencies: - '@vitest/pretty-format': 2.1.8 + '@vitest/pretty-format': 3.0.2 magic-string: 0.30.17 - pathe: 1.1.2 + pathe: 2.0.2 - '@vitest/spy@2.1.8': + '@vitest/spy@3.0.2': dependencies: tinyspy: 3.0.2 - '@vitest/utils@2.1.8': + '@vitest/utils@3.0.2': dependencies: - '@vitest/pretty-format': 2.1.8 + '@vitest/pretty-format': 3.0.2 loupe: 3.1.2 - tinyrainbow: 1.2.0 + tinyrainbow: 2.0.0 '@volar/language-core@2.4.9': dependencies: @@ -11021,10 +11028,6 @@ snapshots: longest-streak@3.1.0: {} - loose-envify@1.4.0: - dependencies: - js-tokens: 4.0.0 - loupe@3.1.2: {} lru-cache@10.4.3: {} @@ -11625,6 +11628,8 @@ snapshots: pathe@2.0.1: {} + pathe@2.0.2: {} + pathval@2.0.0: {} perfect-debounce@1.0.0: {} @@ -12039,15 +12044,12 @@ snapshots: iconv-lite: 0.6.3 unpipe: 1.0.0 - react-dom@18.3.1(react@18.3.1): + react-dom@19.0.0(react@19.0.0): dependencies: - loose-envify: 1.4.0 - react: 18.3.1 - scheduler: 0.23.2 + react: 19.0.0 + scheduler: 0.25.0 - react@18.3.1: - dependencies: - loose-envify: 1.4.0 + react@19.0.0: {} read-cache@1.0.0: dependencies: @@ -12344,9 +12346,7 @@ snapshots: sax@1.4.1: {} - scheduler@0.23.2: - dependencies: - loose-envify: 1.4.0 + scheduler@0.25.0: {} scslre@0.3.0: dependencies: @@ -12582,7 +12582,7 @@ snapshots: strip-json-comments@3.1.1: {} - strip-literal@2.1.1: + strip-literal@3.0.0: dependencies: js-tokens: 9.0.1 @@ -12715,14 +12715,16 @@ snapshots: tinyexec@0.3.1: {} + tinyexec@0.3.2: {} + tinyglobby@0.2.10: dependencies: fdir: 6.4.2(picomatch@4.0.2) picomatch: 4.0.2 - tinypool@1.0.1: {} + tinypool@1.0.2: {} - tinyrainbow@1.2.0: {} + tinyrainbow@2.0.0: {} tinyspy@2.2.0: {} @@ -12946,12 +12948,12 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-node@2.1.8: + vite-node@3.0.2: dependencies: cac: 6.7.14 debug: 4.4.0 es-module-lexer: 1.6.0 - pathe: 1.1.2 + pathe: 2.0.2 vite: link:packages/vite transitivePeerDependencies: - supports-color @@ -12964,10 +12966,10 @@ snapshots: transitivePeerDependencies: - supports-color - vitepress@1.5.0(@algolia/client-search@5.13.0)(@types/react@18.3.18)(axios@1.7.9)(postcss@8.5.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2): + vitepress@1.5.0(@algolia/client-search@5.13.0)(axios@1.7.9)(postcss@8.5.1)(typescript@5.7.2): dependencies: '@docsearch/css': 3.7.0 - '@docsearch/js': 3.7.0(@algolia/client-search@5.13.0)(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docsearch/js': 3.7.0(@algolia/client-search@5.13.0) '@iconify-json/simple-icons': 1.2.11 '@shikijs/core': 1.27.0 '@shikijs/transformers': 1.22.2 @@ -13006,27 +13008,27 @@ snapshots: - typescript - universal-cookie - vitest@2.1.8(@types/node@22.10.6): + vitest@3.0.2(@types/node@22.10.6): dependencies: - '@vitest/expect': 2.1.8 - '@vitest/mocker': 2.1.8(vite@packages+vite) - '@vitest/pretty-format': 2.1.8 - '@vitest/runner': 2.1.8 - '@vitest/snapshot': 2.1.8 - '@vitest/spy': 2.1.8 - '@vitest/utils': 2.1.8 + '@vitest/expect': 3.0.2 + '@vitest/mocker': 3.0.2(vite@packages+vite) + '@vitest/pretty-format': 3.0.2 + '@vitest/runner': 3.0.2 + '@vitest/snapshot': 3.0.2 + '@vitest/spy': 3.0.2 + '@vitest/utils': 3.0.2 chai: 5.1.2 debug: 4.4.0 expect-type: 1.1.0 magic-string: 0.30.17 - pathe: 1.1.2 + pathe: 2.0.2 std-env: 3.8.0 tinybench: 2.9.0 - tinyexec: 0.3.1 - tinypool: 1.0.1 - tinyrainbow: 1.2.0 + tinyexec: 0.3.2 + tinypool: 1.0.2 + tinyrainbow: 2.0.0 vite: link:packages/vite - vite-node: 2.1.8 + vite-node: 3.0.2 why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.10.6 diff --git a/vitest.config.ts b/vitest.config.ts index 7ee61c4585006d..919e0a30d650bd 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -13,6 +13,10 @@ export default defineConfig({ './playground/**/*.*', './playground-temp/**/*.*', ], + deps: { + // we specify 'packages' so Vitest doesn't inline the files + moduleDirectories: ['node_modules', 'packages'], + }, testTimeout: 20000, isolate: false, },