[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

feat!: use module-runner instead of vite-node (#8208)

Co-authored-by: Ari Perkkiö <ari.perkkio@gmail.com>

authored by

Vladimir
Ari Perkkiö
and committed by
GitHub
(Jul 28, 2025, 1:43 PM +0200) 9be01ba5 94ab392b

+3552 -2043
+1 -1
.npmrc
··· 3 3 provenance=true 4 4 shell-emulator=true 5 5 registry=https://registry.npmjs.org/ 6 - VITE_NODE_DEPS_MODULE_DIRECTORIES=/node_modules/,/packages/ 6 + VITEST_MODULE_DIRECTORIES=/node_modules/,/packages/
+47 -20
pnpm-lock.yaml
··· 30 30 '@types/istanbul-lib-coverage': 31 31 specifier: ^2.0.6 32 32 version: 2.0.6 33 + '@types/istanbul-lib-report': 34 + specifier: ^3.0.3 35 + version: 3.0.3 36 + '@types/istanbul-lib-source-maps': 37 + specifier: ^4.0.4 38 + version: 4.0.4 33 39 '@types/istanbul-reports': 34 40 specifier: ^3.0.4 35 41 version: 3.0.4 ··· 63 69 flatted: 64 70 specifier: ^3.3.3 65 71 version: 3.3.3 72 + istanbul-lib-coverage: 73 + specifier: ^3.2.2 74 + version: 3.2.2 75 + istanbul-lib-report: 76 + specifier: ^3.0.1 77 + version: 3.0.1 78 + istanbul-lib-source-maps: 79 + specifier: ^5.0.6 80 + version: 5.0.6 81 + istanbul-reports: 82 + specifier: ^3.1.7 83 + version: 3.1.7 66 84 magic-string: 67 85 specifier: ^0.30.17 68 86 version: 0.30.17 ··· 75 93 pathe: 76 94 specifier: ^2.0.3 77 95 version: 2.0.3 96 + sirv: 97 + specifier: ^3.0.1 98 + version: 3.0.1 78 99 std-env: 79 100 specifier: ^3.9.0 80 101 version: 3.9.0 ··· 566 587 '@bcoe/v8-coverage': 567 588 specifier: ^1.0.2 568 589 version: 1.0.2 590 + '@vitest/utils': 591 + specifier: workspace:* 592 + version: link:../utils 569 593 ast-v8-to-istanbul: 570 594 specifier: ^0.3.3 571 595 version: 0.3.3 ··· 615 639 pathe: 616 640 specifier: 'catalog:' 617 641 version: 2.0.3 618 - vite-node: 619 - specifier: workspace:* 620 - version: link:../vite-node 621 642 vitest: 622 643 specifier: workspace:* 623 644 version: link:../vitest ··· 946 967 debug: 947 968 specifier: 'catalog:' 948 969 version: 4.4.1 970 + es-module-lexer: 971 + specifier: ^1.7.0 972 + version: 1.7.0 949 973 expect-type: 950 974 specifier: ^1.2.2 951 975 version: 1.2.2 ··· 979 1003 vite: 980 1004 specifier: ^6.3.5 981 1005 version: 6.3.5(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.2)(terser@5.36.0)(tsx@4.20.3)(yaml@2.8.0) 982 - vite-node: 983 - specifier: workspace:* 984 - version: link:../vite-node 985 1006 why-is-node-running: 986 1007 specifier: ^2.3.0 987 1008 version: 2.3.0 ··· 1156 1177 1157 1178 test/cli: 1158 1179 devDependencies: 1180 + '@test/test-dep-error': 1181 + specifier: file:./deps/error 1182 + version: file:test/cli/deps/error 1183 + '@test/test-dep-linked': 1184 + specifier: link:./deps/linked 1185 + version: link:deps/linked 1159 1186 '@types/debug': 1160 1187 specifier: 'catalog:' 1161 1188 version: 4.1.12 ··· 1168 1195 '@vitest/runner': 1169 1196 specifier: workspace:^ 1170 1197 version: link:../../packages/runner 1171 - '@vitest/test-dep-error': 1172 - specifier: file:./deps/error 1173 - version: file:test/cli/deps/error 1174 - '@vitest/test-dep-linked': 1175 - specifier: link:./deps/linked 1176 - version: link:deps/linked 1177 1198 '@vitest/utils': 1178 1199 specifier: workspace:* 1179 1200 version: link:../../packages/utils ··· 1216 1237 1217 1238 test/core: 1218 1239 devDependencies: 1240 + '@test/vite-environment-external': 1241 + specifier: link:./projects/vite-environment-external 1242 + version: link:projects/vite-environment-external 1243 + '@test/vite-external': 1244 + specifier: link:./projects/vite-external 1245 + version: link:projects/vite-external 1219 1246 '@types/debug': 1220 1247 specifier: 'catalog:' 1221 1248 version: 4.1.12 ··· 3905 3932 '@swc/types@0.1.12': 3906 3933 resolution: {integrity: sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA==} 3907 3934 3935 + '@test/test-dep-error@file:test/cli/deps/error': 3936 + resolution: {directory: test/cli/deps/error, type: directory} 3937 + 3908 3938 '@testing-library/dom@10.4.0': 3909 3939 resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} 3910 3940 engines: {node: '>=18'} ··· 4358 4388 4359 4389 '@vitest/test-dep-conditions@file:test/config/deps/test-dep-conditions': 4360 4390 resolution: {directory: test/config/deps/test-dep-conditions, type: directory} 4361 - 4362 - '@vitest/test-dep-error@file:test/cli/deps/error': 4363 - resolution: {directory: test/cli/deps/error, type: directory} 4364 4391 4365 4392 '@vitest/test-dep1@file:test/core/deps/dep1': 4366 4393 resolution: {directory: test/core/deps/dep1, type: directory} ··· 9554 9581 '@babel/parser': 7.27.5 9555 9582 '@babel/types': 7.27.1 9556 9583 '@jridgewell/gen-mapping': 0.3.5 9557 - '@jridgewell/trace-mapping': 0.3.25 9584 + '@jridgewell/trace-mapping': 0.3.29 9558 9585 jsesc: 3.1.0 9559 9586 9560 9587 '@babel/generator@7.27.5': ··· 9562 9589 '@babel/parser': 7.27.5 9563 9590 '@babel/types': 7.27.6 9564 9591 '@jridgewell/gen-mapping': 0.3.5 9565 - '@jridgewell/trace-mapping': 0.3.25 9592 + '@jridgewell/trace-mapping': 0.3.29 9566 9593 jsesc: 3.1.0 9567 9594 9568 9595 '@babel/generator@7.28.0': ··· 11601 11628 dependencies: 11602 11629 '@swc/counter': 0.1.3 11603 11630 11631 + '@test/test-dep-error@file:test/cli/deps/error': {} 11632 + 11604 11633 '@testing-library/dom@10.4.0': 11605 11634 dependencies: 11606 11635 '@babel/code-frame': 7.26.2 ··· 12212 12241 '@vitest/test-dep-conditions@file:test/config/deps/test-dep-conditions': 12213 12242 dependencies: 12214 12243 '@vitest/test-dep-conditions-indirect': file:test/config/deps/test-dep-conditions-indirect 12215 - 12216 - '@vitest/test-dep-error@file:test/cli/deps/error': {} 12217 12244 12218 12245 '@vitest/test-dep1@file:test/core/deps/dep1': {} 12219 12246 ··· 14752 14779 14753 14780 istanbul-lib-source-maps@5.0.6: 14754 14781 dependencies: 14755 - '@jridgewell/trace-mapping': 0.3.25 14782 + '@jridgewell/trace-mapping': 0.3.29 14756 14783 debug: 4.4.1 14757 14784 istanbul-lib-coverage: 3.2.2 14758 14785 transitivePeerDependencies:
+1
tsconfig.base.json
··· 23 23 "@vitest/browser/client": ["./packages/browser/src/client/client.ts"], 24 24 "~/*": ["./packages/ui/client/*"], 25 25 "vitest": ["./packages/vitest/src/public/index.ts"], 26 + "vitest/internal/module-runner": ["./packages/vitest/src/public/module-runner.ts"], 26 27 "vitest/globals": ["./packages/vitest/globals.d.ts"], 27 28 "vitest/*": ["./packages/vitest/src/public/*"], 28 29 "vite-node": ["./packages/vite-node/src/index.ts"],
+3 -3
docs/advanced/runner.md
··· 121 121 ``` 122 122 123 123 ::: warning 124 - Vitest also injects an instance of `ViteNodeRunner` as `__vitest_executor` property. You can use it to process files in `importFile` method (this is default behavior of `TestRunner` and `BenchmarkRunner`). 124 + Vitest also injects an instance of `ModuleRunner` from `vite/module-runner` as `moduleRunner` property. You can use it to process files in `importFile` method (this is default behavior of `TestRunner` and `BenchmarkRunner`). 125 125 126 - `ViteNodeRunner` exposes `executeId` method, which is used to import test files in a Vite-friendly environment. Meaning, it will resolve imports and transform file content at runtime so that Node can understand it: 126 + `ModuleRunner` exposes `import` method, which is used to import test files in a Vite-friendly environment. Meaning, it will resolve imports and transform file content at runtime so that Node can understand it: 127 127 128 128 ```ts 129 129 export default class Runner { 130 130 async importFile(filepath: string) { 131 - await this.__vitest_executor.executeId(filepath) 131 + await this.moduleRunner.import(filepath) 132 132 } 133 133 } 134 134 ```
+8 -30
docs/config/index.md
··· 233 233 234 234 #### deps.optimizer {#deps-optimizer} 235 235 236 - - **Type:** `{ ssr?, web? }` 236 + - **Type:** `{ ssr?, client? }` 237 237 - **See also:** [Dep Optimization Options](https://vitejs.dev/config/dep-optimization-options.html) 238 238 239 239 Enable dependency optimization. If you have a lot of tests, this might improve their performance. ··· 245 245 - Your `alias` configuration is now respected inside bundled packages 246 246 - Code in your tests is running closer to how it's running in the browser 247 247 248 - Be aware that only packages in `deps.optimizer?.[mode].include` option are bundled (some plugins populate this automatically, like Svelte). You can read more about available options in [Vite](https://vitejs.dev/config/dep-optimization-options.html) docs (Vitest doesn't support `disable` and `noDiscovery` options). By default, Vitest uses `optimizer.web` for `jsdom` and `happy-dom` environments, and `optimizer.ssr` for `node` and `edge` environments, but it is configurable by [`transformMode`](#testtransformmode). 248 + Be aware that only packages in `deps.optimizer?.[mode].include` option are bundled (some plugins populate this automatically, like Svelte). You can read more about available options in [Vite](https://vitejs.dev/config/dep-optimization-options.html) docs (Vitest doesn't support `disable` and `noDiscovery` options). By default, Vitest uses `optimizer.client` for `jsdom` and `happy-dom` environments, and `optimizer.ssr` for `node` and `edge` environments. 249 249 250 250 This options also inherits your `optimizeDeps` configuration (for web Vitest will extend `optimizeDeps`, for ssr - `ssr.optimizeDeps`). If you redefine `include`/`exclude` option in `deps.optimizer` it will extend your `optimizeDeps` when running tests. Vitest automatically removes the same options from `include`, if they are listed in `exclude`. 251 251 ··· 260 260 261 261 Enable dependency optimization. 262 262 263 - #### deps.web {#deps-web} 263 + #### deps.client {#deps-client} 264 264 265 265 - **Type:** `{ transformAssets?, ... }` 266 266 267 - Options that are applied to external files when transform mode is set to `web`. By default, `jsdom` and `happy-dom` use `web` mode, while `node` and `edge` environments use `ssr` transform mode, so these options will have no affect on files inside those environments. 267 + Options that are applied to external files when the environment is set to `client`. By default, `jsdom` and `happy-dom` use `client` environment, while `node` and `edge` environments use `ssr`, so these options will have no affect on files inside those environments. 268 268 269 269 Usually, files inside `node_modules` are externalized, but these options also affect files in [`server.deps.external`](#server-deps-external). 270 270 271 - #### deps.web.transformAssets 271 + #### deps.client.transformAssets 272 272 273 273 - **Type:** `boolean` 274 274 - **Default:** `true` ··· 281 281 At the moment, this option only works with [`vmThreads`](#vmthreads) and [`vmForks`](#vmforks) pools. 282 282 ::: 283 283 284 - #### deps.web.transformCss 284 + #### deps.client.transformCss 285 285 286 286 - **Type:** `boolean` 287 287 - **Default:** `true` ··· 294 294 At the moment, this option only works with [`vmThreads`](#vmthreads) and [`vmForks`](#vmforks) pools. 295 295 ::: 296 296 297 - #### deps.web.transformGlobPattern 297 + #### deps.client.transformGlobPattern 298 298 299 299 - **Type:** `RegExp | RegExp[]` 300 300 - **Default:** `[]` ··· 560 560 561 561 export default <Environment>{ 562 562 name: 'custom', 563 - transformMode: 'ssr', 563 + viteEnvironment: 'ssr', 564 564 setup() { 565 565 // custom setup 566 566 return { ··· 1675 1675 - **Default:** `false` 1676 1676 1677 1677 Will call [`vi.unstubAllGlobals`](/api/vi#vi-unstuballglobals) before each test. 1678 - 1679 - ### testTransformMode {#testtransformmode} 1680 - 1681 - - **Type:** `{ web?, ssr? }` 1682 - 1683 - Determine the transform method for all modules imported inside a test that matches the glob pattern. By default, relies on the environment. For example, tests with JSDOM environment will process all files with `ssr: false` flag and tests with Node environment process all modules with `ssr: true`. 1684 - 1685 - #### testTransformMode.ssr 1686 - 1687 - - **Type:** `string[]` 1688 - - **Default:** `[]` 1689 - 1690 - Use SSR transform pipeline for all modules inside specified tests.<br> 1691 - Vite plugins will receive `ssr: true` flag when processing those files. 1692 - 1693 - #### testTransformMode&#46;web 1694 - 1695 - - **Type:** `string[]` 1696 - - **Default:** `[]` 1697 - 1698 - First do a normal transform pipeline (targeting browser), then do a SSR rewrite to run the code in Node.<br> 1699 - Vite plugins will receive `ssr: false` flag when processing those files. 1700 1678 1701 1679 ### snapshotFormat<NonProjectOption /> 1702 1680
+2 -2
docs/guide/environment.md
··· 48 48 49 49 export default <Environment>{ 50 50 name: 'custom', 51 - transformMode: 'ssr', 51 + viteEnvironment: 'ssr', 52 52 // optional - only if you support "experimental-vm" pool 53 53 async setupVM() { 54 54 const vm = await import('node:vm') ··· 74 74 ``` 75 75 76 76 ::: warning 77 - Vitest requires `transformMode` option on environment object. It should be equal to `ssr` or `web`. This value determines how plugins will transform source code. If it's set to `ssr`, plugin hooks will receive `ssr: true` when transforming or resolving files. Otherwise, `ssr` is set to `false`. 77 + Vitest requires `viteEnvironment` option on environment object (fallbacks to the Vitest environment name by default). It should be equal to `ssr`, `client` or any custom [Vite environment](https://vite.dev/guide/api-environment) name. This value determines which environment is used to process file. 78 78 ::: 79 79 80 80 You also have access to default Vitest environments through `vitest/environments` entry:
+17 -1
docs/guide/migration.md
··· 145 145 ``` 146 146 ::: 147 147 148 + ### Replacing `vite-node` with [Module Runner](https://vite.dev/guide/api-environment-runtimes.html#modulerunner) 149 + 150 + Module Runner is a successor to `vite-node` implemented directly in Vite. Vitest now uses it directly instead of having a wrapper around Vite SSR handler. This means that certain features are no longer available: 151 + 152 + - `VITE_NODE_DEPS_MODULE_DIRECTORIES` environment variable was replaced with `VITEST_MODULE_DIRECTORIES` 153 + - Vitest no longer injects `__vitest_executor` into every [test runner](/advanced/runner). Instead, it injects `moduleRunner` which is an instance of [`ModuleRunner`](https://vite.dev/guide/api-environment-runtimes.html#modulerunner) 154 + - `vitest/execute` entry point was removed. It was always meant to be internal 155 + - [Custom environments](/guide/environment) no longer need to provide a `transformMode` property. Instead, provide `viteEnvironment`. If it is not provided, Vitest will use the environment name to transform files on the server (see [`server.environments`](https://vite.dev/guide/api-environment-instances.html)) 156 + - `vite-node` is no longer a dependency of Vitest 157 + - `deps.optimizer.web` was renamed to [`deps.optimizer.client`](/config/#deps-optimizer-client). You can also use any custom names to apply optimizer configs when using other server environments 158 + 159 + Vite has its own externalization mechanism, but we decided to keep using the old one to reduce the amount of breaking changes. You can keep using [`server.deps`](/config/#server-deps) to inline or externalize packages. 160 + 161 + This update should not be noticeable unless you rely on advanced features mentioned above. 162 + 148 163 ### Deprecated APIs are Removed 149 164 150 165 Vitest 4.0 removes some deprecated APIs, including: ··· 152 167 - `poolMatchGlobs` config option. Use [`projects`](/guide/projects) instead. 153 168 - `environmentMatchGlobs` config option. Use [`projects`](/guide/projects) instead. 154 169 - `workspace` config option. Use [`projects`](/guide/projects) instead. 170 + - `deps.external`, `deps.inline`, `deps.fallbackCJS` config options. Use `server.deps.external`, `server.deps.inline`, or `server.deps.fallbackCJS` instead. 155 171 156 - This release also removes all deprecated types. This finally fixes an issue where Vitest accidentally pulled in `node` types (see [#5481](https://github.com/vitest-dev/vitest/issues/5481) and [#6141](https://github.com/vitest-dev/vitest/issues/6141)). 172 + This release also removes all deprecated types. This finally fixes an issue where Vitest accidentally pulled in `@types/node` (see [#5481](https://github.com/vitest-dev/vitest/issues/5481) and [#6141](https://github.com/vitest-dev/vitest/issues/6141)). 157 173 158 174 ## Migrating from Jest {#jest} 159 175
+1 -1
packages/coverage-v8/package.json
··· 55 55 }, 56 56 "dependencies": { 57 57 "@bcoe/v8-coverage": "^1.0.2", 58 + "@vitest/utils": "workspace:*", 58 59 "ast-v8-to-istanbul": "^0.3.3", 59 60 "debug": "catalog:", 60 61 "istanbul-lib-coverage": "catalog:", ··· 73 74 "@types/istanbul-reports": "catalog:", 74 75 "@vitest/browser": "workspace:*", 75 76 "pathe": "catalog:", 76 - "vite-node": "workspace:*", 77 77 "vitest": "workspace:*" 78 78 } 79 79 }
+4
packages/utils/package.json
··· 28 28 "types": "./dist/ast.d.ts", 29 29 "default": "./dist/ast.js" 30 30 }, 31 + "./resolver": { 32 + "types": "./dist/resolver.d.ts", 33 + "default": "./dist/resolver.js" 34 + }, 31 35 "./error": { 32 36 "types": "./dist/error.d.ts", 33 37 "default": "./dist/error.js"
+1
packages/utils/rollup.config.js
··· 16 16 'error': 'src/error.ts', 17 17 'source-map': 'src/source-map.ts', 18 18 'types': 'src/types.ts', 19 + 'resolver': 'src/resolver.ts', 19 20 } 20 21 21 22 const external = [
+11 -5
packages/vitest/package.json
··· 22 22 "jest" 23 23 ], 24 24 "sideEffects": false, 25 + "imports": { 26 + "#module-evaluator": { 27 + "types": "./dist/module-evaluator.d.ts", 28 + "default": "./dist/module-evaluator.js" 29 + } 30 + }, 25 31 "exports": { 26 32 ".": { 27 33 "import": { ··· 50 56 "types": "./dist/node.d.ts", 51 57 "default": "./dist/node.js" 52 58 }, 53 - "./execute": { 54 - "types": "./dist/execute.d.ts", 55 - "default": "./dist/execute.js" 56 - }, 57 59 "./workers": { 58 60 "types": "./dist/workers.d.ts", 59 61 "import": "./dist/workers.js" ··· 61 63 "./internal/browser": { 62 64 "types": "./dist/browser.d.ts", 63 65 "default": "./dist/browser.js" 66 + }, 67 + "./internal/module-runner": { 68 + "types": "./dist/module-runner.d.ts", 69 + "default": "./dist/module-runner.js" 64 70 }, 65 71 "./runners": { 66 72 "types": "./dist/runners.d.ts", ··· 160 166 "@vitest/utils": "workspace:*", 161 167 "chai": "catalog:", 162 168 "debug": "catalog:", 169 + "es-module-lexer": "^1.7.0", 163 170 "expect-type": "^1.2.2", 164 171 "magic-string": "catalog:", 165 172 "pathe": "catalog:", ··· 171 178 "tinypool": "^1.1.1", 172 179 "tinyrainbow": "catalog:", 173 180 "vite": "^6.0.0 || ^7.0.0-0", 174 - "vite-node": "workspace:*", 175 181 "why-is-node-running": "^2.3.0" 176 182 }, 177 183 "devDependencies": {
+18 -19
packages/vitest/rollup.config.js
··· 28 28 'mocker': 'src/public/mocker.ts', 29 29 'spy': 'src/integrations/spy.ts', 30 30 'coverage': 'src/public/coverage.ts', 31 - 'execute': 'src/public/execute.ts', 32 31 'reporters': 'src/public/reporters.ts', 33 32 // TODO: advanced docs 34 33 'workers': 'src/public/workers.ts', 34 + 'module-runner': 'src/public/module-runner.ts', 35 + 'module-evaluator': 'src/runtime/moduleRunner/moduleEvaluator.ts', 35 36 36 37 // for performance reasons we bundle them separately so we don't import everything at once 37 38 'worker': 'src/runtime/worker.ts', ··· 46 47 } 47 48 48 49 const dtsEntries = { 49 - index: 'src/public/index.ts', 50 - node: 'src/public/node.ts', 51 - environments: 'src/public/environments.ts', 52 - browser: 'src/public/browser.ts', 53 - runners: 'src/public/runners.ts', 54 - suite: 'src/public/suite.ts', 55 - config: 'src/public/config.ts', 56 - coverage: 'src/public/coverage.ts', 57 - execute: 'src/public/execute.ts', 58 - reporters: 'src/public/reporters.ts', 59 - mocker: 'src/public/mocker.ts', 60 - workers: 'src/public/workers.ts', 61 - snapshot: 'src/public/snapshot.ts', 50 + 'index': 'src/public/index.ts', 51 + 'node': 'src/public/node.ts', 52 + 'environments': 'src/public/environments.ts', 53 + 'browser': 'src/public/browser.ts', 54 + 'runners': 'src/public/runners.ts', 55 + 'suite': 'src/public/suite.ts', 56 + 'config': 'src/public/config.ts', 57 + 'coverage': 'src/public/coverage.ts', 58 + 'reporters': 'src/public/reporters.ts', 59 + 'mocker': 'src/public/mocker.ts', 60 + 'workers': 'src/public/workers.ts', 61 + 'snapshot': 'src/public/snapshot.ts', 62 + 'module-evaluator': 'src/runtime/moduleRunner/moduleEvaluator.ts', 62 63 } 63 64 64 65 const external = [ ··· 75 76 'node:console', 76 77 'inspector', 77 78 'vitest/optional-types.js', 78 - 'vite-node/source-map', 79 - 'vite-node/client', 80 - 'vite-node/server', 81 - 'vite-node/constants', 82 - 'vite-node/utils', 79 + 'vite/module-runner', 83 80 '@vitest/mocker', 84 81 '@vitest/mocker/node', 85 82 '@vitest/utils/diff', ··· 90 87 '@vitest/runner/types', 91 88 '@vitest/snapshot/environment', 92 89 '@vitest/snapshot/manager', 90 + 91 + '#module-evaluator', 93 92 ] 94 93 95 94 const dir = dirname(fileURLToPath(import.meta.url))
+1
test/browser/.env.local
··· 1 + VITE_TEST_ENV=local
+3 -3
test/browser/vitest.config.mts
··· 49 49 includeSource: ['src/*.ts'], 50 50 // having a snapshot environment doesn't affect browser tests 51 51 snapshotEnvironment: './custom-snapshot-env.ts', 52 + env: { 53 + CUSTOM_ENV: 'foo', 54 + }, 52 55 browser: { 53 56 enabled: true, 54 57 headless: false, ··· 109 112 outputFile: { 110 113 html: './html/index.html', 111 114 json: './browser.json', 112 - }, 113 - env: { 114 - BROWSER: browser, 115 115 }, 116 116 onConsoleLog(log) { 117 117 if (log.includes('MESSAGE ADDED')) {
+1 -1
test/cli/custom.ts
··· 7 7 8 8 export default <Environment>{ 9 9 name: 'custom', 10 - transformMode: 'ssr', 10 + viteEnvironment: 'ssr', 11 11 setupVM({ custom }) { 12 12 const context = vm.createContext({ 13 13 testEnvironment: 'custom',
+4 -3
test/cli/package.json
··· 3 3 "type": "module", 4 4 "private": true, 5 5 "scripts": { 6 - "test": "vitest" 6 + "test": "vitest", 7 + "stacktraces": "vitest --root=./fixtures/stacktraces --watch=false" 7 8 }, 8 9 "devDependencies": { 10 + "@test/test-dep-error": "file:./deps/error", 11 + "@test/test-dep-linked": "link:./deps/linked", 9 12 "@types/debug": "catalog:", 10 13 "@types/ws": "catalog:", 11 14 "@vitejs/plugin-basic-ssl": "^2.1.0", 12 15 "@vitest/runner": "workspace:^", 13 - "@vitest/test-dep-error": "file:./deps/error", 14 - "@vitest/test-dep-linked": "link:./deps/linked", 15 16 "@vitest/utils": "workspace:*", 16 17 "debug": "^4.4.1", 17 18 "unplugin-swc": "^1.5.5",
+2
test/core/package.json
··· 13 13 "collect": "vitest list" 14 14 }, 15 15 "devDependencies": { 16 + "@test/vite-environment-external": "link:./projects/vite-environment-external", 17 + "@test/vite-external": "link:./projects/vite-external", 16 18 "@types/debug": "catalog:", 17 19 "@vitest/expect": "workspace:*", 18 20 "@vitest/mocker": "workspace:*",
+22 -14
test/core/vite.config.ts
··· 44 44 }, 45 45 resolve: { 46 46 alias: [ 47 - { find: '#', replacement: resolve(import.meta.dirname, 'src') }, 47 + { find: /^#/, replacement: resolve(import.meta.dirname, 'src') }, 48 48 { find: /^custom-lib$/, replacement: resolve(import.meta.dirname, 'projects', 'custom-lib') }, 49 49 { find: /^inline-lib$/, replacement: resolve(import.meta.dirname, 'projects', 'inline-lib') }, 50 50 ], 51 + noExternal: [/projects\/vite-external/], 52 + }, 53 + environments: { 54 + ssr: { 55 + resolve: { 56 + noExternal: [/projects\/vite-environment-external/], 57 + }, 58 + }, 51 59 }, 52 60 server: { 53 61 port: 3022, ··· 72 80 setupFiles: [ 73 81 './test/setup.ts', 74 82 ], 83 + server: { 84 + deps: { 85 + external: [ 86 + 'tinyspy', 87 + /src\/external/, 88 + /esm\/esm/, 89 + /packages\/web-worker/, 90 + /\.wasm$/, 91 + /\/wasm-bindgen-no-cyclic\/index_bg.js/, 92 + ], 93 + inline: ['inline-lib'], 94 + }, 95 + }, 75 96 includeTaskLocation: true, 76 97 reporters: process.env.GITHUB_ACTIONS 77 98 ? ['default', 'github-actions'] ··· 113 134 allowOnly: true, 114 135 deps: { 115 136 moduleDirectories: ['node_modules', 'projects', 'packages'], 116 - }, 117 - server: { 118 - deps: { 119 - external: [ 120 - 'tinyspy', 121 - /src\/external/, 122 - /esm\/esm/, 123 - /packages\/web-worker/, 124 - /\.wasm$/, 125 - /\/wasm-bindgen-no-cyclic\/index_bg.js/, 126 - ], 127 - inline: ['inline-lib'], 128 - }, 129 137 }, 130 138 alias: [ 131 139 {
+1 -1
test/optimize-deps/vitest.config.ts
··· 15 15 }, 16 16 deps: { 17 17 optimizer: { 18 - web: { 18 + client: { 19 19 enabled: true, 20 20 }, 21 21 ssr: {
+4 -4
packages/coverage-istanbul/src/provider.ts
··· 89 89 const start = debug.enabled ? performance.now() : 0 90 90 91 91 const coverageMap = this.createCoverageMap() 92 - let coverageMapByTransformMode = this.createCoverageMap() 92 + let coverageMapByEnvironment = this.createCoverageMap() 93 93 94 94 await this.readCoverageFiles<CoverageMap>({ 95 95 onFileRead(coverage) { 96 - coverageMapByTransformMode.merge(coverage) 96 + coverageMapByEnvironment.merge(coverage) 97 97 }, 98 98 onFinished: async () => { 99 99 // Source maps can change based on projectName and transform mode. 100 100 // Coverage transform re-uses source maps so we need to separate transforms from each other. 101 - const transformedCoverage = await transformCoverage(coverageMapByTransformMode) 101 + const transformedCoverage = await transformCoverage(coverageMapByEnvironment) 102 102 coverageMap.merge(transformedCoverage) 103 103 104 - coverageMapByTransformMode = this.createCoverageMap() 104 + coverageMapByEnvironment = this.createCoverageMap() 105 105 }, 106 106 onDebug: debug, 107 107 })
+6 -4
packages/coverage-v8/src/index.ts
··· 37 37 try { 38 38 const result = coverage.result 39 39 .filter(filterResult) 40 - .map(res => ({ 41 - ...res, 42 - startOffset: options?.moduleExecutionInfo?.get(fileURLToPath(res.url))?.startOffset || 0, 43 - })) 40 + .map((res) => { 41 + return { 42 + ...res, 43 + startOffset: options?.moduleExecutionInfo?.get(fileURLToPath(res.url))?.startOffset || 0, 44 + } 45 + }) 44 46 45 47 resolve({ result }) 46 48 }
+52 -32
packages/coverage-v8/src/provider.ts
··· 1 1 import type { CoverageMap } from 'istanbul-lib-coverage' 2 2 import type { ProxifiedModule } from 'magicast' 3 3 import type { Profiler } from 'node:inspector' 4 - import type { EncodedSourceMap, FetchResult } from 'vite-node' 5 - import type { AfterSuiteRunMeta } from 'vitest' 6 - import type { CoverageProvider, ReportContext, ResolvedCoverageOptions, TestProject, Vitest } from 'vitest/node' 4 + import type { CoverageProvider, ReportContext, ResolvedCoverageOptions, TestProject, Vite, Vitest } from 'vitest/node' 7 5 import { promises as fs } from 'node:fs' 8 6 import { fileURLToPath } from 'node:url' 9 7 // @ts-expect-error -- untyped 10 8 import { mergeProcessCovs } from '@bcoe/v8-coverage' 9 + import { cleanUrl } from '@vitest/utils' 11 10 import astV8ToIstanbul from 'ast-v8-to-istanbul' 12 11 import createDebug from 'debug' 13 12 import libCoverage from 'istanbul-lib-coverage' ··· 17 16 import { parseModule } from 'magicast' 18 17 import { normalize } from 'pathe' 19 18 import { provider } from 'std-env' 20 - import c from 'tinyrainbow' 21 - import { cleanUrl } from 'vite-node/utils' 22 19 20 + import c from 'tinyrainbow' 23 21 import { BaseCoverageProvider } from 'vitest/coverage' 24 - import { parseAstAsync } from 'vitest/node' 22 + import { isCSSRequest, parseAstAsync } from 'vitest/node' 25 23 import { version } from '../package.json' with { type: 'json' } 26 24 27 25 export interface ScriptCoverageWithOffset extends Profiler.ScriptCoverage { 28 26 startOffset: number 29 27 } 30 28 31 - type TransformResults = Map<string, FetchResult> 29 + type TransformResults = Map<string, Vite.TransformResult> 32 30 interface RawCoverage { result: ScriptCoverageWithOffset[] } 33 31 34 32 const FILE_PROTOCOL = 'file://' ··· 65 63 } 66 64 }) 67 65 }, 68 - onFinished: async (project, transformMode) => { 66 + onFinished: async (project, environment) => { 69 67 const converted = await this.convertCoverage( 70 68 merged, 71 69 project, 72 - transformMode, 70 + environment, 73 71 ) 74 72 75 73 // Source maps can change based on projectName and transform mode. ··· 148 146 149 147 private async getCoverageMapForUncoveredFiles(testedFiles: string[]): Promise<CoverageMap> { 150 148 const transformResults = normalizeTransformResults( 151 - this.ctx.vitenode.fetchCache, 149 + this.ctx.vite.environments, 152 150 ) 153 151 const transform = this.createUncoveredFileTransformer(this.ctx) 154 152 ··· 293 291 return true 294 292 } 295 293 294 + // SSR mode's "import.meta.env =" 295 + if ( 296 + type === 'statement' 297 + && node.type === 'ExpressionStatement' 298 + && node.expression.type === 'AssignmentExpression' 299 + && node.expression.left.type === 'MemberExpression' 300 + && node.expression.left.object.type === 'Identifier' 301 + && node.expression.left.object.name === '__vite_ssr_import_meta__') { 302 + return true 303 + } 304 + 296 305 // SWC's decorators 297 306 if ( 298 307 type === 'statement' ··· 307 316 ) 308 317 } 309 318 310 - private async getSources<TransformResult extends (FetchResult | Awaited<ReturnType<typeof this.ctx.vitenode.transformRequest>>)>( 319 + private async getSources( 311 320 url: string, 312 321 transformResults: TransformResults, 313 - onTransform: (filepath: string) => Promise<TransformResult>, 322 + onTransform: (filepath: string) => Promise<Vite.TransformResult | undefined | null>, 314 323 functions: Profiler.FunctionCoverage[] = [], 315 324 ): Promise<{ 316 325 code: string 317 - map?: EncodedSourceMap 326 + map?: Vite.Rollup.SourceMap 318 327 }> { 319 328 const filePath = normalize(fileURLToPath(url)) 320 329 321 - let transformResult: FetchResult | TransformResult | undefined = transformResults.get(filePath) 330 + let transformResult: Vite.TransformResult | null | undefined = transformResults.get(filePath) 322 331 323 332 if (!transformResult) { 324 333 transformResult = await onTransform(removeStartsWith(url, FILE_PROTOCOL)).catch(() => undefined) 325 334 } 326 335 327 - const map = transformResult?.map as EncodedSourceMap | undefined 336 + const map = transformResult?.map as Vite.Rollup.SourceMap | undefined 328 337 const code = transformResult?.code 329 338 330 - if (!code) { 339 + if (code == null) { 331 340 const original = await fs.readFile(filePath, 'utf-8').catch(() => { 332 341 // If file does not exist construct a dummy source for it. 333 342 // These can be files that were generated dynamically during the test run and were removed after it. ··· 357 366 private async convertCoverage( 358 367 coverage: RawCoverage, 359 368 project: TestProject = this.ctx.getRootProject(), 360 - transformMode?: AfterSuiteRunMeta['transformMode'], 369 + environment: string, 361 370 ): Promise<CoverageMap> { 362 - let fetchCache = project.vitenode.fetchCache 363 - 364 - if (transformMode) { 365 - fetchCache = transformMode === 'browser' ? new Map() : project.vitenode.fetchCaches[transformMode] 371 + if (environment === '__browser__' && !project.browser) { 372 + throw new Error(`Cannot access browser module graph because it was torn down.`) 366 373 } 367 374 368 - const transformResults = normalizeTransformResults(fetchCache) 375 + const moduleGraph = environment === '__browser__' 376 + ? project.browser!.vite.environments.client.moduleGraph 377 + : project.vite.environments[environment]?.moduleGraph 378 + 379 + if (!moduleGraph) { 380 + throw new Error(`Module graph for environment ${environment} was not defined.`) 381 + } 382 + 383 + const transformResults = normalizeTransformResults({ [environment]: { moduleGraph } }) 369 384 370 385 async function onTransform(filepath: string) { 371 - if (transformMode === 'browser' && project.browser) { 386 + if (environment === '__browser__' && project.browser) { 372 387 const result = await project.browser.vite.transformRequest(removeStartsWith(filepath, project.config.root)) 373 388 374 389 if (result) { 375 390 return { ...result, code: `${result.code}// <inline-source-map>` } 376 391 } 377 392 } 378 - return project.vitenode.transformRequest(filepath) 393 + return project.vite.environments[environment].transformRequest(filepath) 379 394 } 380 395 381 396 const scriptCoverages = [] 382 397 383 398 for (const result of coverage.result) { 384 - if (transformMode === 'browser') { 399 + if (environment === '__browser__') { 385 400 if (result.url.startsWith('/@fs')) { 386 401 result.url = `${FILE_PROTOCOL}${removeStartsWith(result.url, '/@fs')}` 387 402 } ··· 393 408 } 394 409 } 395 410 396 - if (this.isIncluded(fileURLToPath(result.url))) { 411 + // Ignore all CSS requests, so we don't override the actual code coverage 412 + // In cases where CSS and JS are in the same file (.vue, .svelte) 413 + // The file has a `.vue` extension, but the URL has `lang.css` query 414 + if (!isCSSRequest(result.url) && this.isIncluded(fileURLToPath(result.url))) { 397 415 scriptCoverages.push(result) 398 416 } 399 417 } ··· 466 484 } 467 485 468 486 function normalizeTransformResults( 469 - fetchCache: Map<string, { result: FetchResult }>, 487 + environments: Record<string, { moduleGraph: Vite.EnvironmentModuleGraph }>, 470 488 ) { 471 489 const normalized: TransformResults = new Map() 472 490 473 - for (const [key, value] of fetchCache.entries()) { 474 - const cleanEntry = cleanUrl(key) 475 - 476 - if (!normalized.has(cleanEntry)) { 477 - normalized.set(cleanEntry, value.result) 491 + for (const environmentName in environments) { 492 + const moduleGraph = environments[environmentName].moduleGraph 493 + for (const [key, value] of moduleGraph.idToModuleMap) { 494 + const cleanEntry = cleanUrl(key) 495 + if (value.transformResult && !normalized.has(cleanEntry)) { 496 + normalized.set(cleanEntry, value.transformResult) 497 + } 478 498 } 479 499 } 480 500
+4
packages/mocker/src/registry.ts
··· 135 135 this.registryByUrl.delete(id) 136 136 } 137 137 138 + public deleteById(id: string): void { 139 + this.registryById.delete(id) 140 + } 141 + 138 142 public get(id: string): MockedModule | undefined { 139 143 return this.registryByUrl.get(id) 140 144 }
+60
packages/utils/src/constants.ts
··· 1 + // TODO: this is all copy pasted from Vite - can they expose a module that exports only constants? 2 + 3 + export const KNOWN_ASSET_TYPES: string[] = [ 4 + // images 5 + 'apng', 6 + 'bmp', 7 + 'png', 8 + 'jpe?g', 9 + 'jfif', 10 + 'pjpeg', 11 + 'pjp', 12 + 'gif', 13 + 'svg', 14 + 'ico', 15 + 'webp', 16 + 'avif', 17 + 18 + // media 19 + 'mp4', 20 + 'webm', 21 + 'ogg', 22 + 'mp3', 23 + 'wav', 24 + 'flac', 25 + 'aac', 26 + 27 + // fonts 28 + 'woff2?', 29 + 'eot', 30 + 'ttf', 31 + 'otf', 32 + 33 + // other 34 + 'webmanifest', 35 + 'pdf', 36 + 'txt', 37 + ] 38 + 39 + export const KNOWN_ASSET_RE: RegExp = new RegExp( 40 + `\\.(${KNOWN_ASSET_TYPES.join('|')})$`, 41 + ) 42 + export const CSS_LANGS_RE: RegExp 43 + = /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/ 44 + /** 45 + * Prefix for resolved Ids that are not valid browser import specifiers 46 + */ 47 + export const VALID_ID_PREFIX = `/@id/` 48 + 49 + /** 50 + * Plugins that use 'virtual modules' (e.g. for helper functions), prefix the 51 + * module ID with `\0`, a convention from the rollup ecosystem. 52 + * This prevents other plugins from trying to process the id (like node resolution), 53 + * and core features like sourcemaps can use this info to differentiate between 54 + * virtual modules and regular files. 55 + * `\0` is not a permitted char in import URLs so we have to replace them during 56 + * import analysis. The id will be decoded back before entering the plugins pipeline. 57 + * These encoded virtual ids are also prefixed by the VALID_ID_PREFIX, so virtual 58 + * modules in the browser end up encoded as `/@id/__x00__{id}` 59 + */ 60 + export const NULL_BYTE_PLACEHOLDER = `__x00__`
+42
packages/utils/src/helpers.ts
··· 1 1 import type { Arrayable, Nullable } from './types' 2 + import { NULL_BYTE_PLACEHOLDER, VALID_ID_PREFIX } from './constants' 2 3 3 4 interface CloneOptions { 4 5 forceWritable?: boolean ··· 54 55 55 56 export function slash(path: string): string { 56 57 return path.replace(/\\/g, '/') 58 + } 59 + 60 + const postfixRE = /[?#].*$/ 61 + export function cleanUrl(url: string): string { 62 + return url.replace(postfixRE, '') 63 + } 64 + 65 + const externalRE = /^(?:[a-z]+:)?\/\// 66 + export const isExternalUrl = (url: string): boolean => externalRE.test(url) 67 + 68 + /** 69 + * Prepend `/@id/` and replace null byte so the id is URL-safe. 70 + * This is prepended to resolved ids that are not valid browser 71 + * import specifiers by the importAnalysis plugin. 72 + */ 73 + export function wrapId(id: string): string { 74 + return id.startsWith(VALID_ID_PREFIX) 75 + ? id 76 + : VALID_ID_PREFIX + id.replace('\0', NULL_BYTE_PLACEHOLDER) 77 + } 78 + 79 + /** 80 + * Undo {@link wrapId}'s `/@id/` and null byte replacements. 81 + */ 82 + export function unwrapId(id: string): string { 83 + return id.startsWith(VALID_ID_PREFIX) 84 + ? id.slice(VALID_ID_PREFIX.length).replace(NULL_BYTE_PLACEHOLDER, '\0') 85 + : id 86 + } 87 + 88 + export function withTrailingSlash(path: string): string { 89 + if (path[path.length - 1] !== '/') { 90 + return `${path}/` 91 + } 92 + return path 93 + } 94 + 95 + const bareImportRE = /^(?![a-z]:)[\w@](?!.*:\/\/)/i 96 + 97 + export function isBareImport(id: string): boolean { 98 + return bareImportRE.test(id) 57 99 } 58 100 59 101 // convert RegExp.toString to RegExp
+13
packages/utils/src/index.ts
··· 1 1 export { 2 + CSS_LANGS_RE, 3 + KNOWN_ASSET_RE, 4 + KNOWN_ASSET_TYPES, 5 + NULL_BYTE_PLACEHOLDER, 6 + VALID_ID_PREFIX, 7 + } from './constants' 8 + export { 2 9 format, 3 10 inspect, 4 11 objDisplay, ··· 8 15 9 16 export { 10 17 assertTypes, 18 + cleanUrl, 11 19 clone, 12 20 createDefer, 13 21 createSimpleStackTrace, ··· 16 24 getCallLastIndex, 17 25 getOwnProperties, 18 26 getType, 27 + isBareImport, 28 + isExternalUrl, 19 29 isNegativeNaN, 20 30 isObject, 21 31 isPrimitive, ··· 25 35 parseRegexp, 26 36 slash, 27 37 toArray, 38 + unwrapId, 39 + withTrailingSlash, 40 + wrapId, 28 41 } from './helpers' 29 42 export type { DeferPromise } from './helpers' 30 43
+99
packages/utils/src/resolver.ts
··· 1 + import fs from 'node:fs' 2 + import { dirname, join } from 'pathe' 3 + 4 + const packageCache = new Map<string, { type?: 'module' | 'commonjs' }>() 5 + 6 + export function findNearestPackageData( 7 + basedir: string, 8 + ): { type?: 'module' | 'commonjs' } { 9 + const originalBasedir = basedir 10 + while (basedir) { 11 + const cached = getCachedData(packageCache, basedir, originalBasedir) 12 + if (cached) { 13 + return cached 14 + } 15 + 16 + const pkgPath = join(basedir, 'package.json') 17 + if (tryStatSync(pkgPath)?.isFile()) { 18 + const pkgData = JSON.parse(stripBomTag(fs.readFileSync(pkgPath, 'utf8'))) 19 + 20 + if (packageCache) { 21 + setCacheData(packageCache, pkgData, basedir, originalBasedir) 22 + } 23 + 24 + return pkgData 25 + } 26 + 27 + const nextBasedir = dirname(basedir) 28 + if (nextBasedir === basedir) { 29 + break 30 + } 31 + basedir = nextBasedir 32 + } 33 + 34 + return {} 35 + } 36 + 37 + function stripBomTag(content: string): string { 38 + if (content.charCodeAt(0) === 0xFEFF) { 39 + return content.slice(1) 40 + } 41 + 42 + return content 43 + } 44 + 45 + function tryStatSync(file: string): fs.Stats | undefined { 46 + try { 47 + // The "throwIfNoEntry" is a performance optimization for cases where the file does not exist 48 + return fs.statSync(file, { throwIfNoEntry: false }) 49 + } 50 + catch { 51 + // Ignore errors 52 + } 53 + } 54 + 55 + export function getCachedData<T>( 56 + cache: Map<string, T>, 57 + basedir: string, 58 + originalBasedir: string, 59 + ): NonNullable<T> | undefined { 60 + const pkgData = cache.get(getFnpdCacheKey(basedir)) 61 + if (pkgData) { 62 + traverseBetweenDirs(originalBasedir, basedir, (dir) => { 63 + cache.set(getFnpdCacheKey(dir), pkgData) 64 + }) 65 + return pkgData 66 + } 67 + } 68 + 69 + export function setCacheData<T>( 70 + cache: Map<string, T>, 71 + data: T, 72 + basedir: string, 73 + originalBasedir: string, 74 + ): void { 75 + cache.set(getFnpdCacheKey(basedir), data) 76 + traverseBetweenDirs(originalBasedir, basedir, (dir) => { 77 + cache.set(getFnpdCacheKey(dir), data) 78 + }) 79 + } 80 + 81 + function getFnpdCacheKey(basedir: string) { 82 + return `fnpd_${basedir}` 83 + } 84 + 85 + /** 86 + * Traverse between `longerDir` (inclusive) and `shorterDir` (exclusive) and call `cb` for each dir. 87 + * @param longerDir Longer dir path, e.g. `/User/foo/bar/baz` 88 + * @param shorterDir Shorter dir path, e.g. `/User/foo` 89 + */ 90 + function traverseBetweenDirs( 91 + longerDir: string, 92 + shorterDir: string, 93 + cb: (dir: string) => void, 94 + ) { 95 + while (longerDir !== shorterDir) { 96 + cb(longerDir) 97 + longerDir = dirname(longerDir) 98 + } 99 + }
+2
packages/utils/src/source-map.ts
··· 34 34 '/node_modules/chai/', 35 35 '/node_modules/tinypool/', 36 36 '/node_modules/tinyspy/', 37 + '/vite/dist/node/module-runner', 38 + '/rolldown-vite/dist/node/module-runner', 37 39 // browser related deps 38 40 '/deps/chunk-', 39 41 '/deps/@vitest',
-7
packages/vitest/src/constants.ts
··· 5 5 6 6 export const API_PATH = '/__vitest_api__' 7 7 8 - export const extraInlineDeps: RegExp[] = [ 9 - /^(?!.*node_modules).*\.mjs$/, 10 - /^(?!.*node_modules).*\.cjs\.js$/, 11 - // Vite client 12 - /vite\w*\/dist\/client\/env.mjs/, 13 - ] 14 - 15 8 export const CONFIG_NAMES: string[] = ['vitest.config', 'vite.config'] 16 9 17 10 export const CONFIG_EXTENSIONS: string[] = ['.ts', '.mts', '.cts', '.js', '.mjs', '.cjs']
+31 -15
packages/web-worker/src/runner.ts
··· 1 - import { VitestExecutor } from 'vitest/execute' 1 + import type { VitestModuleRunner } from 'vitest/internal/module-runner' 2 + import { 3 + getWorkerState, 4 + startVitestModuleRunner, 5 + VITEST_VM_CONTEXT_SYMBOL, 6 + VitestModuleEvaluator, 7 + } from 'vitest/internal/module-runner' 2 8 3 - export class InlineWorkerRunner extends VitestExecutor { 4 - constructor(options: any, private context: any) { 5 - // share the same mocker as main executor 6 - const mocker = (globalThis as any).__vitest_mocker__ 7 - super(options) 8 - this.mocker = (globalThis as any).__vitest_mocker__ = mocker 9 - } 9 + export function startWebWorkerModuleRunner(context: Record<string, unknown>): Promise<VitestModuleRunner> { 10 + const state = getWorkerState() 11 + const mocker = (globalThis as any).__vitest_mocker__ 10 12 11 - prepareContext(context: Record<string, any>): any { 12 - const ctx = super.prepareContext(context) 13 - // not supported for now, we can't synchronously load modules 14 - return Object.assign(ctx, this.context, { 15 - importScripts, 16 - }) 17 - } 13 + const compiledFunctionArgumentsNames = Object.keys(context) 14 + const compiledFunctionArgumentsValues = Object.values(context) 15 + compiledFunctionArgumentsNames.push('importScripts') 16 + compiledFunctionArgumentsValues.push(importScripts) 17 + 18 + const vm = (globalThis as any)[VITEST_VM_CONTEXT_SYMBOL] 19 + 20 + const evaluator = new VitestModuleEvaluator(vm, { 21 + interopDefault: state.config.deps.interopDefault, 22 + moduleExecutionInfo: state.moduleExecutionInfo, 23 + getCurrentTestFilepath: () => state.filepath, 24 + compiledFunctionArgumentsNames, 25 + compiledFunctionArgumentsValues, 26 + }) 27 + 28 + return startVitestModuleRunner({ 29 + evaluator, 30 + evaluatedModules: state.evaluatedModules, 31 + mocker, 32 + state, 33 + }) 18 34 } 19 35 20 36 function importScripts() {
+22 -35
packages/web-worker/src/shared-worker.ts
··· 1 1 import type { MessagePort as NodeMessagePort } from 'node:worker_threads' 2 2 import type { Procedure } from './types' 3 - import { 4 - MessageChannel, 5 - 6 - } from 'node:worker_threads' 7 - import { InlineWorkerRunner } from './runner' 8 - import { debug, getFileIdFromUrl, getRunnerOptions } from './utils' 3 + import { MessageChannel } from 'node:worker_threads' 4 + import { startWebWorkerModuleRunner } from './runner' 5 + import { debug, getFileIdFromUrl } from './utils' 9 6 10 7 function convertNodePortToWebPort(port: NodeMessagePort): MessagePort { 11 8 if (!('addEventListener' in port)) { ··· 53 50 } 54 51 55 52 export function createSharedWorkerConstructor(): typeof SharedWorker { 56 - const runnerOptions = getRunnerOptions() 57 - 58 53 return class SharedWorker extends EventTarget { 59 54 static __VITEST_WEB_WORKER__ = true 60 55 ··· 83 78 onrtctransform: null, 84 79 onunhandledrejection: null, 85 80 origin: typeof location !== 'undefined' ? location.origin : 'http://localhost:3000', 86 - importScripts: () => { 87 - throw new Error( 88 - '[vitest] `importScripts` is not supported in Vite workers. Please, consider using `import` instead.', 89 - ) 90 - }, 91 81 crossOriginIsolated: false, 92 82 onconnect: null as ((e: MessageEvent) => void) | null, 93 83 name: name || '', ··· 123 113 context.onconnect?.(e as MessageEvent) 124 114 }) 125 115 126 - const runner = new InlineWorkerRunner(runnerOptions, context) 116 + const fileId = getFileIdFromUrl(url) 127 117 128 - const id = getFileIdFromUrl(url) 118 + this._vw_name = fileId 129 119 130 - this._vw_name = id 120 + startWebWorkerModuleRunner(context) 121 + .then((runner) => { 122 + return runner.mocker.resolveId(fileId).then(({ url, id: resolvedId }) => { 123 + this._vw_name = name ?? url 124 + debug('initialize shared worker %s', this._vw_name) 131 125 132 - runner 133 - .resolveUrl(id) 134 - .then(([, fsPath]) => { 135 - this._vw_name = name ?? fsPath 136 - 137 - debug('initialize shared worker %s', this._vw_name) 138 - 139 - return runner.executeFile(fsPath).then(() => { 140 - // worker should be new every time, invalidate its sub dependency 141 - runnerOptions.moduleCache.invalidateSubDepTree([ 142 - fsPath, 143 - runner.mocker.getMockPath(fsPath), 144 - ]) 145 - this._vw_workerTarget.dispatchEvent( 146 - new MessageEvent('connect', { 147 - ports: [this._vw_workerPort], 148 - }), 149 - ) 150 - debug('shared worker %s successfully initialized', this._vw_name) 126 + return runner.import(url).then(() => { 127 + runner._invalidateSubTreeById([ 128 + resolvedId, 129 + runner.mocker.getMockPath(resolvedId), 130 + ]) 131 + this._vw_workerTarget.dispatchEvent( 132 + new MessageEvent('connect', { 133 + ports: [this._vw_workerPort], 134 + }), 135 + ) 136 + debug('shared worker %s successfully initialized', this._vw_name) 137 + }) 151 138 }) 152 139 }) 153 140 .catch((e) => {
-30
packages/web-worker/src/utils.ts
··· 1 1 import type { WorkerGlobalState } from 'vitest' 2 2 import type { CloneOption } from './types' 3 - import { readFileSync as _readFileSync } from 'node:fs' 4 3 import ponyfillStructuredClone from '@ungap/structured-clone' 5 4 import createDebug from 'debug' 6 - 7 - // keep the reference in case it was mocked 8 - const readFileSync = _readFileSync 9 5 10 6 export const debug: createDebug.Debugger = createDebug('vitest:web-worker') 11 7 ··· 76 72 return new MessageEvent('messageerror', { 77 73 data: error, 78 74 }) 79 - } 80 - } 81 - 82 - export function getRunnerOptions(): any { 83 - const state = getWorkerState() 84 - const { config, rpc, moduleCache, moduleExecutionInfo } = state 85 - 86 - return { 87 - async fetchModule(id: string) { 88 - const result = await rpc.fetch(id, 'web') 89 - if (result.id && !result.externalize) { 90 - const code = readFileSync(result.id, 'utf-8') 91 - return { code } 92 - } 93 - return result 94 - }, 95 - resolveId(id: string, importer?: string) { 96 - return rpc.resolveId(id, importer, 'web') 97 - }, 98 - moduleCache, 99 - moduleExecutionInfo, 100 - interopDefault: config.deps.interopDefault ?? true, 101 - moduleDirectories: config.deps.moduleDirectories, 102 - root: config.root, 103 - base: config.base, 104 - state, 105 75 } 106 76 } 107 77
+23 -33
packages/web-worker/src/worker.ts
··· 3 3 DefineWorkerOptions, 4 4 Procedure, 5 5 } from './types' 6 - import { InlineWorkerRunner } from './runner' 6 + import { startWebWorkerModuleRunner } from './runner' 7 7 import { 8 8 createMessageEvent, 9 9 debug, 10 10 getFileIdFromUrl, 11 - getRunnerOptions, 12 11 } from './utils' 13 12 14 13 export function createWorkerConstructor( 15 14 options?: DefineWorkerOptions, 16 15 ): typeof Worker { 17 - const runnerOptions = getRunnerOptions() 18 16 const cloneType = () => 19 17 (options?.clone 20 18 ?? process.env.VITEST_WEB_WORKER_CLONE ··· 58 56 onrtctransform: null, 59 57 onunhandledrejection: null, 60 58 origin: typeof location !== 'undefined' ? location.origin : 'http://localhost:3000', 61 - importScripts: () => { 62 - throw new Error( 63 - '[vitest] `importScripts` is not supported in Vite workers. Please, consider using `import` instead.', 64 - ) 65 - }, 66 59 crossOriginIsolated: false, 67 60 name: options?.name || '', 68 61 close: () => this.terminate(), ··· 119 112 this.onmessageerror?.(e) 120 113 }) 121 114 122 - const runner = new InlineWorkerRunner(runnerOptions, context) 115 + const fileId = getFileIdFromUrl(url) 123 116 124 - const id = getFileIdFromUrl(url) 117 + this._vw_name = fileId 125 118 126 - this._vw_name = id 119 + startWebWorkerModuleRunner(context) 120 + .then((runner) => { 121 + return runner.mocker.resolveId(fileId).then(({ url, id: resolvedId }) => { 122 + this._vw_name = options?.name ?? url 123 + debug('initialize worker %s', this._vw_name) 127 124 128 - runner 129 - .resolveUrl(id) 130 - .then(([, fsPath]) => { 131 - this._vw_name = options?.name ?? fsPath 132 - 133 - debug('initialize worker %s', this._vw_name) 134 - 135 - return runner.executeFile(fsPath).then(() => { 136 - // worker should be new every time, invalidate its sub dependency 137 - runnerOptions.moduleCache.invalidateSubDepTree([ 138 - fsPath, 139 - runner.mocker.getMockPath(fsPath), 140 - ]) 141 - const q = this._vw_messageQueue 142 - this._vw_messageQueue = null 143 - if (q) { 144 - q.forEach( 145 - ([data, transfer]) => this.postMessage(data, transfer), 146 - this, 147 - ) 148 - } 149 - debug('worker %s successfully initialized', this._vw_name) 125 + return runner.import(url).then(() => { 126 + runner._invalidateSubTreeById([ 127 + resolvedId, 128 + runner.mocker.getMockPath(resolvedId), 129 + ]) 130 + const q = this._vw_messageQueue 131 + this._vw_messageQueue = null 132 + if (q) { 133 + q.forEach( 134 + ([data, transfer]) => this.postMessage(data, transfer), 135 + this, 136 + ) 137 + } 138 + debug('worker %s successfully initialized', this._vw_name) 139 + }) 150 140 }) 151 141 }) 152 142 .catch((e) => {
+3
test/browser/src/env.ts
··· 1 + export function getAuthToken() { 2 + return import.meta.env.AUTH_TOKEN 3 + }
+40
test/browser/test/env.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + import { getAuthToken } from '../src/env' 3 + 4 + test('reads envs from .env file', () => { 5 + expect(import.meta.env.VITE_TEST_ENV).toBe('local') 6 + }) 7 + 8 + test('can reassign env locally', () => { 9 + import.meta.env.VITEST_ENV = 'TEST' 10 + expect(import.meta.env.VITEST_ENV).toBe('TEST') 11 + }) 12 + 13 + test('can reassign env everywhere', () => { 14 + import.meta.env.AUTH_TOKEN = '123' 15 + expect(getAuthToken()).toBe('123') 16 + }) 17 + 18 + test('custom env', () => { 19 + expect(import.meta.env.CUSTOM_ENV).toBe('foo') 20 + }) 21 + 22 + test('ignores import.meta.env in string literals', () => { 23 + expect('import.meta.env').toBe('import' + '.meta.env') 24 + }) 25 + 26 + test('define process and using import.meta.env together', () => { 27 + const process = {} 28 + expect(process).toMatchObject({}) 29 + expect(import.meta.env.MODE).toEqual('test') 30 + }) 31 + 32 + test('PROD, DEV, SSR should be boolean', () => { 33 + expect(import.meta.env.PROD).toBe(false) 34 + expect(import.meta.env.DEV).toBe(true) 35 + 36 + expect(import.meta.env.SSR).toBe(false) 37 + 38 + import.meta.env.SSR = true 39 + expect(import.meta.env.SSR).toEqual(true) 40 + })
+1 -1
test/cli/test/inspect.test.ts
··· 51 51 52 52 await vitest.waitForStdout('Test Files 1 passed (1)') 53 53 await waitForClose() 54 - }) 54 + }, 5_000) 55 55 56 56 async function createChannel(url: string) { 57 57 const ws = new WebSocket(url, { allowSynchronousEvents: false })
+1 -1
test/cli/test/stacktraces.test.ts
··· 82 82 root, 83 83 server: { 84 84 deps: { 85 - inline: [/@vitest\/test-dep-error/], 85 + inline: [/@test\/test-dep-error/], 86 86 }, 87 87 }, 88 88 }, [testFile])
-111
test/config/test/override.test.ts
··· 1 1 import type { UserConfig as ViteUserConfig } from 'vite' 2 2 import type { TestUserConfig } from 'vitest/node' 3 3 import { describe, expect, it, onTestFinished } from 'vitest' 4 - import { extraInlineDeps } from 'vitest/config' 5 4 import { createVitest, parseCLI } from 'vitest/node' 6 5 7 6 type VitestOptions = Parameters<typeof createVitest>[3] ··· 126 125 expect(c.poolOptions?.forks?.isolate).toBe(undefined) 127 126 // set in configDefaults, so it's always defined 128 127 expect(c.isolate).toBe(true) 129 - }) 130 - }) 131 - 132 - describe('correctly defines inline and noExternal flags', async () => { 133 - it('both are true if inline is true', async () => { 134 - const v = await vitest({}, { 135 - server: { 136 - deps: { 137 - inline: true, 138 - }, 139 - }, 140 - }) 141 - expect(v.vitenode.options.deps?.inline).toBe(true) 142 - expect(v.vitenode.server.config.ssr.noExternal).toBe(true) 143 - }) 144 - 145 - it('both are true if noExternal is true', async () => { 146 - const v = await vitest({}, {}, { 147 - ssr: { 148 - noExternal: true, 149 - }, 150 - }) 151 - expect(v.vitenode.options.deps?.inline).toBe(true) 152 - expect(v.vitenode.server.config.ssr.noExternal).toBe(true) 153 - }) 154 - 155 - it('inline are added to noExternal', async () => { 156 - const regexp1 = /dep1/ 157 - const regexp2 = /dep2/ 158 - 159 - const v = await vitest({}, { 160 - server: { 161 - deps: { 162 - inline: ['dep1', 'dep2', regexp1, regexp2], 163 - }, 164 - }, 165 - }) 166 - 167 - expect(v.vitenode.options.deps?.inline).toEqual([ 168 - 'dep1', 169 - 'dep2', 170 - regexp1, 171 - regexp2, 172 - ...extraInlineDeps, 173 - ]) 174 - expect(v.server.config.ssr.noExternal).toEqual([ 175 - 'dep1', 176 - 'dep2', 177 - regexp1, 178 - regexp2, 179 - ...extraInlineDeps, 180 - ]) 181 - }) 182 - 183 - it('noExternal are added to inline', async () => { 184 - const regexp1 = /dep1/ 185 - const regexp2 = /dep2/ 186 - 187 - const v = await vitest({}, {}, { 188 - ssr: { 189 - noExternal: ['dep1', 'dep2', regexp1, regexp2], 190 - }, 191 - }) 192 - 193 - expect(v.vitenode.options.deps?.inline).toEqual([ 194 - ...extraInlineDeps, 195 - 'dep1', 196 - 'dep2', 197 - regexp1, 198 - regexp2, 199 - ]) 200 - expect(v.server.config.ssr.noExternal).toEqual([ 201 - 'dep1', 202 - 'dep2', 203 - regexp1, 204 - regexp2, 205 - ]) 206 - }) 207 - 208 - it('noExternal and inline don\'t have duplicates', async () => { 209 - const regexp1 = /dep1/ 210 - const regexp2 = /dep2/ 211 - 212 - const v = await vitest({}, { 213 - server: { 214 - deps: { 215 - inline: ['dep2', regexp1, 'dep3'], 216 - }, 217 - }, 218 - }, { 219 - ssr: { 220 - noExternal: ['dep1', 'dep2', regexp1, regexp2], 221 - }, 222 - }) 223 - 224 - expect(v.vitenode.options.deps?.inline).toEqual([ 225 - 'dep2', 226 - regexp1, 227 - 'dep3', 228 - ...extraInlineDeps, 229 - 'dep1', 230 - regexp2, 231 - ]) 232 - expect(v.server.config.ssr.noExternal).toEqual([ 233 - 'dep1', 234 - 'dep2', 235 - regexp1, 236 - regexp2, 237 - 'dep3', 238 - ]) 239 128 }) 240 129 }) 241 130
+1 -1
test/config/test/rollup-error.test.ts
··· 10 10 }) 11 11 expect(stdout).toContain(`Error: Missing &quot;./no-such-export&quot; specifier in &quot;${rolldownVersion ? 'rolldown-vite' : 'vite'}&quot; package`) 12 12 expect(stdout).toContain(`Plugin: vite:import-analysis`) 13 - expect(stdout).toContain(`Error: Failed to load url @vitejs/no-such-package`) 13 + expect(stdout).toContain(`Error: Cannot find package &apos;@vitejs/no-such-package&apos;`) 14 14 }) 15 15 16 16 test('rollup error web', async () => {
+16 -8
test/core/test/exports.test.ts
··· 16 16 .toMatchInlineSnapshot(` 17 17 { 18 18 ".": { 19 + "EvaluatedModules": "function", 19 20 "afterAll": "function", 20 21 "afterEach": "function", 21 22 "assert": "function", ··· 46 47 "defaultInclude": "object", 47 48 "defineConfig": "function", 48 49 "defineProject": "function", 49 - "extraInlineDeps": "object", 50 50 "mergeConfig": "function", 51 51 }, 52 52 "./coverage": { ··· 55 55 "./environments": { 56 56 "builtinEnvironments": "object", 57 57 "populateGlobal": "function", 58 - }, 59 - "./execute": { 60 - "VitestExecutor": "function", 61 58 }, 62 59 "./internal/browser": { 63 60 "SpyModule": "object", ··· 76 73 "stopCoverageInsideWorker": "function", 77 74 "stringify": "function", 78 75 "takeCoverageInsideWorker": "function", 76 + }, 77 + "./internal/module-runner": { 78 + "VITEST_VM_CONTEXT_SYMBOL": "string", 79 + "VitestModuleEvaluator": "function", 80 + "VitestModuleRunner": "function", 81 + "getWorkerState": "function", 82 + "startVitestModuleRunner": "function", 79 83 }, 80 84 "./mocker": { 81 85 "AutomockedModule": "function", ··· 169 173 .toMatchInlineSnapshot(` 170 174 { 171 175 ".": { 176 + "EvaluatedModules": "function", 172 177 "afterAll": "function", 173 178 "afterEach": "function", 174 179 "assert": "function", ··· 199 204 "defaultInclude": "object", 200 205 "defineConfig": "function", 201 206 "defineProject": "function", 202 - "extraInlineDeps": "object", 203 207 "mergeConfig": "function", 204 208 }, 205 209 "./coverage": { ··· 208 212 "./environments": { 209 213 "builtinEnvironments": "object", 210 214 "populateGlobal": "function", 211 - }, 212 - "./execute": { 213 - "VitestExecutor": "function", 214 215 }, 215 216 "./internal/browser": { 216 217 "SpyModule": "object", ··· 229 230 "stopCoverageInsideWorker": "function", 230 231 "stringify": "function", 231 232 "takeCoverageInsideWorker": "function", 233 + }, 234 + "./internal/module-runner": { 235 + "VITEST_VM_CONTEXT_SYMBOL": "string", 236 + "VitestModuleEvaluator": "function", 237 + "VitestModuleRunner": "function", 238 + "getWorkerState": "function", 239 + "startVitestModuleRunner": "function", 232 240 }, 233 241 "./mocker": { 234 242 "AutomockedModule": "function",
+7
test/core/test/import-client.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + // @ts-expect-error not typed import 3 + import * as client from '/@vite/client' 4 + 5 + test('client is imported', () => { 6 + expect(client).toHaveProperty('createHotContext') 7 + })
+2 -2
test/core/test/imports.test.ts
··· 82 82 test('dynamic import throws an error', async () => { 83 83 const path = './some-unknown-path' 84 84 const imported = import(path) 85 - await expect(imported).rejects.toThrowError(/Cannot find module '\.\/some-unknown-path' imported/) 85 + await expect(imported).rejects.toThrowError(`Cannot find module '/test/some-unknown-path' imported from '${resolve(import.meta.filename)}'`) 86 86 // @ts-expect-error path does not exist 87 - await expect(() => import('./some-unknown-path')).rejects.toThrowError(/Cannot find module/) 87 + await expect(() => import('./some-unknown-path')).rejects.toThrowError(`Cannot find module '/test/some-unknown-path' imported from '${resolve(import.meta.filename)}'`) 88 88 }) 89 89 90 90 test('can import @vite/client', async () => {
+9 -1
test/core/test/module.test.ts
··· 1 + import { external as viteEnvironmentExternal } from '@test/vite-environment-external' 2 + import { external as viteExternal } from '@test/vite-external' 3 + 1 4 import { describe, expect, it } from 'vitest' 2 5 3 6 // @ts-expect-error is not typed with imports ··· 37 40 38 41 // @ts-expect-error is not typed with imports 39 42 import * as nestedDefaultExternalCjs from '../src/external/nested-default-cjs' 40 - 41 43 import c, { d } from '../src/module-esm' 44 + 42 45 import * as timeout from '../src/timeout' 46 + 47 + it('extect vite.noExternal to be respected', () => { 48 + expect(viteExternal).toBe(false) 49 + expect(viteEnvironmentExternal).toBe(false) 50 + }) 43 51 44 52 it('doesn\'t when extending module', () => { 45 53 expect(() => Object.assign(globalThis, timeout)).not.toThrow()
-1
test/core/test/require.test.ts
··· 1 1 // @vitest-environment jsdom 2 2 3 - // import { KNOWN_ASSET_RE } from 'vite-node/constants' 4 3 import { describe, expect, it } from 'vitest' 5 4 6 5 const _require = require
+18 -13
test/core/test/utils.spec.ts
··· 1 - import type { EncodedSourceMap } from '../../../packages/vite-node/src/types' 2 1 import { assertTypes, deepClone, deepMerge, isNegativeNaN, objDisplay, objectAttr, toArray } from '@vitest/utils' 2 + import { EvaluatedModules } from 'vite/module-runner' 3 3 import { beforeAll, describe, expect, test } from 'vitest' 4 4 import { deepMergeSnapshot } from '../../../packages/snapshot/src/port/utils' 5 - import { ModuleCacheMap } from '../../../packages/vite-node/src/client' 6 5 import { resetModules } from '../../../packages/vitest/src/runtime/utils' 7 6 8 7 describe('assertTypes', () => { ··· 206 205 }) 207 206 208 207 describe('resetModules doesn\'t resets only user modules', () => { 209 - const mod = () => ({ evaluated: true, promise: Promise.resolve({}), resolving: false, exports: {}, map: {} as EncodedSourceMap }) 210 - 211 - const moduleCache = new ModuleCacheMap() 208 + const moduleCache = new EvaluatedModules() 212 209 const modules = [ 213 210 ['/some-module.ts', true], 214 211 ['/@fs/some-path.ts', true], ··· 222 219 223 220 beforeAll(() => { 224 221 modules.forEach(([path]) => { 225 - moduleCache.set(path, mod()) 222 + const exports = {} 223 + moduleCache.idToModuleMap.set(path, { 224 + id: path, 225 + url: path, 226 + file: path, 227 + importers: new Set(), 228 + imports: new Set(), 229 + evaluated: true, 230 + meta: undefined, 231 + exports, 232 + promise: Promise.resolve(exports), 233 + map: undefined, 234 + }) 226 235 }) 227 236 resetModules(moduleCache) 228 237 }) 229 238 230 239 test.each(modules)('Cache for %s is reset (%s)', (path, reset) => { 231 - const cached = moduleCache.get(path) 240 + const cached = moduleCache.idToModuleMap.get(path) 232 241 233 242 if (reset) { 234 - expect(cached).not.toHaveProperty('evaluated') 235 - expect(cached).not.toHaveProperty('resolving') 236 - expect(cached).not.toHaveProperty('exports') 237 - expect(cached).not.toHaveProperty('promise') 243 + expect(cached).toHaveProperty('exports', undefined) 244 + expect(cached).toHaveProperty('promise', undefined) 238 245 } 239 246 else { 240 - expect(cached).toHaveProperty('evaluated') 241 - expect(cached).toHaveProperty('resolving') 242 247 expect(cached).toHaveProperty('exports') 243 248 expect(cached).toHaveProperty('promise') 244 249 }
+1 -1
test/core/vitest-environment-custom/index.ts
··· 7 7 8 8 export default <Environment>{ 9 9 name: 'custom', 10 - transformMode: 'ssr', 10 + viteEnvironment: 'ssr', 11 11 setupVM({ custom }) { 12 12 const context = vm.createContext({ 13 13 testEnvironment: 'custom',
+1
test/core/vitest-environment-custom/package.json
··· 1 1 { 2 2 "name": "vitest-environment-custom", 3 + "type": "module", 3 4 "private": true, 4 5 "exports": { 5 6 ".": "./index.ts"
+2 -2
test/coverage-test/test/custom-provider.custom.test.ts
··· 19 19 "reportCoverage with {\\"allTestsRun\\":true}" 20 20 ], 21 21 "coverageReports": [ 22 - "{\\"coverage\\":{\\"customCoverage\\":\\"Coverage report passed from workers to main thread\\"},\\"testFiles\\":[\\"fixtures/test/even.test.ts\\"],\\"transformMode\\":\\"ssr\\",\\"projectName\\":\\"\\"}", 23 - "{\\"coverage\\":{\\"customCoverage\\":\\"Coverage report passed from workers to main thread\\"},\\"testFiles\\":[\\"fixtures/test/math.test.ts\\"],\\"transformMode\\":\\"ssr\\",\\"projectName\\":\\"\\"}" 22 + "{\\"coverage\\":{\\"customCoverage\\":\\"Coverage report passed from workers to main thread\\"},\\"testFiles\\":[\\"fixtures/test/even.test.ts\\"],\\"environment\\":\\"ssr\\",\\"projectName\\":\\"\\"}", 23 + "{\\"coverage\\":{\\"customCoverage\\":\\"Coverage report passed from workers to main thread\\"},\\"testFiles\\":[\\"fixtures/test/math.test.ts\\"],\\"environment\\":\\"ssr\\",\\"projectName\\":\\"\\"}" 24 24 ], 25 25 "transformedFiles": [ 26 26 "<process-cwd>/fixtures/src/even.ts",
+3 -3
test/reporters/tests/utils.test.ts
··· 1 - import type { ViteNodeRunner } from 'vite-node/client' 1 + import type { ModuleRunner } from 'vite/module-runner' 2 2 import type { Vitest } from 'vitest/node' 3 3 /** 4 4 * @format ··· 11 11 12 12 const customReporterPath = resolve(import.meta.dirname, '../src/custom-reporter.js') 13 13 const fetchModule = { 14 - executeId: (id: string) => import(id), 15 - } as ViteNodeRunner 14 + import: (id: string) => import(id), 15 + } as ModuleRunner 16 16 const ctx = { 17 17 runner: fetchModule, 18 18 } as Vitest
+5 -4
packages/browser/src/client/utils.ts
··· 1 1 import type { VitestRunner } from '@vitest/runner' 2 - import type { SerializedConfig, WorkerGlobalState } from 'vitest' 2 + import type { EvaluatedModules, SerializedConfig, WorkerGlobalState } from 'vitest' 3 3 import type { IframeOrchestrator } from './orchestrator' 4 4 import type { CommandsManager } from './tester/utils' 5 5 ··· 13 13 return getBrowserState().wrapModule(() => import(/* @vite-ignore */ name)) 14 14 } 15 15 16 - export const executor = { 16 + export const moduleRunner = { 17 17 isBrowser: true, 18 18 19 - executeId: (id: string): Promise<any> => { 19 + import: (id: string): Promise<any> => { 20 20 if (id[0] === '/' || id[1] === ':') { 21 21 return importFs(id) 22 22 } ··· 65 65 export interface BrowserRunnerState { 66 66 files: string[] 67 67 runningFiles: string[] 68 - moduleCache: Map<string, any> 68 + resolvingModules: Set<string> 69 + evaluatedModules: EvaluatedModules 69 70 config: SerializedConfig 70 71 provider: string 71 72 runner: VitestRunner
+1 -1
packages/browser/src/node/rpc.ts
··· 122 122 123 123 function setupClient(project: TestProject, rpcId: string, ws: WebSocket) { 124 124 const mockResolver = new ServerMockResolver(globalServer.vite, { 125 - moduleDirectories: project.config.server?.deps?.moduleDirectories, 125 + moduleDirectories: project.config?.deps?.moduleDirectories, 126 126 }) 127 127 const mocker = project.browser?.provider.mocker 128 128
+1 -1
packages/vitest/src/api/setup.ts
··· 94 94 const project = ctx.getProjectByName(projectName) 95 95 const result: TransformResultWithSource | null | undefined = browser 96 96 ? await project.browser!.vite.transformRequest(id) 97 - : await project.vitenode.transformRequest(id) 97 + : await project.vite.transformRequest(id) 98 98 if (result) { 99 99 try { 100 100 result.source = result.source || (await fs.readFile(id, 'utf-8'))
+12 -8
packages/vitest/src/integrations/vi.ts
··· 7 7 MockInstance, 8 8 } from '@vitest/spy' 9 9 import type { RuntimeOptions, SerializedConfig } from '../runtime/config' 10 - import type { VitestMocker } from '../runtime/mocker' 10 + import type { VitestMocker } from '../runtime/moduleRunner/moduleMocker' 11 11 import type { MockFactoryWithHelper, MockOptions } from '../types/mocker' 12 12 import { fn, isMockFunction, mocks, spyOn } from '@vitest/spy' 13 13 import { assertTypes, createSimpleStackTrace } from '@vitest/utils' ··· 687 687 }, 688 688 689 689 stubEnv(name: string, value: string | boolean | undefined) { 690 + const state = getWorkerState() 691 + const env = state.metaEnv 690 692 if (!_stubsEnv.has(name)) { 691 - _stubsEnv.set(name, process.env[name]) 693 + _stubsEnv.set(name, env[name]) 692 694 } 693 695 if (_envBooleans.includes(name)) { 694 - process.env[name] = value ? '1' : '' 696 + env[name] = value ? '1' : '' 695 697 } 696 698 else if (value === undefined) { 697 - delete process.env[name] 699 + delete env[name] 698 700 } 699 701 else { 700 - process.env[name] = String(value) 702 + env[name] = String(value) 701 703 } 702 704 return utils 703 705 }, ··· 716 718 }, 717 719 718 720 unstubAllEnvs() { 721 + const state = getWorkerState() 722 + const env = state.metaEnv 719 723 _stubsEnv.forEach((original, name) => { 720 724 if (original === undefined) { 721 - delete process.env[name] 725 + delete env[name] 722 726 } 723 727 else { 724 - process.env[name] = original 728 + env[name] = original 725 729 } 726 730 }) 727 731 _stubsEnv.clear() ··· 729 733 }, 730 734 731 735 resetModules() { 732 - resetModules(workerState.moduleCache as any) 736 + resetModules(workerState.evaluatedModules) 733 737 return utils 734 738 }, 735 739
+13 -18
packages/vitest/src/node/core.ts
··· 2 2 import type { Awaitable } from '@vitest/utils' 3 3 import type { Writable } from 'node:stream' 4 4 import type { ViteDevServer } from 'vite' 5 + import type { ModuleRunner } from 'vite/module-runner' 5 6 import type { SerializedCoverageConfig } from '../runtime/config' 6 7 import type { ArgumentsType, ProvidedContext, UserConsoleLog } from '../types/general' 7 8 import type { CliOptions } from './cli/cli-api' ··· 15 16 import { SnapshotManager } from '@vitest/snapshot/manager' 16 17 import { noop, toArray } from '@vitest/utils' 17 18 import { normalize, relative } from 'pathe' 18 - import { ViteNodeRunner } from 'vite-node/client' 19 - import { ViteNodeServer } from 'vite-node/server' 20 19 import { version } from '../../package.json' with { type: 'json' } 21 20 import { WebSocketReporter } from '../api/setup' 22 21 import { distDir } from '../paths' ··· 26 25 import { VitestCache } from './cache' 27 26 import { resolveConfig } from './config/resolveConfig' 28 27 import { getCoverageProvider } from './coverage' 28 + import { ServerModuleRunner } from './environments/serverRunner' 29 29 import { FilesNotFoundError } from './errors' 30 30 import { Logger } from './logger' 31 31 import { VitestPackageInstaller } from './packageInstaller' ··· 35 35 import { BlobReporter, readBlobs } from './reporters/blob' 36 36 import { HangingProcessReporter } from './reporters/hanging-process' 37 37 import { createBenchmarkReporters, createReporters } from './reporters/utils' 38 + import { VitestResolver } from './resolver' 38 39 import { VitestSpecifications } from './specifications' 39 40 import { StateManager } from './state' 40 41 import { TestRun } from './test-run' ··· 91 92 /** @internal */ _browserSessions = new BrowserSessions() 92 93 /** @internal */ _cliOptions: CliOptions = {} 93 94 /** @internal */ reporters: Reporter[] = [] 94 - /** @internal */ vitenode: ViteNodeServer = undefined! 95 - /** @internal */ runner: ViteNodeRunner = undefined! 95 + /** @internal */ runner!: ModuleRunner 96 96 /** @internal */ _testRun: TestRun = undefined! 97 + /** @internal */ _resolver!: VitestResolver 97 98 98 99 private isFirstRun = true 99 100 private restartsCount = 0 ··· 220 221 this.watcher.registerWatcher() 221 222 } 222 223 223 - this.vitenode = new ViteNodeServer(server, this.config.server) 224 - 225 - const node = this.vitenode 226 - this.runner = new ViteNodeRunner({ 227 - root: server.config.root, 228 - base: server.config.base, 229 - fetchModule(id: string) { 230 - return node.fetchModule(id) 231 - }, 232 - resolveId(id: string, importer?: string) { 233 - return node.resolveId(id, importer) 234 - }, 235 - }) 224 + this._resolver = new VitestResolver(server.config.cacheDir, resolved) 225 + const environment = server.environments.__vitest__ 226 + this.runner = new ServerModuleRunner( 227 + environment, 228 + this._resolver, 229 + resolved, 230 + ) 236 231 237 232 if (this.config.watch) { 238 233 // hijack server restart ··· 387 382 * @param moduleId The ID of the module in Vite module graph 388 383 */ 389 384 public import<T>(moduleId: string): Promise<T> { 390 - return this.runner.executeId(moduleId) 385 + return this.runner.import(moduleId) 391 386 } 392 387 393 388 private async resolveProjects(cliOptions: UserConfig): Promise<TestProject[]> {
+14 -17
packages/vitest/src/node/coverage.ts
··· 6 6 import type { AfterSuiteRunMeta } from '../types/general' 7 7 import { existsSync, promises as fs, readdirSync, writeFileSync } from 'node:fs' 8 8 import path from 'node:path' 9 + import { cleanUrl, slash } from '@vitest/utils' 9 10 import { relative, resolve } from 'pathe' 10 11 import pm from 'picomatch' 11 12 import { glob } from 'tinyglobby' 12 13 import c from 'tinyrainbow' 13 - import { cleanUrl, slash } from 'vite-node/utils' 14 14 import { coverageConfigDefaults } from '../defaults' 15 15 import { resolveCoverageReporters } from '../node/config/resolveConfig' 16 16 import { resolveCoverageProviderModule } from '../utils/coverage' ··· 42 42 type CoverageFiles = Map< 43 43 NonNullable<AfterSuiteRunMeta['projectName']> | symbol, 44 44 Record< 45 - AfterSuiteRunMeta['transformMode'], 45 + AfterSuiteRunMeta['environment'], 46 46 { [TestFilenames: string]: string } 47 47 > 48 48 > ··· 253 253 this.pendingPromises = [] 254 254 } 255 255 256 - onAfterSuiteRun({ coverage, transformMode, projectName, testFiles }: AfterSuiteRunMeta): void { 256 + onAfterSuiteRun({ coverage, environment, projectName, testFiles }: AfterSuiteRunMeta): void { 257 257 if (!coverage) { 258 258 return 259 - } 260 - 261 - if (transformMode !== 'web' && transformMode !== 'ssr' && transformMode !== 'browser') { 262 - throw new Error(`Invalid transform mode: ${transformMode}`) 263 259 } 264 260 265 261 let entry = this.coverageFiles.get(projectName || DEFAULT_PROJECT) 266 262 267 263 if (!entry) { 268 - entry = { web: {}, ssr: {}, browser: {} } 264 + entry = {} 269 265 this.coverageFiles.set(projectName || DEFAULT_PROJECT, entry) 270 266 } 271 267 ··· 275 271 `coverage-${uniqueId++}.json`, 276 272 ) 277 273 274 + entry[environment] ??= {} 278 275 // If there's a result from previous run, overwrite it 279 - entry[transformMode][testFilenames] = filename 276 + entry[environment][testFilenames] = filename 280 277 281 278 const promise = fs.writeFile(filename, JSON.stringify(coverage), 'utf-8') 282 279 this.pendingPromises.push(promise) ··· 286 283 /** Callback invoked with a single coverage result */ 287 284 onFileRead: (data: CoverageType) => void 288 285 /** Callback invoked once all results of a project for specific transform mode are read */ 289 - onFinished: (project: Vitest['projects'][number], transformMode: AfterSuiteRunMeta['transformMode']) => Promise<void> 286 + onFinished: (project: Vitest['projects'][number], environment: string) => Promise<void> 290 287 onDebug: ((...logs: any[]) => void) & { enabled: boolean } 291 288 }): Promise<void> { 292 289 let index = 0 ··· 296 293 this.pendingPromises = [] 297 294 298 295 for (const [projectName, coveragePerProject] of this.coverageFiles.entries()) { 299 - for (const [transformMode, coverageByTestfiles] of Object.entries(coveragePerProject) as Entries<typeof coveragePerProject>) { 296 + for (const [environment, coverageByTestfiles] of Object.entries(coveragePerProject) as Entries<typeof coveragePerProject>) { 300 297 const filenames = Object.values(coverageByTestfiles) 301 298 const project = this.ctx.getProjectByName(projectName as string) 302 299 ··· 315 312 ) 316 313 } 317 314 318 - await onFinished(project, transformMode) 315 + await onFinished(project, environment) 319 316 } 320 317 } 321 318 } ··· 640 637 ...ctx.projects.map(project => ({ 641 638 root: project.config.root, 642 639 isBrowserEnabled: project.isBrowserEnabled(), 643 - vitenode: project.vitenode, 640 + vite: project.vite, 644 641 })), 645 642 // Check core last as it will match all files anyway 646 - { root: ctx.config.root, vitenode: ctx.vitenode, isBrowserEnabled: ctx.getRootProject().isBrowserEnabled() }, 643 + { root: ctx.config.root, vite: ctx.vite, isBrowserEnabled: ctx.getRootProject().isBrowserEnabled() }, 647 644 ] 648 645 649 646 return async function transformFile(filename: string): Promise<TransformResult | null | undefined> { 650 647 let lastError 651 648 652 - for (const { root, vitenode, isBrowserEnabled } of servers) { 649 + for (const { root, vite, isBrowserEnabled } of servers) { 653 650 // On Windows root doesn't start with "/" while filenames do 654 651 if (!filename.startsWith(root) && !filename.startsWith(`/${root}`)) { 655 652 continue 656 653 } 657 654 658 655 if (isBrowserEnabled) { 659 - const result = await vitenode.transformRequest(filename, undefined, 'web').catch(() => null) 656 + const result = await vite.environments.client.transformRequest(filename).catch(() => null) 660 657 661 658 if (result) { 662 659 return result ··· 664 661 } 665 662 666 663 try { 667 - return await vitenode.transformRequest(filename) 664 + return await vite.environments.ssr.transformRequest(filename) 668 665 } 669 666 catch (error) { 670 667 lastError = error
+4 -4
packages/vitest/src/node/globalSetup.ts
··· 1 - import type { ViteNodeRunner } from 'vite-node/client' 1 + import type { ModuleRunner } from 'vite/module-runner' 2 2 import type { TestProject } from './project' 3 3 import { toArray } from '@vitest/utils' 4 4 ··· 9 9 } 10 10 11 11 export async function loadGlobalSetupFiles( 12 - runner: ViteNodeRunner, 12 + runner: ModuleRunner, 13 13 globalSetup: string | string[], 14 14 ): Promise<GlobalSetupFile[]> { 15 15 const globalSetupFiles = toArray(globalSetup) ··· 20 20 21 21 async function loadGlobalSetupFile( 22 22 file: string, 23 - runner: ViteNodeRunner, 23 + runner: ModuleRunner, 24 24 ): Promise<GlobalSetupFile> { 25 - const m = await runner.executeFile(file) 25 + const m = await runner.import(file) 26 26 for (const exp of ['default', 'setup', 'teardown']) { 27 27 if (m[exp] != null && typeof m[exp] !== 'function') { 28 28 throw new Error(
+1 -1
packages/vitest/src/node/pool.ts
··· 144 144 return customPools.get(filepath)! 145 145 } 146 146 147 - const pool = await ctx.runner.executeId(filepath) 147 + const pool = await ctx.runner.import(filepath) 148 148 if (typeof pool.default !== 'function') { 149 149 throw new TypeError( 150 150 `Custom pool "${filepath}" must export a function as default export`,
+9 -3
packages/vitest/src/node/printError.ts
··· 129 129 ? error.stacks[0] 130 130 : stacks.find((stack) => { 131 131 try { 132 + const module = project._vite && project.getModuleById(stack.file) 132 133 return ( 133 - project._vite 134 - && project.getModuleById(stack.file) 134 + (module?.transformResult || module?.ssrTransformResult) 135 135 && existsSync(stack.file) 136 136 ) 137 137 } ··· 261 261 'actual', 262 262 'expected', 263 263 'diffOptions', 264 + 'runnerError', 264 265 // webkit props 265 266 'sourceURL', 266 267 'column', ··· 448 449 } 449 450 450 451 const lineLength = lines[j].length 452 + const strippedContent = stripVTControlCharacters(lines[j]) 453 + 454 + if (strippedContent.startsWith('//# sourceMappingURL')) { 455 + continue 456 + } 451 457 452 458 // too long, maybe it's a minified file, skip for codeframe 453 - if (stripVTControlCharacters(lines[j]).length > 200) { 459 + if (strippedContent.length > 200) { 454 460 return '' 455 461 } 456 462
+18 -25
packages/vitest/src/node/project.ts
··· 1 1 import type { GlobOptions } from 'tinyglobby' 2 - import type { 3 - ModuleNode, 4 - TransformResult, 5 - ViteDevServer, 6 - InlineConfig as ViteInlineConfig, 7 - } from 'vite' 2 + import type { ModuleNode, TransformResult, ViteDevServer, InlineConfig as ViteInlineConfig } from 'vite' 3 + import type { ModuleRunner } from 'vite/module-runner' 8 4 import type { Typechecker } from '../typecheck/typechecker' 9 5 import type { ProvidedContext } from '../types/general' 10 6 import type { OnTestsRerunHandler, Vitest } from './core' ··· 28 24 import { isAbsolute, join, relative } from 'pathe' 29 25 import pm from 'picomatch' 30 26 import { glob } from 'tinyglobby' 31 - import { ViteNodeRunner } from 'vite-node/client' 32 - import { ViteNodeServer } from 'vite-node/server' 33 27 import { setup } from '../api/setup' 34 28 import { isBrowserEnabled, resolveConfig } from './config/resolveConfig' 35 29 import { serializeConfig } from './config/serializeConfig' 30 + import { ServerModuleRunner } from './environments/serverRunner' 36 31 import { loadGlobalSetupFiles } from './globalSetup' 37 32 import { CoverageTransform } from './plugins/coverageTransform' 33 + import { MetaEnvReplacerPlugin } from './plugins/metaEnvReplacer' 38 34 import { MocksPlugins } from './plugins/mocks' 39 35 import { WorkspaceVitestPlugin } from './plugins/workspace' 40 36 import { getFilePoolName } from './pool' 37 + import { VitestResolver } from './resolver' 41 38 import { TestSpecification } from './spec' 42 39 import { createViteServer } from './vite' 43 40 ··· 66 63 */ 67 64 public readonly tmpDir: string = join(tmpdir(), nanoid()) 68 65 69 - /** @internal */ vitenode!: ViteNodeServer 70 66 /** @internal */ typechecker?: Typechecker 71 67 /** @internal */ _config?: ResolvedConfig 72 68 /** @internal */ _vite?: ViteDevServer 73 69 /** @internal */ _hash?: string 70 + /** @internal */ _resolver!: VitestResolver 74 71 75 - private runner!: ViteNodeRunner 72 + private runner!: ModuleRunner 76 73 77 74 private closingPromise: Promise<void> | undefined 78 75 ··· 556 553 return true 557 554 }, 558 555 }), 556 + MetaEnvReplacerPlugin(), 559 557 ], 560 558 [CoverageTransform(this.vitest)], 561 559 ) ··· 601 599 * @param moduleId The ID of the module in Vite module graph 602 600 */ 603 601 public import<T>(moduleId: string): Promise<T> { 604 - return this.runner.executeId(moduleId) 602 + return this.runner.import(moduleId) 605 603 } 606 604 607 605 /** @deprecated use `name` instead */ ··· 642 640 643 641 this.closingPromise = undefined 644 642 643 + this._resolver = new VitestResolver(server.config.cacheDir, this._config) 645 644 this._vite = server 646 645 647 - this.vitenode = new ViteNodeServer(server, this.config.server) 648 - const node = this.vitenode 649 - this.runner = new ViteNodeRunner({ 650 - root: server.config.root, 651 - base: server.config.base, 652 - fetchModule(id: string) { 653 - return node.fetchModule(id) 654 - }, 655 - resolveId(id: string, importer?: string) { 656 - return node.resolveId(id, importer) 657 - }, 658 - }) 646 + const environment = server.environments.__vitest__ 647 + this.runner = new ServerModuleRunner( 648 + environment, 649 + this._resolver, 650 + this._config, 651 + ) 659 652 } 660 653 661 654 private _serializeOverriddenConfig(): SerializedConfig { ··· 715 708 vitest.config.name || vitest.config.root, 716 709 vitest, 717 710 ) 718 - project.vitenode = vitest.vitenode 719 711 project.runner = vitest.runner 720 712 project._vite = vitest.server 721 713 project._config = vitest.config 714 + project._resolver = vitest._resolver 722 715 project._setHash() 723 716 project._provideObject(vitest.config.provide) 724 717 return project ··· 730 723 parent.path, 731 724 parent.vitest, 732 725 ) 733 - clone.vitenode = parent.vitenode 734 726 clone.runner = parent.runner 735 727 clone._vite = parent._vite 728 + clone._resolver = parent._resolver 736 729 clone._config = config 737 730 clone._setHash() 738 731 clone._parent = parent
+222
packages/vitest/src/node/resolver.ts
··· 1 + import type { ResolvedConfig, ServerDepsOptions } from './types/config' 2 + import { existsSync, promises as fsp } from 'node:fs' 3 + import { isBuiltin } from 'node:module' 4 + import { pathToFileURL } from 'node:url' 5 + import { KNOWN_ASSET_RE } from '@vitest/utils' 6 + import { findNearestPackageData } from '@vitest/utils/resolver' 7 + import * as esModuleLexer from 'es-module-lexer' 8 + import { dirname, extname, join, resolve } from 'pathe' 9 + import { isWindows } from '../utils/env' 10 + 11 + export class VitestResolver { 12 + private options: ExternalizeOptions 13 + private externalizeCache = new Map<string, Promise<string | false>>() 14 + 15 + constructor(cacheDir: string, config: ResolvedConfig) { 16 + this.options = { 17 + moduleDirectories: config.deps.moduleDirectories, 18 + inlineFiles: config.setupFiles.flatMap((file) => { 19 + if (file.startsWith('file://')) { 20 + return file 21 + } 22 + const resolvedId = resolve(file) 23 + return [resolvedId, pathToFileURL(resolvedId).href] 24 + }), 25 + cacheDir, 26 + inline: config.server.deps?.inline, 27 + external: config.server.deps?.external, 28 + } 29 + } 30 + 31 + public shouldExternalize(file: string): Promise<string | false> { 32 + return shouldExternalize(normalizeId(file), this.options, this.externalizeCache) 33 + } 34 + } 35 + 36 + function normalizeId(id: string) { 37 + if (id.startsWith('/@fs/')) { 38 + id = id.slice(isWindows ? 5 : 4) 39 + } 40 + return id 41 + } 42 + 43 + interface ExternalizeOptions extends ServerDepsOptions { 44 + moduleDirectories?: string[] 45 + inlineFiles?: string[] 46 + cacheDir?: string 47 + } 48 + 49 + const BUILTIN_EXTENSIONS = new Set(['.mjs', '.cjs', '.node', '.wasm']) 50 + 51 + const ESM_EXT_RE = /\.(es|esm|esm-browser|esm-bundler|es6|module)\.js$/ 52 + const ESM_FOLDER_RE = /\/(es|esm)\/(.*\.js)$/ 53 + 54 + const defaultInline = [ 55 + /virtual:/, 56 + /\.[mc]?ts$/, 57 + 58 + // special Vite query strings 59 + /[?&](init|raw|url|inline)\b/, 60 + // Vite returns a string for assets imports, even if it's inside "node_modules" 61 + KNOWN_ASSET_RE, 62 + 63 + /^(?!.*node_modules).*\.mjs$/, 64 + /^(?!.*node_modules).*\.cjs\.js$/, 65 + // Vite client 66 + /vite\w*\/dist\/client\/env.mjs/, 67 + ] 68 + 69 + const depsExternal = [ 70 + /\/node_modules\/.*\.cjs\.js$/, 71 + /\/node_modules\/.*\.mjs$/, 72 + ] 73 + 74 + export function guessCJSversion(id: string): string | undefined { 75 + if (id.match(ESM_EXT_RE)) { 76 + for (const i of [ 77 + id.replace(ESM_EXT_RE, '.mjs'), 78 + id.replace(ESM_EXT_RE, '.umd.js'), 79 + id.replace(ESM_EXT_RE, '.cjs.js'), 80 + id.replace(ESM_EXT_RE, '.js'), 81 + ]) { 82 + if (existsSync(i)) { 83 + return i 84 + } 85 + } 86 + } 87 + if (id.match(ESM_FOLDER_RE)) { 88 + for (const i of [ 89 + id.replace(ESM_FOLDER_RE, '/umd/$1'), 90 + id.replace(ESM_FOLDER_RE, '/cjs/$1'), 91 + id.replace(ESM_FOLDER_RE, '/lib/$1'), 92 + id.replace(ESM_FOLDER_RE, '/$1'), 93 + ]) { 94 + if (existsSync(i)) { 95 + return i 96 + } 97 + } 98 + } 99 + } 100 + 101 + // The code from https://github.com/unjs/mlly/blob/c5bcca0cda175921344fd6de1bc0c499e73e5dac/src/syntax.ts#L51-L98 102 + async function isValidNodeImport(id: string) { 103 + const extension = extname(id) 104 + 105 + if (BUILTIN_EXTENSIONS.has(extension)) { 106 + return true 107 + } 108 + 109 + if (extension !== '.js') { 110 + return false 111 + } 112 + 113 + id = id.replace('file:///', '') 114 + 115 + const package_ = findNearestPackageData(dirname(id)) 116 + 117 + if (package_.type === 'module') { 118 + return true 119 + } 120 + 121 + if (/\.(?:\w+-)?esm?(?:-\w+)?\.js$|\/esm?\//.test(id)) { 122 + return false 123 + } 124 + 125 + try { 126 + await esModuleLexer.init 127 + const code = await fsp.readFile(id, 'utf8') 128 + const [, , , hasModuleSyntax] = esModuleLexer.parse(code) 129 + return !hasModuleSyntax 130 + } 131 + catch { 132 + return false 133 + } 134 + } 135 + 136 + export async function shouldExternalize( 137 + id: string, 138 + options: ExternalizeOptions, 139 + cache: Map<string, Promise<string | false>>, 140 + ): Promise<string | false> { 141 + if (!cache.has(id)) { 142 + cache.set(id, _shouldExternalize(id, options)) 143 + } 144 + return cache.get(id)! 145 + } 146 + 147 + async function _shouldExternalize( 148 + id: string, 149 + options?: ExternalizeOptions, 150 + ): Promise<string | false> { 151 + if (isBuiltin(id)) { 152 + return id 153 + } 154 + 155 + // data: should be processed by native import, 156 + // since it is a feature of ESM. 157 + // also externalize network imports since nodejs allows it when --experimental-network-imports 158 + if (id.startsWith('data:') || /^(?:https?:)?\/\//.test(id)) { 159 + return id 160 + } 161 + 162 + const moduleDirectories = options?.moduleDirectories || ['/node_modules/'] 163 + 164 + if (matchExternalizePattern(id, moduleDirectories, options?.inline)) { 165 + return false 166 + } 167 + if (options?.inlineFiles && options?.inlineFiles.includes(id)) { 168 + return false 169 + } 170 + if (matchExternalizePattern(id, moduleDirectories, options?.external)) { 171 + return id 172 + } 173 + 174 + // Unless the user explicitly opted to inline them, externalize Vite deps. 175 + // They are too big to inline by default. 176 + if (options?.cacheDir && id.includes(options.cacheDir)) { 177 + return id 178 + } 179 + 180 + const isLibraryModule = moduleDirectories.some(dir => id.includes(dir)) 181 + const guessCJS = isLibraryModule && options?.fallbackCJS 182 + id = guessCJS ? guessCJSversion(id) || id : id 183 + 184 + if (matchExternalizePattern(id, moduleDirectories, defaultInline)) { 185 + return false 186 + } 187 + if (matchExternalizePattern(id, moduleDirectories, depsExternal)) { 188 + return id 189 + } 190 + 191 + if (isLibraryModule && (await isValidNodeImport(id))) { 192 + return id 193 + } 194 + 195 + return false 196 + } 197 + 198 + function matchExternalizePattern( 199 + id: string, 200 + moduleDirectories: string[], 201 + patterns?: (string | RegExp)[] | true, 202 + ) { 203 + if (patterns == null) { 204 + return false 205 + } 206 + if (patterns === true) { 207 + return true 208 + } 209 + for (const ex of patterns) { 210 + if (typeof ex === 'string') { 211 + if (moduleDirectories.some(dir => id.includes(join(dir, ex)))) { 212 + return true 213 + } 214 + } 215 + else { 216 + if (ex.test(id)) { 217 + return true 218 + } 219 + } 220 + } 221 + return false 222 + }
+2 -2
packages/vitest/src/node/specifications.ts
··· 176 176 } 177 177 deps.add(filepath) 178 178 179 - const mod = project.vite.moduleGraph.getModuleById(filepath) 180 - const transformed = mod?.ssrTransformResult || await project.vitenode.transformRequest(filepath) 179 + const mod = project.vite.environments.ssr.moduleGraph.getModuleById(filepath) 180 + const transformed = mod?.transformResult || await project.vite.environments.ssr.transformRequest(filepath) 181 181 if (!transformed) { 182 182 return 183 183 }
+1
packages/vitest/src/node/state.ts
··· 24 24 processTimeoutCauses: Set<string> = new Set() 25 25 reportedTasksMap: WeakMap<Task, TestModule | TestCase | TestSuite> = new WeakMap() 26 26 blobs?: MergedBlobs 27 + transformTime = 0 27 28 28 29 onUnhandledError?: OnUnhandledErrorCallback 29 30
-1
packages/vitest/src/public/config.ts
··· 10 10 } from '../node/types/config' 11 11 import '../node/types/vite' 12 12 13 - export { extraInlineDeps } from '../constants' 14 13 // will import vitest declare test in module 'vite' 15 14 export { 16 15 configDefaults,
-1
packages/vitest/src/public/execute.ts
··· 1 - export { VitestExecutor } from '../runtime/execute'
+2
packages/vitest/src/public/index.ts
··· 136 136 export type { SerializedError } from '@vitest/utils' 137 137 export type { SerializedTestSpecification } 138 138 export type { DiffOptions } from '@vitest/utils/diff' 139 + 140 + export { EvaluatedModules } from 'vite/module-runner'
+14
packages/vitest/src/public/module-runner.ts
··· 1 + export { 2 + VitestModuleEvaluator, 3 + type VitestModuleEvaluatorOptions, 4 + } from '../runtime/moduleRunner/moduleEvaluator' 5 + export { 6 + VitestModuleRunner, 7 + type VitestModuleRunnerOptions, 8 + } from '../runtime/moduleRunner/moduleRunner' 9 + export { 10 + type ContextModuleRunnerOptions, 11 + startVitestModuleRunner, 12 + VITEST_VM_CONTEXT_SYMBOL, 13 + } from '../runtime/moduleRunner/startModuleRunner' 14 + export { getWorkerState } from '../runtime/utils'
-1
packages/vitest/src/public/node.ts
··· 98 98 SequenceHooks, 99 99 SequenceSetupFiles, 100 100 UserConfig as TestUserConfig, 101 - TransformModePatterns, 102 101 TypecheckConfig, 103 102 UserWorkspaceConfig, 104 103 VitestEnvironment,
+1 -8
packages/vitest/src/runtime/config.ts
··· 71 71 transformCss?: boolean 72 72 transformGlobPattern?: RegExp | RegExp[] 73 73 } 74 - optimizer: { 75 - web: { 76 - enabled: boolean 77 - } 78 - ssr: { 79 - enabled: boolean 80 - } 81 - } 74 + optimizer: Record<string, { enabled: boolean }> 82 75 interopDefault: boolean | undefined 83 76 moduleDirectories: string[] | undefined 84 77 }
-404
packages/vitest/src/runtime/execute.ts
··· 1 - import type { ViteNodeRunnerOptions } from 'vite-node' 2 - import type { ModuleCacheMap, ModuleExecutionInfo } from 'vite-node/client' 3 - import type { WorkerGlobalState } from '../types/worker' 4 - import type { ExternalModulesExecutor } from './external-executor' 5 - import fs from 'node:fs' 6 - import { pathToFileURL } from 'node:url' 7 - import vm from 'node:vm' 8 - import { processError } from '@vitest/utils/error' 9 - import { normalize } from 'pathe' 10 - import { DEFAULT_REQUEST_STUBS, ViteNodeRunner } from 'vite-node/client' 11 - import { 12 - isInternalRequest, 13 - isNodeBuiltin, 14 - isPrimitive, 15 - toFilePath, 16 - } from 'vite-node/utils' 17 - import { distDir } from '../paths' 18 - import { VitestMocker } from './mocker' 19 - 20 - const normalizedDistDir = normalize(distDir) 21 - 22 - const { readFileSync } = fs 23 - 24 - export interface ExecuteOptions extends ViteNodeRunnerOptions { 25 - moduleDirectories?: string[] 26 - state: WorkerGlobalState 27 - context?: vm.Context 28 - externalModulesExecutor?: ExternalModulesExecutor 29 - } 30 - 31 - export async function createVitestExecutor(options: ExecuteOptions): Promise<VitestExecutor> { 32 - const runner = new VitestExecutor(options) 33 - 34 - await runner.executeId('/@vite/env') 35 - await runner.mocker.initializeSpyModule() 36 - 37 - return runner 38 - } 39 - 40 - const externalizeMap = new Map<string, string>() 41 - 42 - export interface ContextExecutorOptions { 43 - moduleCache?: ModuleCacheMap 44 - context?: vm.Context 45 - externalModulesExecutor?: ExternalModulesExecutor 46 - state: WorkerGlobalState 47 - requestStubs: Record<string, any> 48 - } 49 - 50 - const bareVitestRegexp = /^@?vitest(?:\/|$)/ 51 - 52 - const dispose: (() => void)[] = [] 53 - 54 - function listenForErrors(state: () => WorkerGlobalState) { 55 - dispose.forEach(fn => fn()) 56 - dispose.length = 0 57 - 58 - function catchError(err: unknown, type: string, event: 'uncaughtException' | 'unhandledRejection') { 59 - const worker = state() 60 - 61 - const listeners = process.listeners(event as 'uncaughtException') 62 - // if there is another listener, assume that it's handled by user code 63 - // one is Vitest's own listener 64 - if (listeners.length > 1) { 65 - return 66 - } 67 - 68 - const error = processError(err) 69 - if (!isPrimitive(error)) { 70 - error.VITEST_TEST_NAME = worker.current?.type === 'test' ? worker.current.name : undefined 71 - if (worker.filepath) { 72 - error.VITEST_TEST_PATH = worker.filepath 73 - } 74 - error.VITEST_AFTER_ENV_TEARDOWN = worker.environmentTeardownRun 75 - } 76 - state().rpc.onUnhandledError(error, type) 77 - } 78 - 79 - const uncaughtException = (e: Error) => catchError(e, 'Uncaught Exception', 'uncaughtException') 80 - const unhandledRejection = (e: Error) => catchError(e, 'Unhandled Rejection', 'unhandledRejection') 81 - 82 - process.on('uncaughtException', uncaughtException) 83 - process.on('unhandledRejection', unhandledRejection) 84 - 85 - dispose.push(() => { 86 - process.off('uncaughtException', uncaughtException) 87 - process.off('unhandledRejection', unhandledRejection) 88 - }) 89 - } 90 - 91 - const relativeIds: Record<string, string> = {} 92 - 93 - function getVitestImport(id: string, state: () => WorkerGlobalState) { 94 - if (externalizeMap.has(id)) { 95 - return { externalize: externalizeMap.get(id)! } 96 - } 97 - // always externalize Vitest because we import from there before running tests 98 - // so we already have it cached by Node.js 99 - const root = state().config.root 100 - const relativeRoot = relativeIds[root] ?? (relativeIds[root] = normalizedDistDir.slice(root.length)) 101 - if ( 102 - // full dist path 103 - id.includes(distDir) 104 - || id.includes(normalizedDistDir) 105 - // "relative" to root path: 106 - // /node_modules/.pnpm/vitest/dist 107 - || (relativeRoot && relativeRoot !== '/' && id.startsWith(relativeRoot)) 108 - ) { 109 - const { path } = toFilePath(id, root) 110 - const externalize = pathToFileURL(path).toString() 111 - externalizeMap.set(id, externalize) 112 - return { externalize } 113 - } 114 - if (bareVitestRegexp.test(id)) { 115 - externalizeMap.set(id, id) 116 - return { externalize: id } 117 - } 118 - return null 119 - } 120 - 121 - export async function startVitestExecutor(options: ContextExecutorOptions): Promise<VitestExecutor> { 122 - const state = (): WorkerGlobalState => 123 - // @ts-expect-error injected untyped global 124 - globalThis.__vitest_worker__ || options.state 125 - const rpc = () => state().rpc 126 - 127 - process.exit = (code = process.exitCode || 0): never => { 128 - throw new Error(`process.exit unexpectedly called with "${code}"`) 129 - } 130 - 131 - listenForErrors(state) 132 - 133 - const getTransformMode = () => { 134 - return state().environment.transformMode ?? 'ssr' 135 - } 136 - 137 - return await createVitestExecutor({ 138 - async fetchModule(id) { 139 - const vitest = getVitestImport(id, state) 140 - if (vitest) { 141 - return vitest 142 - } 143 - 144 - const result = await rpc().fetch(id, getTransformMode()) 145 - if (result.id && !result.externalize) { 146 - const code = readFileSync(result.id, 'utf-8') 147 - return { code } 148 - } 149 - return result 150 - }, 151 - resolveId(id, importer) { 152 - return rpc().resolveId(id, importer, getTransformMode()) 153 - }, 154 - get moduleCache() { 155 - return state().moduleCache as ModuleCacheMap 156 - }, 157 - get moduleExecutionInfo() { 158 - return state().moduleExecutionInfo 159 - }, 160 - get interopDefault() { 161 - return state().config.deps.interopDefault 162 - }, 163 - get moduleDirectories() { 164 - return state().config.deps.moduleDirectories 165 - }, 166 - get root() { 167 - return state().config.root 168 - }, 169 - get base() { 170 - return state().config.base 171 - }, 172 - ...options, 173 - }) 174 - } 175 - 176 - function updateStyle(id: string, css: string) { 177 - if (typeof document === 'undefined') { 178 - return 179 - } 180 - 181 - const element = document.querySelector(`[data-vite-dev-id="${id}"]`) 182 - if (element) { 183 - element.textContent = css 184 - return 185 - } 186 - 187 - const head = document.querySelector('head') 188 - const style = document.createElement('style') 189 - style.setAttribute('type', 'text/css') 190 - style.setAttribute('data-vite-dev-id', id) 191 - style.textContent = css 192 - head?.appendChild(style) 193 - } 194 - 195 - function removeStyle(id: string) { 196 - if (typeof document === 'undefined') { 197 - return 198 - } 199 - const sheet = document.querySelector(`[data-vite-dev-id="${id}"]`) 200 - if (sheet) { 201 - document.head.removeChild(sheet) 202 - } 203 - } 204 - 205 - export function getDefaultRequestStubs(context?: vm.Context): { 206 - '/@vite/client': any 207 - '@vite/client': any 208 - } { 209 - if (!context) { 210 - const clientStub = { 211 - ...DEFAULT_REQUEST_STUBS['@vite/client'], 212 - updateStyle, 213 - removeStyle, 214 - } 215 - return { 216 - '/@vite/client': clientStub, 217 - '@vite/client': clientStub, 218 - } 219 - } 220 - const clientStub = vm.runInContext( 221 - `(defaultClient) => ({ ...defaultClient, updateStyle: ${updateStyle.toString()}, removeStyle: ${removeStyle.toString()} })`, 222 - context, 223 - )(DEFAULT_REQUEST_STUBS['@vite/client']) 224 - return { 225 - '/@vite/client': clientStub, 226 - '@vite/client': clientStub, 227 - } 228 - } 229 - 230 - export class VitestExecutor extends ViteNodeRunner { 231 - public mocker: VitestMocker 232 - public externalModules?: ExternalModulesExecutor 233 - 234 - private primitives: { 235 - Object: typeof Object 236 - Reflect: typeof Reflect 237 - Symbol: typeof Symbol 238 - } 239 - 240 - constructor(public options: ExecuteOptions) { 241 - super({ 242 - ...options, 243 - // interop is done inside the external executor instead 244 - interopDefault: options.context ? false : options.interopDefault, 245 - }) 246 - 247 - this.mocker = new VitestMocker(this) 248 - 249 - if (!options.context) { 250 - Object.defineProperty(globalThis, '__vitest_mocker__', { 251 - value: this.mocker, 252 - writable: true, 253 - configurable: true, 254 - }) 255 - this.primitives = { Object, Reflect, Symbol } 256 - } 257 - else if (options.externalModulesExecutor) { 258 - this.primitives = vm.runInContext( 259 - '({ Object, Reflect, Symbol })', 260 - options.context, 261 - ) 262 - this.externalModules = options.externalModulesExecutor 263 - } 264 - else { 265 - throw new Error( 266 - 'When context is provided, externalModulesExecutor must be provided as well.', 267 - ) 268 - } 269 - } 270 - 271 - protected getContextPrimitives(): { 272 - Object: typeof Object 273 - Reflect: typeof Reflect 274 - Symbol: typeof Symbol 275 - } { 276 - return this.primitives 277 - } 278 - 279 - get state(): WorkerGlobalState { 280 - // @ts-expect-error injected untyped global 281 - return globalThis.__vitest_worker__ || this.options.state 282 - } 283 - 284 - get moduleExecutionInfo(): ModuleExecutionInfo | undefined { 285 - return this.options.moduleExecutionInfo 286 - } 287 - 288 - shouldResolveId(id: string, _importee?: string | undefined): boolean { 289 - if (isInternalRequest(id) || id.startsWith('data:')) { 290 - return false 291 - } 292 - const transformMode = this.state.environment?.transformMode ?? 'ssr' 293 - // do not try and resolve node builtins in Node 294 - // import('url') returns Node internal even if 'url' package is installed 295 - return transformMode === 'ssr' 296 - ? !isNodeBuiltin(id) 297 - : !id.startsWith('node:') 298 - } 299 - 300 - async originalResolveUrl(id: string, importer?: string): Promise<[url: string, fsPath: string]> { 301 - return super.resolveUrl(id, importer) 302 - } 303 - 304 - async resolveUrl(id: string, importer?: string): Promise<[url: string, fsPath: string]> { 305 - if (VitestMocker.pendingIds.length) { 306 - await this.mocker.resolveMocks() 307 - } 308 - 309 - if (importer && importer.startsWith('mock:')) { 310 - importer = importer.slice(5) 311 - } 312 - try { 313 - return await super.resolveUrl(id, importer) 314 - } 315 - catch (error: any) { 316 - if (error.code === 'ERR_MODULE_NOT_FOUND') { 317 - const { id } = error[Symbol.for('vitest.error.not_found.data')] 318 - const path = this.mocker.normalizePath(id) 319 - const mock = this.mocker.getDependencyMock(path) 320 - if (mock !== undefined) { 321 - return [id, id] as [string, string] 322 - } 323 - } 324 - throw error 325 - } 326 - } 327 - 328 - protected async runModule(context: Record<string, any>, transformed: string): Promise<void> { 329 - const vmContext = this.options.context 330 - 331 - if (!vmContext || !this.externalModules) { 332 - return super.runModule(context, transformed) 333 - } 334 - 335 - // add 'use strict' since ESM enables it by default 336 - const codeDefinition = `'use strict';async (${Object.keys(context).join( 337 - ',', 338 - )})=>{{` 339 - const code = `${codeDefinition}${transformed}\n}}` 340 - const options = { 341 - filename: context.__filename, 342 - lineOffset: 0, 343 - columnOffset: -codeDefinition.length, 344 - } 345 - 346 - const finishModuleExecutionInfo = this.startCalculateModuleExecutionInfo(options.filename, codeDefinition.length) 347 - 348 - try { 349 - const fn = vm.runInContext(code, vmContext, { 350 - ...options, 351 - // if we encountered an import, it's not inlined 352 - importModuleDynamically: this.externalModules 353 - .importModuleDynamically as any, 354 - } as any) 355 - await fn(...Object.values(context)) 356 - } 357 - finally { 358 - this.options.moduleExecutionInfo?.set(options.filename, finishModuleExecutionInfo()) 359 - } 360 - } 361 - 362 - public async importExternalModule(path: string): Promise<any> { 363 - if (this.externalModules) { 364 - return this.externalModules.import(path) 365 - } 366 - return super.importExternalModule(path) 367 - } 368 - 369 - async dependencyRequest( 370 - id: string, 371 - fsPath: string, 372 - callstack: string[], 373 - ): Promise<any> { 374 - const mocked = await this.mocker.requestWithMock(fsPath, callstack) 375 - 376 - if (typeof mocked === 'string') { 377 - return super.dependencyRequest(mocked, mocked, callstack) 378 - } 379 - if (mocked && typeof mocked === 'object') { 380 - return mocked 381 - } 382 - return super.dependencyRequest(id, fsPath, callstack) 383 - } 384 - 385 - prepareContext(context: Record<string, any>): Record<string, any> { 386 - // support `import.meta.vitest` for test entry 387 - if ( 388 - this.state.filepath 389 - && normalize(this.state.filepath) === normalize(context.__filename) 390 - ) { 391 - const globalNamespace = this.options.context || globalThis 392 - Object.defineProperty(context.__vite_ssr_import_meta__, 'vitest', { 393 - // @ts-expect-error injected untyped global 394 - get: () => globalNamespace.__vitest_index__, 395 - }) 396 - } 397 - 398 - if (this.options.context && this.externalModules) { 399 - context.require = this.externalModules.createRequire(context.__filename) 400 - } 401 - 402 - return context 403 - } 404 - }
+7 -41
packages/vitest/src/runtime/external-executor.ts
··· 3 3 import type { FileMap } from './vm/file-map' 4 4 import type { VMModule } from './vm/types' 5 5 import fs from 'node:fs' 6 - import { dirname } from 'node:path' 6 + import { isBuiltin } from 'node:module' 7 7 import { fileURLToPath, pathToFileURL } from 'node:url' 8 - import { extname, join, normalize } from 'pathe' 9 - import { getCachedData, isBareImport, isNodeBuiltin, setCacheData } from 'vite-node/utils' 8 + import { isBareImport } from '@vitest/utils' 9 + import { findNearestPackageData } from '@vitest/utils/resolver' 10 + import { extname, normalize } from 'pathe' 10 11 import { CommonjsExecutor } from './vm/commonjs-executor' 11 12 import { EsmExecutor } from './vm/esm-executor' 12 13 import { ViteExecutor } from './vm/vite-executor' 13 14 14 - const { existsSync, statSync } = fs 15 + const { existsSync } = fs 15 16 16 17 // always defined when we use vm pool 17 18 const nativeResolve = import.meta.resolve! ··· 119 120 return nativeResolve(specifier, parent) 120 121 } 121 122 122 - private findNearestPackageData(basedir: string): { 123 - type?: 'module' | 'commonjs' 124 - } { 125 - const originalBasedir = basedir 126 - const packageCache = this.options.packageCache 127 - while (basedir) { 128 - const cached = getCachedData(packageCache, basedir, originalBasedir) 129 - if (cached) { 130 - return cached 131 - } 132 - 133 - const pkgPath = join(basedir, 'package.json') 134 - try { 135 - if (statSync(pkgPath, { throwIfNoEntry: false })?.isFile()) { 136 - const pkgData = JSON.parse(this.fs.readFile(pkgPath)) 137 - 138 - if (packageCache) { 139 - setCacheData(packageCache, pkgData, basedir, originalBasedir) 140 - } 141 - 142 - return pkgData 143 - } 144 - } 145 - catch {} 146 - 147 - const nextBasedir = dirname(basedir) 148 - if (nextBasedir === basedir) { 149 - break 150 - } 151 - basedir = nextBasedir 152 - } 153 - 154 - return {} 155 - } 156 - 157 123 private getModuleInformation(identifier: string): ModuleInformation { 158 124 if (identifier.startsWith('data:')) { 159 125 return { type: 'data', url: identifier, path: identifier } 160 126 } 161 127 162 128 const extension = extname(identifier) 163 - if (extension === '.node' || isNodeBuiltin(identifier)) { 129 + if (extension === '.node' || isBuiltin(identifier)) { 164 130 return { type: 'builtin', url: identifier, path: identifier } 165 131 } 166 132 ··· 193 159 type = 'wasm' 194 160 } 195 161 else { 196 - const pkgData = this.findNearestPackageData(normalize(pathUrl)) 162 + const pkgData = findNearestPackageData(normalize(pathUrl)) 197 163 type = pkgData.type === 'module' ? 'module' : 'commonjs' 198 164 } 199 165
-432
packages/vitest/src/runtime/mocker.ts
··· 1 - import type { ManualMockedModule, MockedModule, MockedModuleType } from '@vitest/mocker' 2 - import type { MockFactory, MockOptions, PendingSuiteMock } from '../types/mocker' 3 - import type { VitestExecutor } from './execute' 4 - import { isAbsolute, resolve } from 'node:path' 5 - import vm from 'node:vm' 6 - import { AutomockedModule, MockerRegistry, mockObject, RedirectedModule } from '@vitest/mocker' 7 - import { findMockRedirect } from '@vitest/mocker/redirect' 8 - import { highlight } from '@vitest/utils' 9 - import { distDir } from '../paths' 10 - 11 - const spyModulePath = resolve(distDir, 'spy.js') 12 - 13 - interface MockContext { 14 - /** 15 - * When mocking with a factory, this refers to the module that imported the mock. 16 - */ 17 - callstack: null | string[] 18 - } 19 - 20 - export class VitestMocker { 21 - static pendingIds: PendingSuiteMock[] = [] 22 - private spyModule?: typeof import('@vitest/spy') 23 - private primitives: { 24 - Object: typeof Object 25 - Function: typeof Function 26 - RegExp: typeof RegExp 27 - Array: typeof Array 28 - Map: typeof Map 29 - Error: typeof Error 30 - Symbol: typeof Symbol 31 - } 32 - 33 - private filterPublicKeys: (symbol | string)[] 34 - 35 - private registries = new Map<string, MockerRegistry>() 36 - 37 - private mockContext: MockContext = { 38 - callstack: null, 39 - } 40 - 41 - constructor(public executor: VitestExecutor) { 42 - const context = this.executor.options.context 43 - if (context) { 44 - this.primitives = vm.runInContext( 45 - '({ Object, Error, Function, RegExp, Symbol, Array, Map })', 46 - context, 47 - ) 48 - } 49 - else { 50 - this.primitives = { 51 - Object, 52 - Error, 53 - Function, 54 - RegExp, 55 - Symbol: globalThis.Symbol, 56 - Array, 57 - Map, 58 - } 59 - } 60 - 61 - const Symbol = this.primitives.Symbol 62 - 63 - this.filterPublicKeys = [ 64 - '__esModule', 65 - Symbol.asyncIterator, 66 - Symbol.hasInstance, 67 - Symbol.isConcatSpreadable, 68 - Symbol.iterator, 69 - Symbol.match, 70 - Symbol.matchAll, 71 - Symbol.replace, 72 - Symbol.search, 73 - Symbol.split, 74 - Symbol.species, 75 - Symbol.toPrimitive, 76 - Symbol.toStringTag, 77 - Symbol.unscopables, 78 - ] 79 - } 80 - 81 - private get root() { 82 - return this.executor.options.root 83 - } 84 - 85 - private get moduleCache() { 86 - return this.executor.moduleCache 87 - } 88 - 89 - private get moduleDirectories() { 90 - return this.executor.options.moduleDirectories || [] 91 - } 92 - 93 - public async initializeSpyModule(): Promise<void> { 94 - this.spyModule = await this.executor.executeId(spyModulePath) 95 - } 96 - 97 - private getMockerRegistry() { 98 - const suite = this.getSuiteFilepath() 99 - if (!this.registries.has(suite)) { 100 - this.registries.set(suite, new MockerRegistry()) 101 - } 102 - return this.registries.get(suite)! 103 - } 104 - 105 - public reset(): void { 106 - this.registries.clear() 107 - } 108 - 109 - private deleteCachedItem(id: string) { 110 - const mockId = this.getMockPath(id) 111 - if (this.moduleCache.has(mockId)) { 112 - this.moduleCache.delete(mockId) 113 - } 114 - } 115 - 116 - private isModuleDirectory(path: string) { 117 - return this.moduleDirectories.some(dir => path.includes(dir)) 118 - } 119 - 120 - public getSuiteFilepath(): string { 121 - return this.executor.state.filepath || 'global' 122 - } 123 - 124 - private createError(message: string, codeFrame?: string) { 125 - const Error = this.primitives.Error 126 - const error = new Error(message) 127 - Object.assign(error, { codeFrame }) 128 - return error 129 - } 130 - 131 - private async resolvePath(rawId: string, importer: string) { 132 - let id: string 133 - let fsPath: string 134 - try { 135 - [id, fsPath] = await this.executor.originalResolveUrl(rawId, importer) 136 - } 137 - catch (error: any) { 138 - // it's allowed to mock unresolved modules 139 - if (error.code === 'ERR_MODULE_NOT_FOUND') { 140 - const { id: unresolvedId } 141 - = error[Symbol.for('vitest.error.not_found.data')] 142 - id = unresolvedId 143 - fsPath = unresolvedId 144 - } 145 - else { 146 - throw error 147 - } 148 - } 149 - // external is node_module or unresolved module 150 - // for example, some people mock "vscode" and don't have it installed 151 - const external 152 - = !isAbsolute(fsPath) || this.isModuleDirectory(fsPath) ? rawId : null 153 - 154 - return { 155 - id, 156 - fsPath, 157 - external: external ? this.normalizePath(external) : external, 158 - } 159 - } 160 - 161 - public async resolveMocks(): Promise<void> { 162 - if (!VitestMocker.pendingIds.length) { 163 - return 164 - } 165 - 166 - await Promise.all( 167 - VitestMocker.pendingIds.map(async (mock) => { 168 - const { fsPath, external } = await this.resolvePath( 169 - mock.id, 170 - mock.importer, 171 - ) 172 - if (mock.action === 'unmock') { 173 - this.unmockPath(fsPath) 174 - } 175 - if (mock.action === 'mock') { 176 - this.mockPath( 177 - mock.id, 178 - fsPath, 179 - external, 180 - mock.type, 181 - mock.factory, 182 - ) 183 - } 184 - }), 185 - ) 186 - 187 - VitestMocker.pendingIds = [] 188 - } 189 - 190 - private async callFunctionMock(dep: string, mock: ManualMockedModule) { 191 - const cached = this.moduleCache.get(dep)?.exports 192 - if (cached) { 193 - return cached 194 - } 195 - const exports = await mock.resolve() 196 - 197 - const moduleExports = new Proxy(exports, { 198 - get: (target, prop) => { 199 - const val = target[prop] 200 - 201 - // 'then' can exist on non-Promise objects, need nested instanceof check for logic to work 202 - if (prop === 'then') { 203 - if (target instanceof Promise) { 204 - return target.then.bind(target) 205 - } 206 - } 207 - else if (!(prop in target)) { 208 - if (this.filterPublicKeys.includes(prop)) { 209 - return undefined 210 - } 211 - throw this.createError( 212 - `[vitest] No "${String(prop)}" export is defined on the "${mock.raw}" mock. ` 213 - + 'Did you forget to return it from "vi.mock"?' 214 - + '\nIf you need to partially mock a module, you can use "importOriginal" helper inside:\n', 215 - highlight(`vi.mock(import("${mock.raw}"), async (importOriginal) => { 216 - const actual = await importOriginal() 217 - return { 218 - ...actual, 219 - // your mocked methods 220 - } 221 - })`), 222 - ) 223 - } 224 - 225 - return val 226 - }, 227 - }) 228 - 229 - this.moduleCache.set(dep, { exports: moduleExports }) 230 - 231 - return moduleExports 232 - } 233 - 234 - // public method to avoid circular dependency 235 - public getMockContext(): MockContext { 236 - return this.mockContext 237 - } 238 - 239 - // path used to store mocked dependencies 240 - public getMockPath(dep: string) { 241 - return `mock:${dep}` 242 - } 243 - 244 - public getDependencyMock(id: string): MockedModule | undefined { 245 - const registry = this.getMockerRegistry() 246 - return registry.get(id) 247 - } 248 - 249 - public normalizePath(path: string): string { 250 - return this.moduleCache.normalizePath(path) 251 - } 252 - 253 - public resolveMockPath(mockPath: string, external: string | null): string | null { 254 - return findMockRedirect(this.root, mockPath, external) 255 - } 256 - 257 - public mockObject( 258 - object: Record<string | symbol, any>, 259 - mockExports: Record<string | symbol, any> = {}, 260 - behavior: MockedModuleType = 'automock', 261 - ): Record<string | symbol, any> { 262 - const spyOn = this.spyModule?.spyOn 263 - if (!spyOn) { 264 - throw this.createError( 265 - '[vitest] `spyModule` is not defined. This is a Vitest error. Please open a new issue with reproduction.', 266 - ) 267 - } 268 - return mockObject({ 269 - globalConstructors: this.primitives, 270 - spyOn, 271 - type: behavior, 272 - }, object, mockExports) 273 - } 274 - 275 - public unmockPath(path: string): void { 276 - const registry = this.getMockerRegistry() 277 - const id = this.normalizePath(path) 278 - 279 - registry.delete(id) 280 - this.deleteCachedItem(id) 281 - } 282 - 283 - public mockPath( 284 - originalId: string, 285 - path: string, 286 - external: string | null, 287 - mockType: MockedModuleType | undefined, 288 - factory: MockFactory | undefined, 289 - ): void { 290 - const registry = this.getMockerRegistry() 291 - const id = this.normalizePath(path) 292 - 293 - if (mockType === 'manual') { 294 - registry.register('manual', originalId, id, id, factory!) 295 - } 296 - else if (mockType === 'autospy') { 297 - registry.register('autospy', originalId, id, id) 298 - } 299 - else { 300 - const redirect = this.resolveMockPath(id, external) 301 - if (redirect) { 302 - registry.register('redirect', originalId, id, id, redirect) 303 - } 304 - else { 305 - registry.register('automock', originalId, id, id) 306 - } 307 - } 308 - 309 - // every time the mock is registered, we remove the previous one from the cache 310 - this.deleteCachedItem(id) 311 - } 312 - 313 - public async importActual<T>( 314 - rawId: string, 315 - importer: string, 316 - callstack?: string[] | null, 317 - ): Promise<T> { 318 - const { id, fsPath } = await this.resolvePath(rawId, importer) 319 - const result = await this.executor.cachedRequest( 320 - id, 321 - fsPath, 322 - callstack || [importer], 323 - ) 324 - return result as T 325 - } 326 - 327 - public async importMock(rawId: string, importee: string): Promise<any> { 328 - const { id, fsPath, external } = await this.resolvePath(rawId, importee) 329 - 330 - const normalizedId = this.normalizePath(fsPath) 331 - let mock = this.getDependencyMock(normalizedId) 332 - 333 - if (!mock) { 334 - const redirect = this.resolveMockPath(normalizedId, external) 335 - if (redirect) { 336 - mock = new RedirectedModule(rawId, normalizedId, normalizedId, redirect) 337 - } 338 - else { 339 - mock = new AutomockedModule(rawId, normalizedId, normalizedId) 340 - } 341 - } 342 - 343 - if (mock.type === 'automock' || mock.type === 'autospy') { 344 - const mod = await this.executor.cachedRequest(id, fsPath, [importee]) 345 - return this.mockObject(mod, {}, mock.type) 346 - } 347 - 348 - if (mock.type === 'manual') { 349 - return this.callFunctionMock(fsPath, mock) 350 - } 351 - return this.executor.dependencyRequest(mock.redirect, mock.redirect, [importee]) 352 - } 353 - 354 - public async requestWithMock(url: string, callstack: string[]): Promise<any> { 355 - const id = this.normalizePath(url) 356 - const mock = this.getDependencyMock(id) 357 - 358 - if (!mock) { 359 - return 360 - } 361 - 362 - const mockPath = this.getMockPath(id) 363 - 364 - if (mock.type === 'automock' || mock.type === 'autospy') { 365 - const cache = this.moduleCache.get(mockPath) 366 - if (cache.exports) { 367 - return cache.exports 368 - } 369 - const exports = {} 370 - // Assign the empty exports object early to allow for cycles to work. The object will be filled by mockObject() 371 - this.moduleCache.set(mockPath, { exports }) 372 - const mod = await this.executor.directRequest(url, url, callstack) 373 - this.mockObject(mod, exports, mock.type) 374 - return exports 375 - } 376 - if ( 377 - mock.type === 'manual' 378 - && !callstack.includes(mockPath) 379 - && !callstack.includes(url) 380 - ) { 381 - try { 382 - callstack.push(mockPath) 383 - // this will not work if user does Promise.all(import(), import()) 384 - // we can also use AsyncLocalStorage to store callstack, but this won't work in the browser 385 - // maybe we should improve mock API in the future? 386 - this.mockContext.callstack = callstack 387 - return await this.callFunctionMock(mockPath, mock) 388 - } 389 - finally { 390 - this.mockContext.callstack = null 391 - const indexMock = callstack.indexOf(mockPath) 392 - callstack.splice(indexMock, 1) 393 - } 394 - } 395 - else if (mock.type === 'redirect' && !callstack.includes(mock.redirect)) { 396 - return mock.redirect 397 - } 398 - } 399 - 400 - public queueMock( 401 - id: string, 402 - importer: string, 403 - factoryOrOptions?: MockFactory | MockOptions, 404 - ): void { 405 - const mockType = getMockType(factoryOrOptions) 406 - VitestMocker.pendingIds.push({ 407 - action: 'mock', 408 - id, 409 - importer, 410 - factory: typeof factoryOrOptions === 'function' ? factoryOrOptions : undefined, 411 - type: mockType, 412 - }) 413 - } 414 - 415 - public queueUnmock(id: string, importer: string): void { 416 - VitestMocker.pendingIds.push({ 417 - action: 'unmock', 418 - id, 419 - importer, 420 - }) 421 - } 422 - } 423 - 424 - function getMockType(factoryOrOptions?: MockFactory | MockOptions): MockedModuleType { 425 - if (!factoryOrOptions) { 426 - return 'automock' 427 - } 428 - if (typeof factoryOrOptions === 'function') { 429 - return 'manual' 430 - } 431 - return factoryOrOptions.spy ? 'autospy' : 'automock' 432 - }
+8 -9
packages/vitest/src/runtime/runBaseTests.ts
··· 1 1 import type { FileSpecification } from '@vitest/runner' 2 - import type { ModuleCacheMap } from 'vite-node' 3 2 import type { ResolvedTestEnvironment } from '../types/environment' 4 3 import type { SerializedConfig } from './config' 5 - import type { VitestExecutor } from './execute' 4 + import type { VitestModuleRunner } from './moduleRunner/moduleRunner' 6 5 import { performance } from 'node:perf_hooks' 7 6 import { collectTests, startTests } from '@vitest/runner' 8 7 import { setupChaiConfig } from '../integrations/chai/config' ··· 22 21 files: FileSpecification[], 23 22 config: SerializedConfig, 24 23 environment: ResolvedTestEnvironment, 25 - executor: VitestExecutor, 24 + moduleRunner: VitestModuleRunner, 26 25 ): Promise<void> { 27 26 const workerState = getWorkerState() 28 27 ··· 30 29 const isIsolatedForks = config.pool === 'forks' && (config.poolOptions?.forks?.isolate ?? true) 31 30 const isolate = isIsolatedThreads || isIsolatedForks 32 31 33 - await setupGlobalEnv(config, environment, executor) 34 - await startCoverageInsideWorker(config.coverage, executor, { isolate }) 32 + await setupGlobalEnv(config, environment, moduleRunner) 33 + await startCoverageInsideWorker(config.coverage, moduleRunner, { isolate }) 35 34 36 35 if (config.chaiConfig) { 37 36 setupChaiConfig(config.chaiConfig) 38 37 } 39 38 40 - const runner = await resolveTestRunner(config, executor) 39 + const runner = await resolveTestRunner(config, moduleRunner) 41 40 42 41 workerState.onCancel.then((reason) => { 43 42 closeInspector(config) ··· 56 55 57 56 for (const file of files) { 58 57 if (isolate) { 59 - executor.mocker.reset() 60 - resetModules(workerState.moduleCache as ModuleCacheMap, true) 58 + moduleRunner.mocker.reset() 59 + resetModules(workerState.evaluatedModules, true) 61 60 } 62 61 63 62 workerState.filepath = file.filepath ··· 75 74 vi.restoreAllMocks() 76 75 } 77 76 78 - await stopCoverageInsideWorker(config.coverage, executor, { isolate }) 77 + await stopCoverageInsideWorker(config.coverage, moduleRunner, { isolate }) 79 78 }, 80 79 ) 81 80
+9 -14
packages/vitest/src/runtime/runVmTests.ts
··· 1 1 import type { FileSpecification } from '@vitest/runner' 2 - import type { ModuleCacheMap } from 'vite-node' 3 2 import type { SerializedConfig } from './config' 4 - import type { VitestExecutor } from './execute' 3 + import type { VitestModuleRunner } from './moduleRunner/moduleRunner' 5 4 import { createRequire } from 'node:module' 6 5 import { performance } from 'node:perf_hooks' 7 6 import timers from 'node:timers' 8 7 import timersPromises from 'node:timers/promises' 9 8 import util from 'node:util' 10 9 import { collectTests, startTests } from '@vitest/runner' 11 - import { KNOWN_ASSET_TYPES } from 'vite-node/constants' 12 - import { installSourcemapsSupport } from 'vite-node/source-map' 10 + import { KNOWN_ASSET_TYPES } from '@vitest/utils' 13 11 import { setupChaiConfig } from '../integrations/chai/config' 14 12 import { 15 13 startCoverageInsideWorker, ··· 26 24 method: 'run' | 'collect', 27 25 files: FileSpecification[], 28 26 config: SerializedConfig, 29 - executor: VitestExecutor, 27 + moduleRunner: VitestModuleRunner, 30 28 ): Promise<void> { 31 29 const workerState = getWorkerState() 32 30 ··· 37 35 enumerable: false, 38 36 }) 39 37 40 - if (workerState.environment.transformMode === 'web') { 38 + const viteEnvironment = workerState.environment.viteEnvironment || workerState.environment.name 39 + if (viteEnvironment === 'client') { 41 40 const _require = createRequire(import.meta.url) 42 41 // always mock "required" `css` files, because we cannot process them 43 42 _require.extensions['.css'] = resolveCss ··· 61 60 timersPromises, 62 61 } 63 62 64 - installSourcemapsSupport({ 65 - getSourceMap: source => (workerState.moduleCache as ModuleCacheMap).getSourceMap(source), 66 - }) 67 - 68 - await startCoverageInsideWorker(config.coverage, executor, { isolate: false }) 63 + await startCoverageInsideWorker(config.coverage, moduleRunner, { isolate: false }) 69 64 70 65 if (config.chaiConfig) { 71 66 setupChaiConfig(config.chaiConfig) 72 67 } 73 68 74 69 const [runner, snapshotEnvironment] = await Promise.all([ 75 - resolveTestRunner(config, executor), 76 - resolveSnapshotEnvironment(config, executor), 70 + resolveTestRunner(config, moduleRunner), 71 + resolveSnapshotEnvironment(config, moduleRunner), 77 72 ]) 78 73 79 74 config.snapshotOptions.snapshotEnvironment = snapshotEnvironment ··· 106 101 vi.restoreAllMocks() 107 102 } 108 103 109 - await stopCoverageInsideWorker(config.coverage, executor, { isolate: false }) 104 + await stopCoverageInsideWorker(config.coverage, moduleRunner, { isolate: false }) 110 105 } 111 106 112 107 function resolveCss(mod: NodeJS.Module) {
+10 -11
packages/vitest/src/runtime/setup-common.ts
··· 2 2 import type { SnapshotSerializer } from '@vitest/snapshot' 3 3 import type { SerializedDiffOptions } from '@vitest/utils/diff' 4 4 import type { SerializedConfig } from './config' 5 - import type { VitestExecutor } from './execute' 5 + import type { VitestModuleRunner } from './moduleRunner/moduleRunner' 6 6 import { addSerializer } from '@vitest/snapshot' 7 7 import { setSafeTimers } from '@vitest/utils' 8 + import { getWorkerState } from './utils' 8 9 9 10 let globalSetup = false 10 11 export async function setupCommonEnv(config: SerializedConfig): Promise<void> { ··· 30 31 } 31 32 32 33 function setupEnv(env: Record<string, any>) { 33 - if (typeof process === 'undefined') { 34 - return 35 - } 34 + const state = getWorkerState() 36 35 // same boolean-to-string assignment as VitestPlugin.configResolved 37 36 const { PROD, DEV, ...restEnvs } = env 38 - process.env.PROD = PROD ? '1' : '' 39 - process.env.DEV = DEV ? '1' : '' 37 + state.metaEnv.PROD = PROD 38 + state.metaEnv.DEV = DEV 40 39 for (const key in restEnvs) { 41 - process.env[key] = env[key] 40 + state.metaEnv[key] = env[key] 42 41 } 43 42 } 44 43 45 44 export async function loadDiffConfig( 46 45 config: SerializedConfig, 47 - executor: VitestExecutor, 46 + moduleRunner: VitestModuleRunner, 48 47 ): Promise<SerializedDiffOptions | undefined> { 49 48 if (typeof config.diff === 'object') { 50 49 return config.diff ··· 53 52 return 54 53 } 55 54 56 - const diffModule = await executor.executeId(config.diff) 55 + const diffModule = await moduleRunner.import(config.diff) 57 56 58 57 if ( 59 58 diffModule ··· 71 70 72 71 export async function loadSnapshotSerializers( 73 72 config: SerializedConfig, 74 - executor: VitestExecutor, 73 + moduleRunner: VitestModuleRunner, 75 74 ): Promise<void> { 76 75 const files = config.snapshotSerializers 77 76 78 77 const snapshotSerializers = await Promise.all( 79 78 files.map(async (file) => { 80 - const mo = await executor.executeId(file) 79 + const mo = await moduleRunner.import(file) 81 80 if (!mo || typeof mo.default !== 'object' || mo.default === null) { 82 81 throw new Error( 83 82 `invalid snapshot serializer file ${file}. Must export a default object`,
+6 -12
packages/vitest/src/runtime/setup-node.ts
··· 1 - import type { ModuleCacheMap } from 'vite-node' 2 1 import type { ResolvedTestEnvironment } from '../types/environment' 3 2 import type { SerializedConfig } from './config' 4 - import type { VitestExecutor } from './execute' 3 + import type { VitestModuleRunner } from './moduleRunner/moduleRunner' 5 4 import { createRequire } from 'node:module' 6 5 import timers from 'node:timers' 7 6 import timersPromises from 'node:timers/promises' 8 7 import util from 'node:util' 9 - import { getSafeTimers } from '@vitest/utils' 10 - import { KNOWN_ASSET_TYPES } from 'vite-node/constants' 11 - import { installSourcemapsSupport } from 'vite-node/source-map' 8 + import { getSafeTimers, KNOWN_ASSET_TYPES } from '@vitest/utils' 12 9 import { expect } from '../integrations/chai' 13 10 import { resolveSnapshotEnvironment } from '../integrations/snapshot/environments/resolveSnapshotEnvironment' 14 11 import * as VitestIndex from '../public/index' ··· 20 17 export async function setupGlobalEnv( 21 18 config: SerializedConfig, 22 19 { environment }: ResolvedTestEnvironment, 23 - executor: VitestExecutor, 20 + moduleRunner: VitestModuleRunner, 24 21 ): Promise<void> { 25 22 await setupCommonEnv(config) 26 23 ··· 33 30 34 31 if (!state.config.snapshotOptions.snapshotEnvironment) { 35 32 state.config.snapshotOptions.snapshotEnvironment 36 - = await resolveSnapshotEnvironment(config, executor) 33 + = await resolveSnapshotEnvironment(config, moduleRunner) 37 34 } 38 35 39 36 if (globalSetup) { ··· 42 39 43 40 globalSetup = true 44 41 45 - if (environment.transformMode === 'web') { 42 + const viteEnvironment = environment.viteEnvironment || environment.name 43 + if (viteEnvironment === 'client') { 46 44 const _require = createRequire(import.meta.url) 47 45 // always mock "required" `css` files, because we cannot process them 48 46 _require.extensions['.css'] = resolveCss ··· 65 63 timers, 66 64 timersPromises, 67 65 } 68 - 69 - installSourcemapsSupport({ 70 - getSourceMap: source => (state.moduleCache as ModuleCacheMap).getSourceMap(source), 71 - }) 72 66 73 67 if (!config.disableConsoleIntercept) { 74 68 await setupConsoleLogSpy()
+10 -11
packages/vitest/src/runtime/utils.ts
··· 1 - import type { ModuleCacheMap } from 'vite-node/client' 2 - 1 + import type { EvaluatedModules } from 'vite/module-runner' 3 2 import type { WorkerGlobalState } from '../types/worker' 4 3 import { getSafeTimers } from '@vitest/utils' 5 4 ··· 48 47 catch {} 49 48 } 50 49 51 - export function resetModules(modules: ModuleCacheMap, resetMocks = false): void { 50 + export function resetModules(modules: EvaluatedModules, resetMocks = false): void { 52 51 const skipPaths = [ 53 52 // Vitest 54 53 /\/vitest\/dist\//, 55 - /\/vite-node\/dist\//, 56 54 // yarn's .store folder 57 55 /vitest-virtual-\w+\/dist/, 58 56 // cnpm ··· 60 58 // don't clear mocks 61 59 ...(!resetMocks ? [/^mock:/] : []), 62 60 ] 63 - modules.forEach((mod, path) => { 61 + modules.idToModuleMap.forEach((node, path) => { 64 62 if (skipPaths.some(re => re.test(path))) { 65 63 return 66 64 } 67 - modules.invalidateModule(mod) 65 + 66 + node.promise = undefined 67 + node.exports = undefined 68 + node.evaluated = false 69 + node.importers.clear() 68 70 }) 69 71 } 70 72 ··· 77 79 await waitNextTick() 78 80 const state = getWorkerState() 79 81 const promises: Promise<unknown>[] = [] 80 - let resolvingCount = 0 81 - for (const mod of state.moduleCache.values()) { 82 + const resolvingCount = state.resolvingModules.size 83 + for (const [_, mod] of state.evaluatedModules.idToModuleMap) { 82 84 if (mod.promise && !mod.evaluated) { 83 85 promises.push(mod.promise) 84 - } 85 - if (mod.resolving) { 86 - resolvingCount++ 87 86 } 88 87 } 89 88 if (!promises.length && !resolvingCount) {
+42 -6
packages/vitest/src/runtime/worker.ts
··· 1 + import type { ModuleRunner } from 'vite/module-runner' 1 2 import type { ContextRPC, WorkerGlobalState } from '../types/worker' 2 3 import type { VitestWorker } from './workers/types' 3 4 import { pathToFileURL } from 'node:url' 4 5 import { createStackString, parseStacktrace } from '@vitest/utils/source-map' 5 6 import { workerId as poolId } from 'tinypool' 6 - import { ModuleCacheMap } from 'vite-node/client' 7 + import { EvaluatedModules } from 'vite/module-runner' 7 8 import { loadEnvironment } from '../integrations/env/loader' 8 9 import { addCleanupListener, cleanup as cleanupWorker } from './cleanup' 9 10 import { setupInspect } from './inspector' ··· 30 31 } 31 32 } 32 33 34 + const resolvingModules = new Set<string>() 35 + 33 36 // this is what every pool executes when running tests 34 37 async function execute(method: 'run' | 'collect', ctx: ContextRPC) { 35 38 disposeInternalListeners() ··· 40 43 41 44 process.env.VITEST_WORKER_ID = String(ctx.workerId) 42 45 process.env.VITEST_POOL_ID = String(poolId) 46 + 47 + let environmentLoader: ModuleRunner | undefined 43 48 44 49 try { 45 50 // worker is a filepath or URL to a file that exposes a default export with "getRpcOptions" and "runTests" methods ··· 81 86 }) 82 87 83 88 const beforeEnvironmentTime = performance.now() 84 - const environment = await loadEnvironment(ctx, rpc) 85 - if (ctx.environment.transformMode) { 86 - environment.transformMode = ctx.environment.transformMode 87 - } 89 + const { environment, loader } = await loadEnvironment(ctx, rpc) 90 + environmentLoader = loader 88 91 89 92 const state = { 90 93 ctx, 91 94 // here we create a new one, workers can reassign this if they need to keep it non-isolated 92 - moduleCache: new ModuleCacheMap(), 95 + evaluatedModules: new EvaluatedModules(), 96 + resolvingModules, 93 97 moduleExecutionInfo: new Map(), 94 98 config: ctx.config, 95 99 onCancel, ··· 104 108 onFilterStackTrace(stack) { 105 109 return createStackString(parseStacktrace(stack)) 106 110 }, 111 + metaEnv: createImportMetaEnvProxy(), 107 112 } satisfies WorkerGlobalState 108 113 109 114 const methodName = method === 'collect' ? 'collectTests' : 'runTests' ··· 120 125 await Promise.all(cleanups.map(fn => fn())) 121 126 122 127 await rpcDone().catch(() => {}) 128 + environmentLoader?.close() 123 129 } 124 130 } 125 131 ··· 133 139 134 140 export async function teardown(): Promise<void> { 135 141 return cleanupWorker() 142 + } 143 + 144 + function createImportMetaEnvProxy(): WorkerGlobalState['metaEnv'] { 145 + // packages/vitest/src/node/plugins/index.ts:146 146 + const booleanKeys = ['DEV', 'PROD', 'SSR'] 147 + return new Proxy(process.env, { 148 + get(_, key) { 149 + if (typeof key !== 'string') { 150 + return undefined 151 + } 152 + if (booleanKeys.includes(key)) { 153 + return !!process.env[key] 154 + } 155 + return process.env[key] 156 + }, 157 + set(_, key, value) { 158 + if (typeof key !== 'string') { 159 + return true 160 + } 161 + 162 + if (booleanKeys.includes(key)) { 163 + process.env[key] = value ? '1' : '' 164 + } 165 + else { 166 + process.env[key] = value 167 + } 168 + 169 + return true 170 + }, 171 + }) as WorkerGlobalState['metaEnv'] 136 172 }
+4 -4
packages/vitest/src/typecheck/collect.ts
··· 1 1 import type { File, RunMode, Suite, Test } from '@vitest/runner' 2 - import type { RawSourceMap } from 'vite-node' 2 + import type { Rollup } from 'vite' 3 3 import type { TestProject } from '../node/project' 4 4 import { 5 5 calculateSuiteHash, ··· 39 39 file: File 40 40 filepath: string 41 41 parsed: string 42 - map: RawSourceMap | null 42 + map: Rollup.SourceMap | null 43 43 definitions: LocalCallDefinition[] 44 44 } 45 45 ··· 47 47 ctx: TestProject, 48 48 filepath: string, 49 49 ): Promise<null | FileInformation> { 50 - const request = await ctx.vitenode.transformRequest(filepath, filepath) 50 + const request = await ctx.vite.environments.ssr.transformRequest(filepath) 51 51 if (!request) { 52 52 return null 53 53 } ··· 229 229 file, 230 230 parsed: request.code, 231 231 filepath, 232 - map: request.map as RawSourceMap | null, 232 + map: request.map as Rollup.SourceMap | null, 233 233 definitions, 234 234 } 235 235 }
+11 -1
packages/vitest/src/types/environment.ts
··· 11 11 12 12 export interface Environment { 13 13 name: string 14 - transformMode: 'web' | 'ssr' 14 + /** 15 + * @deprecated use `viteEnvironment` instead. Uses `name` by default 16 + */ 17 + transformMode?: 'web' | 'ssr' 18 + /** 19 + * Environment initiated by the Vite server. It is usually available 20 + * as `vite.server.environments.${name}`. 21 + * 22 + * By default, fallbacks to `name`. 23 + */ 24 + viteEnvironment?: 'client' | 'ssr' | ({} & string) 15 25 setupVM?: (options: Record<string, any>) => Awaitable<VmEnvironmentReturn> 16 26 setup: ( 17 27 global: any,
+16 -3
packages/vitest/src/types/general.ts
··· 4 4 export type Arrayable<T> = T | Array<T> 5 5 export type ArgumentsType<T> = T extends (...args: infer U) => any ? U : never 6 6 7 - export type TransformMode = 'web' | 'ssr' 8 - 9 7 export interface AfterSuiteRunMeta { 10 8 coverage?: unknown 11 9 testFiles: string[] 12 - transformMode: TransformMode | 'browser' 10 + environment: string 13 11 projectName?: string 14 12 } 15 13 ··· 30 28 } 31 29 32 30 export interface ProvidedContext {} 31 + 32 + export interface ResolveFunctionResult { 33 + id: string 34 + file: string 35 + url: string 36 + } 37 + 38 + export interface FetchCachedFileSystemResult { 39 + cached: true 40 + tmp: string 41 + id: string 42 + file: string | null 43 + url: string 44 + invalidate: boolean 45 + } 33 46 34 47 // These need to be compatible with Tinyrainbow's bg-colors, and CSS's background-color 35 48 export type LabelColor = 'black' | 'red' | 'green' | 'yellow' | 'blue' | 'magenta' | 'cyan' | 'white'
+5 -19
packages/vitest/src/types/rpc.ts
··· 1 1 import type { CancelReason, File, TaskEventPack, TaskResultPack, TestAnnotation } from '@vitest/runner' 2 2 import type { SnapshotResult } from '@vitest/snapshot' 3 - import type { AfterSuiteRunMeta, TransformMode, UserConsoleLog } from './general' 3 + import type { FetchFunctionOptions, FetchResult } from 'vite/module-runner' 4 + import type { AfterSuiteRunMeta, FetchCachedFileSystemResult, ResolveFunctionResult, UserConsoleLog } from './general' 4 5 5 6 export interface RuntimeRPC { 6 - fetch: ( 7 - id: string, 8 - transformMode: TransformMode 9 - ) => Promise<{ 10 - externalize?: string 11 - id?: string 12 - }> 13 - transform: (id: string, transformMode: TransformMode) => Promise<{ 14 - code?: string 15 - }> 16 - resolveId: ( 17 - id: string, 18 - importer: string | undefined, 19 - transformMode: TransformMode 20 - ) => Promise<{ 21 - external?: boolean | 'absolute' | 'relative' 22 - id: string 23 - } | null> 7 + fetch: (id: string, importer: string | undefined, environment: string, options?: FetchFunctionOptions) => Promise<FetchResult | FetchCachedFileSystemResult> 8 + resolve: (id: string, importer: string | undefined, environment: string) => Promise<ResolveFunctionResult | null> 9 + transform: (id: string) => Promise<{ code?: string }> 24 10 25 11 onUserConsoleLog: (log: UserConsoleLog) => void 26 12 onUnhandledError: (err: unknown, type: string) => void
+11 -3
packages/vitest/src/types/worker.ts
··· 1 1 import type { CancelReason, FileSpecification, Task } from '@vitest/runner' 2 2 import type { BirpcReturn } from 'birpc' 3 + import type { EvaluatedModules } from 'vite/module-runner' 3 4 import type { SerializedConfig } from '../runtime/config' 4 5 import type { Environment } from './environment' 5 - import type { TransformMode } from './general' 6 6 import type { RunnerRPC, RuntimeRPC } from './rpc' 7 7 8 8 export type WorkerRPC = BirpcReturn<RuntimeRPC, RunnerRPC> 9 9 10 10 export interface ContextTestEnvironment { 11 11 name: string 12 - transformMode?: TransformMode 13 12 options: Record<string, any> | null 14 13 } 15 14 ··· 33 32 rpc: WorkerRPC 34 33 current?: Task 35 34 filepath?: string 35 + metaEnv: { 36 + [key: string]: any 37 + BASE_URL: string 38 + MODE: string 39 + DEV: boolean 40 + PROD: boolean 41 + SSR: boolean 42 + } 36 43 environment: Environment 37 44 environmentTeardownRun?: boolean 38 45 onCancel: Promise<CancelReason> 39 - moduleCache: Map<string, any> 46 + evaluatedModules: EvaluatedModules 47 + resolvingModules: Set<string> 40 48 moduleExecutionInfo: Map<string, any> 41 49 onCleanup: (listener: () => unknown) => void 42 50 providedContext: Record<string, any>
+5 -6
packages/vitest/src/utils/coverage.ts
··· 1 - import type { ModuleExecutionInfo } from 'vite-node/client' 2 1 import type { SerializedCoverageConfig } from '../runtime/config' 3 2 4 3 export interface RuntimeCoverageModuleLoader { 5 - executeId: (id: string) => Promise<{ default: RuntimeCoverageProviderModule }> 4 + import: (id: string) => Promise<{ default: RuntimeCoverageProviderModule }> 6 5 isBrowser?: boolean 7 - moduleExecutionInfo?: ModuleExecutionInfo 6 + moduleExecutionInfo?: Map<string, { startOffset: number }> 8 7 } 9 8 10 9 export interface RuntimeCoverageProviderModule { ··· 21 20 /** 22 21 * Executed on after each run in the worker thread. Possible to return a payload passed to the provider 23 22 */ 24 - takeCoverage?: (runtimeOptions?: { moduleExecutionInfo?: ModuleExecutionInfo }) => unknown | Promise<unknown> 23 + takeCoverage?: (runtimeOptions?: { moduleExecutionInfo?: Map<string, { startOffset: number }> }) => unknown | Promise<unknown> 25 24 26 25 /** 27 26 * Executed after all tests have been run in the worker thread. ··· 51 50 builtInModule += '/browser' 52 51 } 53 52 54 - const { default: coverageModule } = await loader.executeId(builtInModule) 53 + const { default: coverageModule } = await loader.import(builtInModule) 55 54 56 55 if (!coverageModule) { 57 56 throw new Error( ··· 65 64 let customProviderModule 66 65 67 66 try { 68 - customProviderModule = await loader.executeId(options.customProviderModule!) 67 + customProviderModule = await loader.import(options.customProviderModule!) 69 68 } 70 69 catch (error) { 71 70 throw new Error(
+2 -1
packages/vitest/src/utils/graph.ts
··· 26 26 } 27 27 let id = clearId(mod.id) 28 28 seen.set(mod, id) 29 + // TODO: how to know if it was rewritten(?) - what is rewritten? 29 30 const rewrote = browser 30 31 ? mod.file?.includes(project.browser!.vite.config.cacheDir) 31 32 ? mod.id 32 33 : false 33 - : await project.vitenode.shouldExternalize(id) 34 + : false 34 35 if (rewrote) { 35 36 id = rewrote 36 37 externalized.add(id)
+1 -21
packages/vitest/src/utils/test-helpers.ts
··· 1 1 import type { TestProject } from '../node/project' 2 2 import type { TestSpecification } from '../node/spec' 3 - import type { EnvironmentOptions, TransformModePatterns, VitestEnvironment } from '../node/types/config' 3 + import type { EnvironmentOptions, VitestEnvironment } from '../node/types/config' 4 4 import type { ContextTestEnvironment } from '../types/worker' 5 5 import { promises as fs } from 'node:fs' 6 - import pm from 'picomatch' 7 6 import { groupBy } from './base' 8 7 9 8 export const envsOrder: string[] = ['node', 'jsdom', 'happy-dom', 'edge-runtime'] ··· 12 11 file: string 13 12 env: VitestEnvironment 14 13 envOptions: EnvironmentOptions | null 15 - } 16 - 17 - function getTransformMode( 18 - patterns: TransformModePatterns, 19 - filename: string, 20 - ): 'web' | 'ssr' | undefined { 21 - if (patterns.web && pm.isMatch(filename, patterns.web)) { 22 - return 'web' 23 - } 24 - if (patterns.ssr && pm.isMatch(filename, patterns.ssr)) { 25 - return 'ssr' 26 - } 27 - return undefined 28 14 } 29 15 30 16 export async function groupFilesByEnv( ··· 43 29 // 2. Fallback to global env 44 30 env ||= project.config.environment || 'node' 45 31 46 - const transformMode = getTransformMode( 47 - project.config.testTransformMode, 48 - filepath, 49 - ) 50 - 51 32 let envOptionsJson = code.match(/@(?:vitest|jest)-environment-options\s+(.+)/)?.[1] 52 33 if (envOptionsJson?.endsWith('*/')) { 53 34 // Trim closing Docblock characters the above regex might have captured ··· 58 39 const envKey = env === 'happy-dom' ? 'happyDOM' : env 59 40 const environment: ContextTestEnvironment = { 60 41 name: env as VitestEnvironment, 61 - transformMode, 62 42 options: envOptions 63 43 ? ({ [envKey]: envOptions } as EnvironmentOptions) 64 44 : null,
+1 -1
test/cli/deps/error/package.json
··· 1 1 { 2 - "name": "@vitest/test-dep-error", 2 + "name": "@test/test-dep-error", 3 3 "type": "module", 4 4 "private": true, 5 5 "exports": {
+1 -1
test/cli/deps/linked/package.json
··· 1 1 { 2 - "name": "@vitest/test-dep-linked", 2 + "name": "@test/test-dep-linked", 3 3 "type": "module", 4 4 "private": true, 5 5 "exports": {
+1 -1
test/cli/fixtures/config-loader/vitest.config.ts
··· 1 1 import { defineConfig } from "vitest/config" 2 - import "@vitest/test-dep-linked/ts"; 2 + import "@test/test-dep-linked/ts"; 3 3 4 4 export default defineConfig({ 5 5 test: {
+4 -4
test/cli/fixtures/stacktraces/error-in-package.test.js
··· 1 1 import { test } from 'vitest' 2 - import testStack from "@vitest/test-dep-error" 3 - import testStackTs from "@vitest/test-dep-error/ts.ts" 4 - import testStackTranspiled from "@vitest/test-dep-error/transpiled.js" 5 - import testStackTranspiledInline from "@vitest/test-dep-error/transpiled-inline.js" 2 + import testStack from "@test/test-dep-error" 3 + import testStackTs from "@test/test-dep-error/ts.ts" 4 + import testStackTranspiled from "@test/test-dep-error/transpiled.js" 5 + import testStackTranspiledInline from "@test/test-dep-error/transpiled-inline.js" 6 6 7 7 test('js', () => { 8 8 testStack()
+22 -28
test/cli/test/__snapshots__/stacktraces.test.ts.snap
··· 28 28 29 29 FAIL error-in-package.test.js > js 30 30 Error: __TEST_STACK__ 31 - ❯ innerTestStack (NODE_MODULES)/@vitest/test-dep-error/index.js:10:9 32 - ❯ testStack (NODE_MODULES)/@vitest/test-dep-error/index.js:4:3 31 + ❯ innerTestStack (NODE_MODULES)/@test/test-dep-error/index.js:10:9 32 + ❯ testStack (NODE_MODULES)/@test/test-dep-error/index.js:4:3 33 33 ❯ error-in-package.test.js:8:12 34 34 6| 35 35 7| test('js', () => { ··· 42 42 43 43 FAIL error-in-package.test.js > ts 44 44 Error: __TEST_STACK_TS__ 45 - ❯ innerTestStack (NODE_MODULES)/@vitest/test-dep-error/ts.ts:22:9 45 + ❯ innerTestStack (NODE_MODULES)/@test/test-dep-error/ts.ts:22:9 46 46 20| */ 47 47 21| function innerTestStack() { 48 48 22| throw new Error('__TEST_STACK_TS__') 49 49 | ^ 50 50 23| } 51 51 24| 52 - ❯ testStack (NODE_MODULES)/@vitest/test-dep-error/ts.ts:12:3 52 + ❯ testStack (NODE_MODULES)/@test/test-dep-error/ts.ts:12:3 53 53 ❯ error-in-package.test.js:12:14 54 54 55 55 ⎯⎯[2/4]⎯ 56 56 57 57 FAIL error-in-package.test.js > transpiled 58 58 Error: __TEST_STACK_TRANSPILED__ 59 - ❯ innerTestStack (NODE_MODULES)/@vitest/test-dep-error/transpiled.ts:22:9 60 - ❯ testStack (NODE_MODULES)/@vitest/test-dep-error/transpiled.ts:12:3 59 + ❯ innerTestStack (NODE_MODULES)/@test/test-dep-error/transpiled.js:7:9 60 + ❯ testStack (NODE_MODULES)/@test/test-dep-error/transpiled.js:3:3 61 61 ❯ error-in-package.test.js:16:22 62 62 14| 63 63 15| test('transpiled', () => { ··· 70 70 71 71 FAIL error-in-package.test.js > transpiled inline 72 72 Error: __TEST_STACK_TRANSPILED_INLINE__ 73 - ❯ innerTestStack (NODE_MODULES)/@vitest/test-dep-error/transpiled-inline.ts:22:9 74 - ❯ testStack (NODE_MODULES)/@vitest/test-dep-error/transpiled-inline.ts:12:3 73 + ❯ innerTestStack (NODE_MODULES)/@test/test-dep-error/transpiled-inline.js:7:9 74 + ❯ testStack (NODE_MODULES)/@test/test-dep-error/transpiled-inline.js:3:3 75 75 ❯ error-in-package.test.js:20:28 76 76 18| 77 77 19| test('transpiled inline', () => { ··· 91 91 92 92 FAIL error-in-package.test.js > js 93 93 Error: __TEST_STACK__ 94 - ❯ innerTestStack (NODE_MODULES)/@vitest/test-dep-error/index.js:10:9 94 + ❯ innerTestStack (NODE_MODULES)/@test/test-dep-error/index.js:10:9 95 95 8| 96 96 9| function innerTestStack() { 97 97 10| throw new Error('__TEST_STACK__') 98 98 | ^ 99 99 11| } 100 100 12| 101 - ❯ testStack (NODE_MODULES)/@vitest/test-dep-error/index.js:4:3 101 + ❯ testStack (NODE_MODULES)/@test/test-dep-error/index.js:4:3 102 102 ❯ error-in-package.test.js:8:12 103 103 104 104 ⎯⎯[1/4]⎯ 105 105 106 106 FAIL error-in-package.test.js > ts 107 107 Error: __TEST_STACK_TS__ 108 - ❯ innerTestStack (NODE_MODULES)/@vitest/test-dep-error/ts.ts:22:9 108 + ❯ innerTestStack (NODE_MODULES)/@test/test-dep-error/ts.ts:22:9 109 109 20| */ 110 110 21| function innerTestStack() { 111 111 22| throw new Error('__TEST_STACK_TS__') 112 112 | ^ 113 113 23| } 114 114 24| 115 - ❯ testStack (NODE_MODULES)/@vitest/test-dep-error/ts.ts:12:3 115 + ❯ testStack (NODE_MODULES)/@test/test-dep-error/ts.ts:12:3 116 116 ❯ error-in-package.test.js:12:14 117 117 118 118 ⎯⎯[2/4]⎯ 119 119 120 120 FAIL error-in-package.test.js > transpiled 121 121 Error: __TEST_STACK_TRANSPILED__ 122 - ❯ innerTestStack (NODE_MODULES)/@vitest/test-dep-error/transpiled.ts:22:9 123 - ❯ testStack (NODE_MODULES)/@vitest/test-dep-error/transpiled.ts:12:3 122 + ❯ innerTestStack (NODE_MODULES)/@test/test-dep-error/transpiled.js:22:9 123 + 7| throw new Error("__TEST_STACK_TRANSPILED__"); 124 + 8| } 125 + 10| 126 + ❯ testStack (NODE_MODULES)/@test/test-dep-error/transpiled.js:12:3 124 127 ❯ error-in-package.test.js:16:22 125 - 14| 126 - 15| test('transpiled', () => { 127 - 16| testStackTranspiled() 128 - | ^ 129 - 17| }) 130 - 18| 131 128 132 129 ⎯⎯[3/4]⎯ 133 130 134 131 FAIL error-in-package.test.js > transpiled inline 135 132 Error: __TEST_STACK_TRANSPILED_INLINE__ 136 - ❯ innerTestStack (NODE_MODULES)/@vitest/test-dep-error/transpiled-inline.ts:22:9 137 - ❯ testStack (NODE_MODULES)/@vitest/test-dep-error/transpiled-inline.ts:12:3 133 + ❯ innerTestStack (NODE_MODULES)/@test/test-dep-error/transpiled-inline.js:22:9 134 + 7| throw new Error("__TEST_STACK_TRANSPILED_INLINE__"); 135 + 8| } 136 + 10| 137 + ❯ testStack (NODE_MODULES)/@test/test-dep-error/transpiled-inline.js:12:3 138 138 ❯ error-in-package.test.js:20:28 139 - 18| 140 - 19| test('transpiled inline', () => { 141 - 20| testStackTranspiledInline() 142 - | ^ 143 - 21| }) 144 - 22| 145 139 146 140 ⎯⎯[4/4]⎯ 147 141
+1
test/core/projects/vite-environment-external/index.d.ts
··· 1 + export declare const external: boolean
+1
test/core/projects/vite-environment-external/index.js
··· 1 + export const external = typeof __vite_ssr_import__ === 'undefined'
+5
test/core/projects/vite-environment-external/package.json
··· 1 + { 2 + "name": "@test/vite-environment-external", 3 + "type": "module", 4 + "main": "index.js" 5 + }
+1
test/core/projects/vite-external/index.d.ts
··· 1 + export declare const external: boolean
+1
test/core/projects/vite-external/index.js
··· 1 + export const external = typeof __vite_ssr_import__ === 'undefined'
+5
test/core/projects/vite-external/package.json
··· 1 + { 2 + "name": "@test/vite-external", 3 + "type": "module", 4 + "main": "index.js" 5 + }
+5 -5
packages/browser/src/client/tester/runner.ts
··· 12 12 } from 'vitest/internal/browser' 13 13 import { NodeBenchmarkRunner, VitestTestRunner } from 'vitest/runners' 14 14 import { createStackString, parseStacktrace } from '../../../../utils/src/source-map' 15 - import { executor, getWorkerState } from '../utils' 15 + import { getWorkerState, moduleRunner } from '../utils' 16 16 import { rpc } from './rpc' 17 17 import { VitestBrowserSnapshotEnvironment } from './snapshot' 18 18 ··· 117 117 await rpc().onAfterSuiteRun({ 118 118 coverage, 119 119 testFiles: files.map(file => file.name), 120 - transformMode: 'browser', 120 + environment: '__browser__', 121 121 projectName: this.config.name, 122 122 }) 123 123 } ··· 223 223 224 224 const BrowserRunner = createBrowserRunner(runnerClass, mocker, state, { 225 225 takeCoverage: () => 226 - takeCoverageInsideWorker(config.coverage, executor), 226 + takeCoverageInsideWorker(config.coverage, moduleRunner), 227 227 }) 228 228 if (!config.snapshotOptions.snapshotEnvironment) { 229 229 config.snapshotOptions.snapshotEnvironment = new VitestBrowserSnapshotEnvironment() ··· 238 238 }) 239 239 240 240 const [diffOptions] = await Promise.all([ 241 - loadDiffConfig(config, executor as any), 242 - loadSnapshotSerializers(config, executor as any), 241 + loadDiffConfig(config, moduleRunner as any), 242 + loadSnapshotSerializers(config, moduleRunner as any), 243 243 ]) 244 244 runner.config.diffOptions = diffOptions 245 245 getWorkerState().onFilterStackTrace = (stack: string) => {
+4 -2
packages/browser/src/client/tester/state.ts
··· 25 25 config, 26 26 environment: { 27 27 name: 'browser', 28 - transformMode: 'web', 28 + viteEnvironment: 'client', 29 29 setup() { 30 30 throw new Error('Not called in the browser') 31 31 }, 32 32 }, 33 33 onCleanup: fn => getBrowserState().cleanups.push(fn), 34 - moduleCache: getBrowserState().moduleCache, 34 + evaluatedModules: getBrowserState().evaluatedModules, 35 + resolvingModules: getBrowserState().resolvingModules, 35 36 moduleExecutionInfo: new Map(), 37 + metaEnv: null as any, 36 38 rpc: null as any, 37 39 durations: { 38 40 environment: 0,
+4 -3
packages/browser/src/client/tester/tester.ts
··· 10 10 startTests, 11 11 stopCoverageInsideWorker, 12 12 } from 'vitest/internal/browser' 13 - import { executor, getBrowserState, getConfig, getWorkerState } from '../utils' 13 + import { getBrowserState, getConfig, getWorkerState, moduleRunner } from '../utils' 14 14 import { setupDialogsSpy } from './dialog' 15 15 import { setupConsoleLogSpy } from './logger' 16 16 import { VitestBrowserClientMocker } from './mocker' ··· 101 101 102 102 const state = getWorkerState() 103 103 104 + state.metaEnv = import.meta.env 104 105 state.onCancel = onCancel 105 106 state.rpc = rpc as any 106 107 ··· 207 208 208 209 await Promise.all([ 209 210 setupCommonEnv(config), 210 - startCoverageInsideWorker(config.coverage, executor, { isolate: config.browser.isolate }), 211 + startCoverageInsideWorker(config.coverage, moduleRunner, { isolate: config.browser.isolate }), 211 212 (async () => { 212 213 const VitestIndex = await import('vitest') 213 214 Object.defineProperty(window, '__vitest_index__', { ··· 249 250 .catch(error => unhandledError(error, 'Cleanup Error')) 250 251 } 251 252 state.environmentTeardownRun = true 252 - await stopCoverageInsideWorker(config.coverage, executor, { isolate: config.browser.isolate }).catch((error) => { 253 + await stopCoverageInsideWorker(config.coverage, moduleRunner, { isolate: config.browser.isolate }).catch((error) => { 253 254 return unhandledError(error, 'Coverage Error') 254 255 }) 255 256 }
+1 -1
packages/vitest/src/integrations/env/edge-runtime.ts
··· 4 4 5 5 export default <Environment>{ 6 6 name: 'edge-runtime', 7 - transformMode: 'ssr', 7 + viteEnvironment: 'ssr', 8 8 async setupVM() { 9 9 const { EdgeVM } = await import('@edge-runtime/vm') 10 10 const vm = new EdgeVM({
+1 -1
packages/vitest/src/integrations/env/happy-dom.ts
··· 16 16 17 17 export default <Environment>{ 18 18 name: 'happy-dom', 19 - transformMode: 'web', 19 + viteEnvironment: 'client', 20 20 async setupVM({ happyDOM = {} }) { 21 21 const { Window } = await import('happy-dom') 22 22 let win = new Window({
+1 -1
packages/vitest/src/integrations/env/jsdom.ts
··· 35 35 36 36 export default <Environment>{ 37 37 name: 'jsdom', 38 - transformMode: 'web', 38 + viteEnvironment: 'client', 39 39 async setupVM({ jsdom = {} }) { 40 40 const { CookieJar, JSDOM, ResourceLoader, VirtualConsole } = await import( 41 41 'jsdom'
+59 -29
packages/vitest/src/integrations/env/loader.ts
··· 1 - import type { ViteNodeRunnerOptions } from 'vite-node' 2 1 import type { BuiltinEnvironment, VitestEnvironment } from '../../node/types/config' 3 2 import type { Environment } from '../../types/environment' 4 3 import type { ContextRPC, WorkerRPC } from '../../types/worker' 5 4 import { readFileSync } from 'node:fs' 6 - import { normalize, resolve } from 'pathe' 7 - import { ViteNodeRunner } from 'vite-node/client' 5 + import { isBuiltin } from 'node:module' 6 + import { pathToFileURL } from 'node:url' 7 + import { resolve } from 'pathe' 8 + import { ModuleRunner } from 'vite/module-runner' 9 + import { VitestTransport } from '../../runtime/moduleRunner/moduleTransport' 8 10 import { environments } from './index' 9 11 10 12 function isBuiltinEnvironment( ··· 13 15 return env in environments 14 16 } 15 17 16 - const _loaders = new Map<string, ViteNodeRunner>() 18 + const isWindows = process.platform === 'win32' 19 + const _loaders = new Map<string, ModuleRunner>() 17 20 18 - export async function createEnvironmentLoader(options: ViteNodeRunnerOptions): Promise<ViteNodeRunner> { 19 - if (!_loaders.has(options.root)) { 20 - const loader = new ViteNodeRunner(options) 21 - await loader.executeId('/@vite/env') 22 - _loaders.set(options.root, loader) 21 + export async function createEnvironmentLoader(root: string, rpc: WorkerRPC): Promise<ModuleRunner> { 22 + const cachedLoader = _loaders.get(root) 23 + if (!cachedLoader || cachedLoader.isClosed()) { 24 + _loaders.delete(root) 25 + 26 + const moduleRunner = new ModuleRunner({ 27 + hmr: false, 28 + sourcemapInterceptor: 'prepareStackTrace', 29 + transport: new VitestTransport({ 30 + async fetchModule(id, importer, options) { 31 + const result = await rpc.fetch(id, importer, '__vitest__', options) 32 + if ('cached' in result) { 33 + const code = readFileSync(result.tmp, 'utf-8') 34 + return { code, ...result } 35 + } 36 + if (isWindows && 'externalize' in result) { 37 + // TODO: vitest returns paths for external modules, but Vite returns file:// 38 + // https://github.com/vitejs/vite/pull/20449 39 + result.externalize = isBuiltin(id) || /^(?:node:|data:|http:|https:|file:)/.test(id) 40 + ? result.externalize 41 + : pathToFileURL(result.externalize).toString() 42 + } 43 + return result 44 + }, 45 + async resolveId(id, importer) { 46 + return rpc.resolve(id, importer, '__vitest__') 47 + }, 48 + }), 49 + }) 50 + _loaders.set(root, moduleRunner) 51 + await moduleRunner.import('/@vite/env') 23 52 } 24 - return _loaders.get(options.root)! 53 + return _loaders.get(root)! 25 54 } 26 55 27 56 export async function loadEnvironment( 28 57 ctx: ContextRPC, 29 58 rpc: WorkerRPC, 30 - ): Promise<Environment> { 59 + ): Promise<{ environment: Environment; loader?: ModuleRunner }> { 31 60 const name = ctx.environment.name 32 61 if (isBuiltinEnvironment(name)) { 33 - return environments[name] 62 + return { environment: environments[name] } 34 63 } 35 - const loader = await createEnvironmentLoader({ 36 - root: ctx.config.root, 37 - fetchModule: async (id) => { 38 - const result = await rpc.fetch(id, 'ssr') 39 - if (result.id) { 40 - return { code: readFileSync(result.id, 'utf-8') } 41 - } 42 - return result 43 - }, 44 - resolveId: (id, importer) => rpc.resolveId(id, importer, 'ssr'), 45 - }) 46 - const root = loader.root 64 + const root = ctx.config.root 65 + const loader = await createEnvironmentLoader(root, rpc) 47 66 const packageId 48 67 = name[0] === '.' || name[0] === '/' 49 68 ? resolve(root, name) 50 - : (await rpc.resolveId(`vitest-environment-${name}`, undefined, 'ssr')) 69 + : (await rpc.resolve(`vitest-environment-${name}`, undefined, '__vitest__')) 51 70 ?.id ?? resolve(root, name) 52 - const pkg = await loader.executeId(normalize(packageId)) 71 + const pkg = await loader.import(packageId) as { default: Environment } 53 72 if (!pkg || !pkg.default || typeof pkg.default !== 'object') { 54 73 throw new TypeError( 55 74 `Environment "${name}" is not a valid environment. ` ··· 58 77 } 59 78 const environment = pkg.default 60 79 if ( 61 - environment.transformMode !== 'web' 80 + environment.transformMode != null 81 + && environment.transformMode !== 'web' 62 82 && environment.transformMode !== 'ssr' 63 83 ) { 64 84 throw new TypeError( 65 85 `Environment "${name}" is not a valid environment. ` 66 - + `Path "${packageId}" should export default object with a "transformMode" method equal to "ssr" or "web".`, 86 + + `Path "${packageId}" should export default object with a "transformMode" method equal to "ssr" or "web", received "${environment.transformMode}".`, 67 87 ) 68 88 } 69 - return environment 89 + if (environment.transformMode) { 90 + console.warn(`The Vitest environment ${environment.name} defines the "transformMode". This options was deprecated in Vitest 4 and will be removed in the next major version. Please, use "viteEnvironment" instead.`) 91 + // keep for backwards compat 92 + environment.viteEnvironment ??= environment.transformMode === 'ssr' 93 + ? 'ssr' 94 + : 'client' 95 + } 96 + return { 97 + environment, 98 + loader, 99 + } 70 100 }
+1 -1
packages/vitest/src/integrations/env/node.ts
··· 32 32 33 33 export default <Environment>{ 34 34 name: 'node', 35 - transformMode: 'ssr', 35 + viteEnvironment: 'ssr', 36 36 // this is largely copied from jest's node environment 37 37 async setupVM() { 38 38 const vm = await import('node:vm')
-1
packages/vitest/src/node/cli/cli-config.ts
··· 856 856 uiBase: null, 857 857 benchmark: null, 858 858 include: null, 859 - testTransformMode: null, 860 859 fakeTimers: null, 861 860 chaiConfig: null, 862 861 clearMocks: null,
+14 -67
packages/vitest/src/node/config/resolveConfig.ts
··· 20 20 defaultBrowserPort, 21 21 defaultInspectPort, 22 22 defaultPort, 23 - extraInlineDeps, 24 23 } from '../../constants' 25 24 import { benchmarkConfigDefaults, configDefaults } from '../../defaults' 26 25 import { isCI, stdProvider } from '../../utils/env' ··· 337 336 338 337 resolved.deps ??= {} 339 338 resolved.deps.moduleDirectories ??= [] 339 + 340 + const envModuleDirectories 341 + = process.env.VITEST_MODULE_DIRECTORIES 342 + || process.env.npm_config_VITEST_MODULE_DIRECTORIES 343 + 344 + if (envModuleDirectories) { 345 + resolved.deps.moduleDirectories.push(...envModuleDirectories.split(',')) 346 + } 347 + 340 348 resolved.deps.moduleDirectories = resolved.deps.moduleDirectories.map( 341 349 (dir) => { 342 350 if (!dir.startsWith('/')) { ··· 354 362 355 363 resolved.deps.optimizer ??= {} 356 364 resolved.deps.optimizer.ssr ??= {} 357 - resolved.deps.optimizer.ssr.enabled ??= true 358 - resolved.deps.optimizer.web ??= {} 359 - resolved.deps.optimizer.web.enabled ??= true 365 + resolved.deps.optimizer.ssr.enabled ??= false 366 + resolved.deps.optimizer.client ??= {} 367 + resolved.deps.optimizer.client.enabled ??= false 360 368 361 369 resolved.deps.web ??= {} 362 370 resolved.deps.web.transformAssets ??= true ··· 403 411 ...resolved.setupFiles, 404 412 ] 405 413 406 - resolved.server ??= {} 407 - resolved.server.deps ??= {} 408 - 409 - const deprecatedDepsOptions = ['inline', 'external', 'fallbackCJS'] as const 410 - deprecatedDepsOptions.forEach((option) => { 411 - if (resolved.deps[option] === undefined) { 412 - return 413 - } 414 - 415 - if (option === 'fallbackCJS') { 416 - logger.console.warn( 417 - c.yellow( 418 - `${c.inverse( 419 - c.yellow(' Vitest '), 420 - )} "deps.${option}" is deprecated. Use "server.deps.${option}" instead`, 421 - ), 422 - ) 423 - } 424 - else { 425 - const transformMode 426 - = resolved.environment === 'happy-dom' || resolved.environment === 'jsdom' 427 - ? 'web' 428 - : 'ssr' 429 - logger.console.warn( 430 - c.yellow( 431 - `${c.inverse( 432 - c.yellow(' Vitest '), 433 - )} "deps.${option}" is deprecated. If you rely on vite-node directly, use "server.deps.${option}" instead. Otherwise, consider using "deps.optimizer.${transformMode}.${ 434 - option === 'external' ? 'exclude' : 'include' 435 - }"`, 436 - ), 437 - ) 438 - } 439 - 440 - if (resolved.server.deps![option] === undefined) { 441 - resolved.server.deps![option] = resolved.deps[option] as any 442 - } 443 - }) 444 - 445 414 if (resolved.cliExclude) { 446 415 resolved.exclude.push(...resolved.cliExclude) 447 416 } 448 - 449 - // vitenode will try to import such file with native node, 450 - // but then our mocker will not work properly 451 - if (resolved.server.deps.inline !== true) { 452 - const ssrOptions = viteConfig.ssr 453 - if ( 454 - ssrOptions?.noExternal === true 455 - && resolved.server.deps.inline == null 456 - ) { 457 - resolved.server.deps.inline = true 458 - } 459 - else { 460 - resolved.server.deps.inline ??= [] 461 - resolved.server.deps.inline.push(...extraInlineDeps) 462 - } 463 - } 464 - 465 - resolved.server.deps.inlineFiles ??= [] 466 - resolved.server.deps.inlineFiles.push(...resolved.setupFiles) 467 - resolved.server.deps.moduleDirectories ??= [] 468 - resolved.server.deps.moduleDirectories.push( 469 - ...resolved.deps.moduleDirectories, 470 - ) 471 417 472 418 if (resolved.runner) { 473 419 resolved.runner = resolvePath(resolved.runner, resolved.root) ··· 858 804 resolved.includeTaskLocation ??= true 859 805 } 860 806 861 - resolved.testTransformMode ??= {} 807 + resolved.server ??= {} 808 + resolved.server.deps ??= {} 862 809 863 810 resolved.testTimeout ??= resolved.browser.enabled ? 15000 : 5000 864 811 resolved.hookTimeout ??= resolved.browser.enabled ? 30000 : 10000
+5 -9
packages/vitest/src/node/config/serializeConfig.ts
··· 6 6 coreConfig: ResolvedConfig, 7 7 viteConfig: ViteConfig | undefined, 8 8 ): SerializedConfig { 9 - const optimizer = config.deps?.optimizer 9 + const optimizer = config.deps?.optimizer || {} 10 10 const poolOptions = config.poolOptions 11 11 12 12 // Resolve from server.config to avoid comparing against default value ··· 103 103 }, 104 104 deps: { 105 105 web: config.deps.web || {}, 106 - optimizer: { 107 - web: { 108 - enabled: optimizer?.web?.enabled ?? true, 109 - }, 110 - ssr: { 111 - enabled: optimizer?.ssr?.enabled ?? true, 112 - }, 113 - }, 106 + optimizer: Object.entries(optimizer).reduce((acc, [name, option]) => { 107 + acc[name] = { enabled: option?.enabled ?? false } 108 + return acc 109 + }, {} as Record<string, { enabled: boolean }>), 114 110 interopDefault: config.deps.interopDefault, 115 111 moduleDirectories: config.deps.moduleDirectories, 116 112 },
+258
packages/vitest/src/node/environments/fetchModule.ts
··· 1 + import type { DevEnvironment, FetchResult, Rollup, TransformResult } from 'vite' 2 + import type { FetchFunctionOptions } from 'vite/module-runner' 3 + import type { FetchCachedFileSystemResult } from '../../types/general' 4 + import type { VitestResolver } from '../resolver' 5 + import { mkdirSync } from 'node:fs' 6 + import { rename, stat, unlink, writeFile } from 'node:fs/promises' 7 + import { tmpdir } from 'node:os' 8 + import { isExternalUrl, nanoid, unwrapId } from '@vitest/utils' 9 + import { dirname, join } from 'pathe' 10 + import { fetchModule } from 'vite' 11 + import { hash } from '../hash' 12 + 13 + const created = new Set() 14 + const promises = new Map<string, Promise<void>>() 15 + 16 + export function createFetchModuleFunction( 17 + resolver: VitestResolver, 18 + cacheFs: boolean = false, 19 + tmpDir: string = join(tmpdir(), nanoid()), 20 + ): ( 21 + url: string, 22 + importer: string | undefined, 23 + environment: DevEnvironment, 24 + options?: FetchFunctionOptions 25 + ) => Promise<FetchResult | FetchCachedFileSystemResult> { 26 + const cachedFsResults = new Map<string, string>() 27 + return async ( 28 + url, 29 + importer, 30 + environment, 31 + options, 32 + ) => { 33 + // We are copy pasting Vite's externalization logic from `fetchModule` because 34 + // we instead rely on our own `shouldExternalize` method because Vite 35 + // doesn't support `resolve.external` in non SSR environments (jsdom/happy-dom) 36 + if (url.startsWith('data:')) { 37 + return { externalize: url, type: 'builtin' } 38 + } 39 + 40 + if (url === '/@vite/client' || url === '@vite/client') { 41 + // this will be stubbed 42 + return { externalize: '/@vite/client', type: 'module' } 43 + } 44 + 45 + const isFileUrl = url.startsWith('file://') 46 + 47 + if (isExternalUrl(url) && !isFileUrl) { 48 + return { externalize: url, type: 'network' } 49 + } 50 + 51 + // Vite does the same in `fetchModule`, but we want to externalize modules ourselves, 52 + // so we do this first to resolve the module and check its `id`. The next call of 53 + // `ensureEntryFromUrl` inside `fetchModule` is cached and should take no time 54 + // This also makes it so externalized modules are inside the module graph. 55 + const moduleGraphModule = await environment.moduleGraph.ensureEntryFromUrl(unwrapId(url)) 56 + const cached = !!moduleGraphModule.transformResult 57 + 58 + // if url is already cached, we can just confirm it's also cached on the server 59 + if (options?.cached && cached) { 60 + return { cache: true } 61 + } 62 + 63 + if (moduleGraphModule.id) { 64 + const externalize = await resolver.shouldExternalize(moduleGraphModule.id) 65 + if (externalize) { 66 + return { externalize, type: 'module' } 67 + } 68 + } 69 + 70 + const moduleRunnerModule = await fetchModule( 71 + environment, 72 + url, 73 + importer, 74 + { 75 + ...options, 76 + inlineSourceMap: false, 77 + }, 78 + ).catch(handleRollupError) 79 + 80 + const result = processResultSource(environment, moduleRunnerModule) 81 + 82 + if (!cacheFs || !('code' in result)) { 83 + return result 84 + } 85 + 86 + const code = result.code 87 + // to avoid serialising large chunks of code, 88 + // we store them in a tmp file and read in the test thread 89 + if (cachedFsResults.has(result.id)) { 90 + return getCachedResult(result, cachedFsResults) 91 + } 92 + const dir = join(tmpDir, environment.name) 93 + const name = hash('sha1', result.id, 'hex') 94 + const tmp = join(dir, name) 95 + if (!created.has(dir)) { 96 + mkdirSync(dir, { recursive: true }) 97 + created.add(dir) 98 + } 99 + if (promises.has(tmp)) { 100 + await promises.get(tmp) 101 + cachedFsResults.set(result.id, tmp) 102 + return getCachedResult(result, cachedFsResults) 103 + } 104 + promises.set( 105 + tmp, 106 + 107 + atomicWriteFile(tmp, code) 108 + // Fallback to non-atomic write for windows case where file already exists: 109 + .catch(() => writeFile(tmp, code, 'utf-8')) 110 + .finally(() => promises.delete(tmp)), 111 + ) 112 + await promises.get(tmp) 113 + cachedFsResults.set(result.id, tmp) 114 + return getCachedResult(result, cachedFsResults) 115 + } 116 + } 117 + 118 + let SOURCEMAPPING_URL = 'sourceMa' 119 + SOURCEMAPPING_URL += 'ppingURL' 120 + 121 + const MODULE_RUNNER_SOURCEMAPPING_SOURCE = '//# sourceMappingSource=vite-generated' 122 + 123 + function processResultSource(environment: DevEnvironment, result: FetchResult): FetchResult { 124 + if (!('code' in result)) { 125 + return result 126 + } 127 + 128 + const node = environment.moduleGraph.getModuleById(result.id) 129 + if (node?.transformResult) { 130 + // this also overrides node.transformResult.code which is also what the module 131 + // runner does under the hood by default (we disable source maps inlining) 132 + inlineSourceMap(node.transformResult) 133 + } 134 + 135 + return { 136 + ...result, 137 + code: node?.transformResult?.code || result.code, 138 + } 139 + } 140 + 141 + const OTHER_SOURCE_MAP_REGEXP = new RegExp( 142 + `//# ${SOURCEMAPPING_URL}=data:application/json[^,]+base64,([A-Za-z0-9+/=]+)$`, 143 + 'gm', 144 + ) 145 + 146 + // we have to inline the source map ourselves, because 147 + // - we don't need //# sourceURL since we are running code in VM 148 + // - important in stack traces and the V8 coverage 149 + // - we need to inject an empty line for --inspect-brk 150 + function inlineSourceMap(result: TransformResult) { 151 + const map = result.map 152 + let code = result.code 153 + 154 + if ( 155 + !map 156 + || !('version' in map) 157 + || code.includes(MODULE_RUNNER_SOURCEMAPPING_SOURCE) 158 + ) { 159 + return result 160 + } 161 + 162 + // to reduce the payload size, we only inline vite node source map, because it's also the only one we use 163 + OTHER_SOURCE_MAP_REGEXP.lastIndex = 0 164 + if (OTHER_SOURCE_MAP_REGEXP.test(code)) { 165 + code = code.replace(OTHER_SOURCE_MAP_REGEXP, '') 166 + } 167 + 168 + const sourceMap = { ...map } 169 + 170 + // If the first line is not present on source maps, add simple 1:1 mapping ([0,0,0,0], [1,0,0,0]) 171 + // so that debuggers can be set to break on first line 172 + if (sourceMap.mappings.startsWith(';')) { 173 + sourceMap.mappings = `AAAA,CAAA${sourceMap.mappings}` 174 + } 175 + 176 + result.code = `${code.trimEnd()}\n${ 177 + MODULE_RUNNER_SOURCEMAPPING_SOURCE 178 + }\n//# ${SOURCEMAPPING_URL}=${genSourceMapUrl(sourceMap)}\n` 179 + 180 + return result 181 + } 182 + 183 + function genSourceMapUrl(map: Rollup.SourceMap | string): string { 184 + if (typeof map !== 'string') { 185 + map = JSON.stringify(map) 186 + } 187 + return `data:application/json;base64,${Buffer.from(map).toString('base64')}` 188 + } 189 + 190 + function getCachedResult(result: Extract<FetchResult, { code: string }>, cachedFsResults: Map<string, string>): FetchCachedFileSystemResult { 191 + const tmp = cachedFsResults.get(result.id) 192 + if (!tmp) { 193 + throw new Error(`The cached result was returned too early for ${result.id}.`) 194 + } 195 + return { 196 + cached: true as const, 197 + file: result.file, 198 + id: result.id, 199 + tmp, 200 + url: result.url, 201 + invalidate: result.invalidate, 202 + } 203 + } 204 + 205 + // serialize rollup error on server to preserve details as a test error 206 + export function handleRollupError(e: unknown): never { 207 + if ( 208 + e instanceof Error 209 + && ('plugin' in e || 'frame' in e || 'id' in e) 210 + ) { 211 + // eslint-disable-next-line no-throw-literal 212 + throw { 213 + name: e.name, 214 + message: e.message, 215 + stack: e.stack, 216 + cause: e.cause, 217 + __vitest_rollup_error__: { 218 + plugin: (e as any).plugin, 219 + id: (e as any).id, 220 + loc: (e as any).loc, 221 + frame: (e as any).frame, 222 + }, 223 + } 224 + } 225 + throw e 226 + } 227 + 228 + /** 229 + * Performs an atomic write operation using the write-then-rename pattern. 230 + * 231 + * Why we need this: 232 + * - Ensures file integrity by never leaving partially written files on disk 233 + * - Prevents other processes from reading incomplete data during writes 234 + * - Particularly important for test files where incomplete writes could cause test failures 235 + * 236 + * The implementation writes to a temporary file first, then renames it to the target path. 237 + * This rename operation is atomic on most filesystems (including POSIX-compliant ones), 238 + * guaranteeing that other processes will only ever see the complete file. 239 + * 240 + * Added in https://github.com/vitest-dev/vitest/pull/7531 241 + */ 242 + async function atomicWriteFile(realFilePath: string, data: string): Promise<void> { 243 + const dir = dirname(realFilePath) 244 + const tmpFilePath = join(dir, `.tmp-${Date.now()}-${Math.random().toString(36).slice(2)}`) 245 + 246 + try { 247 + await writeFile(tmpFilePath, data, 'utf-8') 248 + await rename(tmpFilePath, realFilePath) 249 + } 250 + finally { 251 + try { 252 + if (await stat(tmpFilePath)) { 253 + await unlink(tmpFilePath) 254 + } 255 + } 256 + catch {} 257 + } 258 + }
+47
packages/vitest/src/node/environments/normalizeUrl.ts
··· 1 + import type { DevEnvironment } from 'vite' 2 + import { existsSync } from 'node:fs' 3 + import path from 'node:path' 4 + import { cleanUrl, withTrailingSlash, wrapId } from '@vitest/utils' 5 + 6 + // this is copy pasted from vite 7 + export function normalizeResolvedIdToUrl( 8 + environment: DevEnvironment, 9 + resolvedId: string, 10 + ): string { 11 + const root = environment.config.root 12 + const depsOptimizer = environment.depsOptimizer 13 + 14 + let url: string 15 + 16 + // normalize all imports into resolved URLs 17 + // e.g. `import 'foo'` -> `import '/@fs/.../node_modules/foo/index.js'` 18 + if (resolvedId.startsWith(withTrailingSlash(root))) { 19 + // in root: infer short absolute path from root 20 + url = resolvedId.slice(root.length) 21 + } 22 + else if ( 23 + depsOptimizer?.isOptimizedDepFile(resolvedId) 24 + // vite-plugin-react isn't following the leading \0 virtual module convention. 25 + // This is a temporary hack to avoid expensive fs checks for React apps. 26 + // We'll remove this as soon we're able to fix the react plugins. 27 + || (resolvedId !== '/@react-refresh' 28 + && path.isAbsolute(resolvedId) 29 + && existsSync(cleanUrl(resolvedId))) 30 + ) { 31 + // an optimized deps may not yet exists in the filesystem, or 32 + // a regular file exists but is out of root: rewrite to absolute /@fs/ paths 33 + url = path.posix.join('/@fs/', resolvedId) 34 + } 35 + else { 36 + url = resolvedId 37 + } 38 + 39 + // if the resolved id is not a valid browser import specifier, 40 + // prefix it to make it valid. We will strip this before feeding it 41 + // back into the transform pipeline 42 + if (url[0] !== '.' && url[0] !== '/') { 43 + url = wrapId(resolvedId) 44 + } 45 + 46 + return url 47 + }
+56
packages/vitest/src/node/environments/serverRunner.ts
··· 1 + import type { DevEnvironment } from 'vite' 2 + import type { VitestResolver } from '../resolver' 3 + import type { ResolvedConfig } from '../types/config' 4 + import { VitestModuleEvaluator } from '#module-evaluator' 5 + import { ModuleRunner } from 'vite/module-runner' 6 + import { createFetchModuleFunction } from './fetchModule' 7 + import { normalizeResolvedIdToUrl } from './normalizeUrl' 8 + 9 + export class ServerModuleRunner extends ModuleRunner { 10 + constructor( 11 + private environment: DevEnvironment, 12 + resolver: VitestResolver, 13 + private config: ResolvedConfig, 14 + ) { 15 + const fetchModule = createFetchModuleFunction( 16 + resolver, 17 + false, 18 + ) 19 + super( 20 + { 21 + hmr: false, 22 + sourcemapInterceptor: 'node', 23 + transport: { 24 + async invoke(event) { 25 + if (event.type !== 'custom') { 26 + throw new Error(`Vitest Module Runner doesn't support Vite HMR events.`) 27 + } 28 + const { data } = event.data 29 + try { 30 + const result = await fetchModule(data[0], data[1], environment, data[2]) 31 + return { result } 32 + } 33 + catch (error) { 34 + return { error } 35 + } 36 + }, 37 + }, 38 + }, 39 + new VitestModuleEvaluator(), 40 + ) 41 + } 42 + 43 + async import(rawId: string): Promise<any> { 44 + const resolved = await this.environment.pluginContainer.resolveId( 45 + rawId, 46 + this.config.root, 47 + ) 48 + if (!resolved) { 49 + return super.import(rawId) 50 + } 51 + // Vite will make "@vitest/coverage-v8" into "@vitest/coverage-v8.js" url 52 + // instead of using an actual file path-like URL, so we resolve it here first 53 + const url = normalizeResolvedIdToUrl(this.environment, resolved.id) 54 + return super.import(url) 55 + } 56 + }
+1 -3
packages/vitest/src/node/plugins/coverageTransform.ts
··· 1 1 import type { Plugin as VitePlugin } from 'vite' 2 2 import type { Vitest } from '../core' 3 3 4 - import { normalizeRequestId } from 'vite-node/utils' 5 - 6 4 export function CoverageTransform(ctx: Vitest): VitePlugin { 7 5 return { 8 6 name: 'vitest:coverage-transform', 9 7 transform(srcCode, id) { 10 8 return ctx.coverageProvider?.onFileTransform?.( 11 9 srcCode, 12 - normalizeRequestId(id), 10 + id, 13 11 this, 14 12 ) 15 13 },
+7 -26
packages/vitest/src/node/plugins/index.ts
··· 4 4 deepClone, 5 5 deepMerge, 6 6 notNullish, 7 - toArray, 8 7 } from '@vitest/utils' 9 8 import { relative } from 'pathe' 10 9 import { defaultPort } from '../../constants' ··· 15 14 import { createViteLogger, silenceImportViteIgnoreWarning } from '../viteLogger' 16 15 import { CoverageTransform } from './coverageTransform' 17 16 import { CSSEnablerPlugin } from './cssEnabler' 17 + import { MetaEnvReplacerPlugin } from './metaEnvReplacer' 18 18 import { MocksPlugins } from './mocks' 19 19 import { NormalizeURLPlugin } from './normalizeURL' 20 20 import { VitestOptimizer } from './optimizer' 21 - import { SsrReplacerPlugin } from './ssrReplacer' 21 + import { ModuleRunnerTransform } from './runnerTransform' 22 22 import { 23 23 deleteDefineConfig, 24 24 getDefaultResolveOptions, ··· 121 121 ssr: { 122 122 resolve: resolveOptions, 123 123 }, 124 + __vitest__: { 125 + dev: {}, 126 + }, 124 127 }, 125 128 test: { 126 129 poolOptions: { ··· 163 166 }, 164 167 ) 165 168 config.customLogger = silenceImportViteIgnoreWarning(config.customLogger) 166 - 167 - // we want inline dependencies to be resolved by analyser plugin so module graph is populated correctly 168 - if (viteConfig.ssr?.noExternal !== true) { 169 - const inline = testConfig.server?.deps?.inline 170 - if (inline === true) { 171 - config.ssr = { noExternal: true } 172 - } 173 - else { 174 - const noExternal = viteConfig.ssr?.noExternal 175 - const noExternalArray 176 - = typeof noExternal !== 'undefined' 177 - ? toArray(noExternal) 178 - : undefined 179 - // filter the same packages 180 - const uniqueInline 181 - = inline && noExternalArray 182 - ? inline.filter(dep => !noExternalArray.includes(dep)) 183 - : inline 184 - config.ssr = { 185 - noExternal: uniqueInline, 186 - } 187 - } 188 - } 189 169 190 170 // chokidar fsevents is unstable on macos when emitting "ready" event 191 171 if ( ··· 298 278 }, 299 279 }, 300 280 }, 301 - SsrReplacerPlugin(), 281 + MetaEnvReplacerPlugin(), 302 282 ...CSSEnablerPlugin(vitest), 303 283 CoverageTransform(vitest), 304 284 VitestCoreResolver(vitest), 305 285 ...MocksPlugins(), 306 286 VitestOptimizer(), 307 287 NormalizeURLPlugin(), 288 + ModuleRunnerTransform(), 308 289 ].filter(notNullish) 309 290 } 310 291 function removeUndefinedValues<T extends Record<string, any>>(
+47
packages/vitest/src/node/plugins/metaEnvReplacer.ts
··· 1 + import type { Plugin } from 'vite' 2 + import { cleanUrl } from '@vitest/utils' 3 + import MagicString from 'magic-string' 4 + import { stripLiteral } from 'strip-literal' 5 + 6 + // so people can reassign envs at runtime 7 + // import.meta.env.VITE_NAME = 'app' -> process.env.VITE_NAME = 'app' 8 + export function MetaEnvReplacerPlugin(): Plugin { 9 + return { 10 + name: 'vitest:meta-env-replacer', 11 + enforce: 'pre', 12 + transform(code, id) { 13 + if (!/\bimport\.meta\.env\b/.test(code)) { 14 + return null 15 + } 16 + 17 + let s: MagicString | null = null 18 + const cleanCode = stripLiteral(code) 19 + const envs = cleanCode.matchAll(/\bimport\.meta\.env\b/g) 20 + 21 + for (const env of envs) { 22 + s ||= new MagicString(code) 23 + 24 + const startIndex = env.index! 25 + const endIndex = startIndex + env[0].length 26 + 27 + s.overwrite( 28 + startIndex, 29 + endIndex, 30 + `Object.assign(/* istanbul ignore next */ globalThis.__vitest_worker__?.metaEnv ?? import.meta.env)`, 31 + ) 32 + } 33 + 34 + if (s) { 35 + return { 36 + code: s.toString(), 37 + map: s.generateMap({ 38 + hires: 'boundary', 39 + 40 + // Remove possible query parameters, e.g. vue's "?vue&type=script&src=true&lang.ts" 41 + source: cleanUrl(id), 42 + }), 43 + } 44 + } 45 + }, 46 + } 47 + }
+2 -3
packages/vitest/src/node/plugins/normalizeURL.ts
··· 12 12 return { 13 13 name: 'vitest:normalize-url', 14 14 enforce: 'post', 15 - transform(code, id, options) { 16 - const ssr = options?.ssr === true 15 + transform(code) { 17 16 if ( 18 - ssr 17 + this.environment.name !== 'client' 19 18 || !code.includes('new URL') 20 19 || !code.includes('import.meta.url') 21 20 ) {
-12
packages/vitest/src/node/plugins/optimizer.ts
··· 1 1 import type { Plugin } from 'vite' 2 2 import { resolve } from 'pathe' 3 3 import { VitestCache } from '../cache' 4 - import { resolveOptimizerConfig } from './utils' 5 4 6 5 export function VitestOptimizer(): Plugin { 7 6 return { ··· 10 9 order: 'post', 11 10 handler(viteConfig) { 12 11 const testConfig = viteConfig.test || {} 13 - const webOptimizer = resolveOptimizerConfig( 14 - testConfig.deps?.optimizer?.web, 15 - viteConfig.optimizeDeps, 16 - ) 17 - const ssrOptimizer = resolveOptimizerConfig( 18 - testConfig.deps?.optimizer?.ssr, 19 - viteConfig.ssr?.optimizeDeps, 20 - ) 21 12 22 13 const root = resolve(viteConfig.root || process.cwd()) 23 14 const name = viteConfig.test?.name ··· 30 21 : viteConfig.cacheDir, 31 22 label, 32 23 ) 33 - viteConfig.optimizeDeps = webOptimizer.optimizeDeps 34 - viteConfig.ssr ??= {} 35 - viteConfig.ssr.optimizeDeps = ssrOptimizer.optimizeDeps 36 24 }, 37 25 }, 38 26 }
+163
packages/vitest/src/node/plugins/runnerTransform.ts
··· 1 + import type { ResolvedConfig, UserConfig, Plugin as VitePlugin } from 'vite' 2 + import { builtinModules } from 'node:module' 3 + import { mergeConfig } from 'vite' 4 + import { resolveOptimizerConfig } from './utils' 5 + 6 + export function ModuleRunnerTransform(): VitePlugin { 7 + // make sure Vite always applies the module runner transform 8 + return { 9 + name: 'vitest:environments-module-runner', 10 + config: { 11 + order: 'post', 12 + handler(config) { 13 + const testConfig = config.test || {} 14 + 15 + config.environments ??= {} 16 + 17 + const names = new Set(Object.keys(config.environments)) 18 + names.add('client') 19 + names.add('ssr') 20 + 21 + const pool = config.test?.pool 22 + if (pool === 'vmForks' || pool === 'vmThreads') { 23 + names.add('__vitest_vm__') 24 + } 25 + 26 + const external: (string | RegExp)[] = [] 27 + const noExternal: (string | RegExp)[] = [] 28 + 29 + let noExternalAll: true | undefined 30 + 31 + for (const name of names) { 32 + config.environments[name] ??= {} 33 + 34 + const environment = config.environments[name] 35 + environment.dev ??= {} 36 + // vm tests run using the native import mechanism 37 + if (name === '__vitest_vm__') { 38 + environment.dev.moduleRunnerTransform = false 39 + environment.consumer = 'client' 40 + } 41 + else { 42 + environment.dev.moduleRunnerTransform = true 43 + } 44 + environment.dev.preTransformRequests = false 45 + environment.keepProcessEnv = true 46 + 47 + const resolveExternal = name === 'client' 48 + ? config.resolve?.external 49 + : [] 50 + const resolveNoExternal = name === 'client' 51 + ? config.resolve?.noExternal 52 + : [] 53 + 54 + const topLevelResolveOptions: UserConfig['resolve'] = {} 55 + if (resolveExternal != null) { 56 + topLevelResolveOptions.external = resolveExternal 57 + } 58 + if (resolveNoExternal != null) { 59 + topLevelResolveOptions.noExternal = resolveNoExternal 60 + } 61 + 62 + const currentResolveOptions = mergeConfig( 63 + topLevelResolveOptions, 64 + environment.resolve || {}, 65 + ) as ResolvedConfig['resolve'] 66 + 67 + const envNoExternal = resolveViteResolveOptions('noExternal', currentResolveOptions) 68 + if (envNoExternal === true) { 69 + noExternalAll = true 70 + } 71 + else { 72 + noExternal.push(...envNoExternal) 73 + } 74 + 75 + const envExternal = resolveViteResolveOptions('external', currentResolveOptions) 76 + if (envExternal !== true) { 77 + external.push(...envExternal) 78 + } 79 + 80 + // remove Vite's externalization logic because we have our own (unfortunetly) 81 + environment.resolve ??= {} 82 + 83 + environment.resolve.external = [ 84 + ...builtinModules, 85 + ...builtinModules.map(m => `node:${m}`), 86 + ] 87 + // by setting `noExternal` to `true`, we make sure that 88 + // Vite will never use its own externalization mechanism 89 + // to externalize modules and always resolve static imports 90 + // in both SSR and Client environments 91 + environment.resolve.noExternal = true 92 + 93 + if (name === '__vitest_vm__' || name === '__vitest__') { 94 + continue 95 + } 96 + 97 + const currentOptimizeDeps = environment.optimizeDeps || ( 98 + name === 'client' 99 + ? config.optimizeDeps 100 + : name === 'ssr' 101 + ? config.ssr?.optimizeDeps 102 + : undefined 103 + ) 104 + 105 + const optimizeDeps = resolveOptimizerConfig( 106 + testConfig.deps?.optimizer?.[name], 107 + currentOptimizeDeps, 108 + ) 109 + 110 + // Vite respects the root level optimize deps, so we override it instead 111 + if (name === 'client') { 112 + config.optimizeDeps = optimizeDeps 113 + environment.optimizeDeps = undefined 114 + } 115 + else if (name === 'ssr') { 116 + config.ssr ??= {} 117 + config.ssr.optimizeDeps = optimizeDeps 118 + environment.optimizeDeps = undefined 119 + } 120 + else { 121 + environment.optimizeDeps = optimizeDeps 122 + } 123 + } 124 + 125 + testConfig.server ??= {} 126 + testConfig.server.deps ??= {} 127 + 128 + if (testConfig.server.deps.inline !== true) { 129 + if (noExternalAll) { 130 + testConfig.server.deps.inline = true 131 + } 132 + else if (noExternal.length) { 133 + testConfig.server.deps.inline ??= [] 134 + testConfig.server.deps.inline.push(...noExternal) 135 + } 136 + } 137 + if (external.length) { 138 + testConfig.server.deps.external ??= [] 139 + testConfig.server.deps.external.push(...external) 140 + } 141 + }, 142 + }, 143 + } 144 + } 145 + 146 + function resolveViteResolveOptions( 147 + key: 'noExternal' | 'external', 148 + options: ResolvedConfig['resolve'], 149 + ): true | (string | RegExp)[] { 150 + if (Array.isArray(options[key])) { 151 + return options[key] 152 + } 153 + else if ( 154 + typeof options[key] === 'string' 155 + || options[key] instanceof RegExp 156 + ) { 157 + return [options[key]] 158 + } 159 + else if (typeof options[key] === 'boolean') { 160 + return true 161 + } 162 + return [] 163 + }
-43
packages/vitest/src/node/plugins/ssrReplacer.ts
··· 1 - import type { Plugin } from 'vite' 2 - import MagicString from 'magic-string' 3 - import { stripLiteral } from 'strip-literal' 4 - import { cleanUrl } from 'vite-node/utils' 5 - 6 - // so people can reassign envs at runtime 7 - // import.meta.env.VITE_NAME = 'app' -> process.env.VITE_NAME = 'app' 8 - export function SsrReplacerPlugin(): Plugin { 9 - return { 10 - name: 'vitest:ssr-replacer', 11 - enforce: 'pre', 12 - transform(code, id) { 13 - if (!/\bimport\.meta\.env\b/.test(code)) { 14 - return null 15 - } 16 - 17 - let s: MagicString | null = null 18 - const cleanCode = stripLiteral(code) 19 - const envs = cleanCode.matchAll(/\bimport\.meta\.env\b/g) 20 - 21 - for (const env of envs) { 22 - s ||= new MagicString(code) 23 - 24 - const startIndex = env.index! 25 - const endIndex = startIndex + env[0].length 26 - 27 - s.overwrite(startIndex, endIndex, '__vite_ssr_import_meta__.env') 28 - } 29 - 30 - if (s) { 31 - return { 32 - code: s.toString(), 33 - map: s.generateMap({ 34 - hires: 'boundary', 35 - 36 - // Remove possible query parameters, e.g. vue's "?vue&type=script&src=true&lang.ts" 37 - source: cleanUrl(id), 38 - }), 39 - } 40 - } 41 - }, 42 - } 43 - }
+11 -27
packages/vitest/src/node/plugins/utils.ts
··· 11 11 export function resolveOptimizerConfig( 12 12 _testOptions: DepsOptimizationOptions | undefined, 13 13 viteOptions: DepOptimizationOptions | undefined, 14 - ): { cacheDir?: string; optimizeDeps: DepOptimizationOptions } { 14 + ): DepOptimizationOptions { 15 15 const testOptions = _testOptions || {} 16 - const newConfig: { cacheDir?: string; optimizeDeps: DepOptimizationOptions } 17 - = {} as any 18 - const [major, minor, fix] = viteVersion.split('.').map(Number) 19 - const allowed 20 - = major >= 5 21 - || (major === 4 && minor >= 4) 22 - || (major === 4 && minor === 3 && fix >= 2) 23 - if (!allowed && testOptions?.enabled === true) { 24 - console.warn( 25 - `Vitest: "deps.optimizer" is only available in Vite >= 4.3.2, current Vite version: ${viteVersion}`, 26 - ) 27 - } 28 - // disabled by default 29 - else { 16 + let optimizeDeps: DepOptimizationOptions 17 + if (testOptions.enabled !== true) { 30 18 testOptions.enabled ??= false 31 - } 32 - if (!allowed || testOptions?.enabled !== true) { 33 - newConfig.cacheDir = undefined 34 - newConfig.optimizeDeps = { 19 + 20 + optimizeDeps = { 35 21 // experimental in Vite >2.9.2, entries remains to help with older versions 36 22 disabled: true, 37 23 entries: [], ··· 55 41 (n: string) => !exclude.includes(n), 56 42 ) 57 43 58 - newConfig.optimizeDeps = { 44 + optimizeDeps = { 59 45 ...viteOptions, 60 46 ...testOptions, 61 47 noDiscovery: true, ··· 68 54 69 55 // `optimizeDeps.disabled` is deprecated since v5.1.0-beta.1 70 56 // https://github.com/vitejs/vite/pull/15184 71 - if ((major >= 5 && minor >= 1) || major >= 6) { 72 - if (newConfig.optimizeDeps.disabled) { 73 - newConfig.optimizeDeps.noDiscovery = true 74 - newConfig.optimizeDeps.include = [] 75 - } 76 - delete newConfig.optimizeDeps.disabled 57 + if (optimizeDeps.disabled) { 58 + optimizeDeps.noDiscovery = true 59 + optimizeDeps.include = [] 77 60 } 61 + delete optimizeDeps.disabled 78 62 79 - return newConfig 63 + return optimizeDeps 80 64 } 81 65 82 66 export function deleteDefineConfig(viteConfig: ViteConfig): Record<string, any> {
+9 -2
packages/vitest/src/node/plugins/workspace.ts
··· 10 10 import { createViteLogger, silenceImportViteIgnoreWarning } from '../viteLogger' 11 11 import { CoverageTransform } from './coverageTransform' 12 12 import { CSSEnablerPlugin } from './cssEnabler' 13 + import { MetaEnvReplacerPlugin } from './metaEnvReplacer' 13 14 import { MocksPlugins } from './mocks' 14 15 import { NormalizeURLPlugin } from './normalizeURL' 15 16 import { VitestOptimizer } from './optimizer' 16 - import { SsrReplacerPlugin } from './ssrReplacer' 17 + import { ModuleRunnerTransform } from './runnerTransform' 17 18 import { 18 19 deleteDefineConfig, 19 20 getDefaultResolveOptions, ··· 92 93 } 93 94 94 95 return { 96 + environments: { 97 + __vitest__: { 98 + dev: {}, 99 + }, 100 + }, 95 101 test: { 96 102 name: { label: name, color }, 97 103 }, ··· 202 208 await server.watcher.close() 203 209 }, 204 210 }, 205 - SsrReplacerPlugin(), 211 + MetaEnvReplacerPlugin(), 206 212 ...CSSEnablerPlugin(project), 207 213 CoverageTransform(project.vitest), 208 214 ...MocksPlugins(), 209 215 VitestProjectResolver(project.vitest), 210 216 VitestOptimizer(), 211 217 NormalizeURLPlugin(), 218 + ModuleRunnerTransform(), 212 219 ] 213 220 }
+48 -102
packages/vitest/src/node/pools/rpc.ts
··· 1 1 import type { RuntimeRPC } from '../../types/rpc' 2 2 import type { TestProject } from '../project' 3 3 import type { ResolveSnapshotPathHandlerContext } from '../types/config' 4 - import { mkdirSync } from 'node:fs' 5 - import { rename, stat, unlink, writeFile } from 'node:fs/promises' 6 - import { dirname, join } from 'pathe' 7 - import { hash } from '../hash' 8 - 9 - const created = new Set() 10 - const promises = new Map<string, Promise<void>>() 4 + import { fileURLToPath } from 'node:url' 5 + import { cleanUrl } from '@vitest/utils' 6 + import { createFetchModuleFunction, handleRollupError } from '../environments/fetchModule' 7 + import { normalizeResolvedIdToUrl } from '../environments/normalizeUrl' 11 8 12 9 interface MethodsOptions { 13 10 cacheFs?: boolean ··· 18 15 export function createMethodsRPC(project: TestProject, options: MethodsOptions = {}): RuntimeRPC { 19 16 const ctx = project.vitest 20 17 const cacheFs = options.cacheFs ?? false 18 + const fetch = createFetchModuleFunction(project._resolver, cacheFs, project.tmpDir) 21 19 return { 20 + async fetch( 21 + url, 22 + importer, 23 + environmentName, 24 + options, 25 + ) { 26 + const environment = project.vite.environments[environmentName] 27 + if (!environment) { 28 + throw new Error(`The environment ${environmentName} was not defined in the Vite config.`) 29 + } 30 + 31 + const start = performance.now() 32 + 33 + try { 34 + return await fetch(url, importer, environment, options) 35 + } 36 + finally { 37 + project.vitest.state.transformTime += (performance.now() - start) 38 + } 39 + }, 40 + async resolve(id, importer, environmentName) { 41 + const environment = project.vite.environments[environmentName] 42 + if (!environment) { 43 + throw new Error(`The environment ${environmentName} was not defined in the Vite config.`) 44 + } 45 + const resolved = await environment.pluginContainer.resolveId(id, importer) 46 + if (!resolved) { 47 + return null 48 + } 49 + return { 50 + file: cleanUrl(resolved.id), 51 + url: normalizeResolvedIdToUrl(environment, resolved.id), 52 + id: resolved.id, 53 + } 54 + }, 55 + 22 56 snapshotSaved(snapshot) { 23 57 ctx.snapshot.add(snapshot) 24 58 }, ··· 27 61 config: project.serializedConfig, 28 62 }) 29 63 }, 30 - async fetch(id, transformMode) { 31 - const result = await project.vitenode.fetchResult(id, transformMode).catch(handleRollupError) 32 - const code = result.code 33 - if (!cacheFs || result.externalize) { 34 - return result 35 - } 36 - if ('id' in result && typeof result.id === 'string') { 37 - return { id: result.id } 64 + async transform(id) { 65 + const environment = project.vite.environments.__vitest_vm__ 66 + if (!environment) { 67 + throw new Error(`The VM environment was not defined in the Vite config. This is a bug in Vitest. Please, open a new issue with reproduction.`) 38 68 } 39 69 40 - if (code == null) { 41 - throw new Error(`Failed to fetch module ${id}`) 42 - } 43 - 44 - const dir = join(project.tmpDir, transformMode) 45 - const name = hash('sha1', id, 'hex') 46 - const tmp = join(dir, name) 47 - if (!created.has(dir)) { 48 - mkdirSync(dir, { recursive: true }) 49 - created.add(dir) 50 - } 51 - if (promises.has(tmp)) { 52 - await promises.get(tmp) 53 - return { id: tmp } 54 - } 55 - promises.set( 56 - tmp, 57 - 58 - atomicWriteFile(tmp, code) 59 - // Fallback to non-atomic write for windows case where file already exists: 60 - .catch(() => writeFile(tmp, code, 'utf-8')) 61 - .finally(() => promises.delete(tmp)), 62 - ) 63 - await promises.get(tmp) 64 - Object.assign(result, { id: tmp }) 65 - return { id: tmp } 66 - }, 67 - resolveId(id, importer, transformMode) { 68 - return project.vitenode.resolveId(id, importer, transformMode).catch(handleRollupError) 69 - }, 70 - transform(id, environment) { 71 - return project.vitenode.transformModule(id, environment).catch(handleRollupError) 70 + const url = normalizeResolvedIdToUrl(environment, fileURLToPath(id)) 71 + const result = await environment.transformRequest(url).catch(handleRollupError) 72 + return { code: result?.code } 72 73 }, 73 74 async onQueued(file) { 74 75 if (options.collect) { ··· 117 118 getCountOfFailedTests() { 118 119 return ctx.state.getCountOfFailedTests() 119 120 }, 120 - } 121 - } 122 - 123 - // serialize rollup error on server to preserve details as a test error 124 - function handleRollupError(e: unknown): never { 125 - if ( 126 - e instanceof Error 127 - && ('plugin' in e || 'frame' in e || 'id' in e) 128 - ) { 129 - // eslint-disable-next-line no-throw-literal 130 - throw { 131 - name: e.name, 132 - message: e.message, 133 - stack: e.stack, 134 - cause: e.cause, 135 - __vitest_rollup_error__: { 136 - plugin: (e as any).plugin, 137 - id: (e as any).id, 138 - loc: (e as any).loc, 139 - frame: (e as any).frame, 140 - }, 141 - } 142 - } 143 - throw e 144 - } 145 - 146 - /** 147 - * Performs an atomic write operation using the write-then-rename pattern. 148 - * 149 - * Why we need this: 150 - * - Ensures file integrity by never leaving partially written files on disk 151 - * - Prevents other processes from reading incomplete data during writes 152 - * - Particularly important for test files where incomplete writes could cause test failures 153 - * 154 - * The implementation writes to a temporary file first, then renames it to the target path. 155 - * This rename operation is atomic on most filesystems (including POSIX-compliant ones), 156 - * guaranteeing that other processes will only ever see the complete file. 157 - * 158 - * Added in https://github.com/vitest-dev/vitest/pull/7531 159 - */ 160 - async function atomicWriteFile(realFilePath: string, data: string): Promise<void> { 161 - const dir = dirname(realFilePath) 162 - const tmpFilePath = join(dir, `.tmp-${Date.now()}-${Math.random().toString(36).slice(2)}`) 163 - 164 - try { 165 - await writeFile(tmpFilePath, data, 'utf-8') 166 - await rename(tmpFilePath, realFilePath) 167 - } 168 - finally { 169 - try { 170 - if (await stat(tmpFilePath)) { 171 - await unlink(tmpFilePath) 172 - } 173 - } 174 - catch {} 175 121 } 176 122 }
+1 -1
packages/vitest/src/node/reporters/base.ts
··· 524 524 525 525 const environmentTime = sum(files, file => file.environmentLoad) 526 526 const prepareTime = sum(files, file => file.prepareDuration) 527 - const transformTime = sum(this.ctx.projects, project => project.vitenode.getTotalDuration()) 527 + const transformTime = this.ctx.state.transformTime 528 528 const typecheck = sum(this.ctx.projects, project => project.typechecker?.getResult().time) 529 529 530 530 const timers = [
+5
packages/vitest/src/node/reporters/blob.ts
··· 150 150 const moduleNode = project.vite.moduleGraph.createFileOnlyEntry(file) 151 151 moduleNode.url = url 152 152 moduleNode.id = moduleId 153 + moduleNode.transformResult = { 154 + // print error checks that transformResult is set 155 + code: ' ', 156 + map: null, 157 + } 153 158 project.vite.moduleGraph.idToModuleMap.set(moduleId, moduleNode) 154 159 }) 155 160 })
+5 -5
packages/vitest/src/node/reporters/utils.ts
··· 1 - import type { ViteNodeRunner } from 'vite-node/client' 1 + import type { ModuleRunner } from 'vite/module-runner' 2 2 import type { Vitest } from '../core' 3 3 import type { ResolvedConfig } from '../types/config' 4 4 import type { Reporter } from '../types/reporter' ··· 8 8 9 9 async function loadCustomReporterModule<C extends Reporter>( 10 10 path: string, 11 - runner: ViteNodeRunner, 11 + runner: ModuleRunner, 12 12 ): Promise<new (options?: unknown) => C> { 13 13 let customReporterModule: { default: new () => C } 14 14 try { 15 - customReporterModule = await runner.executeId(path) 15 + customReporterModule = await runner.import(path) 16 16 } 17 17 catch (customReporterModuleError) { 18 18 throw new Error(`Failed to load custom Reporter from ${path}`, { ··· 43 43 const [reporterName, reporterOptions] = referenceOrInstance 44 44 45 45 if (reporterName === 'html') { 46 - await ctx.packageInstaller.ensureInstalled('@vitest/ui', runner.root, ctx.version) 46 + await ctx.packageInstaller.ensureInstalled('@vitest/ui', ctx.config.root, ctx.version) 47 47 const CustomReporter = await loadCustomReporterModule( 48 48 '@vitest/ui/reporter', 49 49 runner, ··· 72 72 73 73 function createBenchmarkReporters( 74 74 reporterReferences: Array<string | Reporter | BenchmarkBuiltinReporters>, 75 - runner: ViteNodeRunner, 75 + runner: ModuleRunner, 76 76 ): Promise<(Reporter | BenchmarkReporter)[]> { 77 77 const promisedReporters = reporterReferences.map( 78 78 async (referenceOrInstance) => {
+1 -1
packages/vitest/src/node/sequencers/BaseSequencer.ts
··· 1 1 import type { Vitest } from '../core' 2 2 import type { TestSpecification } from '../spec' 3 3 import type { TestSequencer } from './types' 4 + import { slash } from '@vitest/utils' 4 5 import { relative, resolve } from 'pathe' 5 - import { slash } from 'vite-node/utils' 6 6 import { hash } from '../hash' 7 7 8 8 export class BaseSequencer implements TestSequencer {
-1
packages/vitest/src/node/types/browser.ts
··· 65 65 // non-browser options 66 66 | 'api' 67 67 | 'deps' 68 - | 'testTransformMode' 69 68 | 'environment' 70 69 | 'environmentOptions' 71 70 | 'server'
+29 -64
packages/vitest/src/node/types/config.ts
··· 4 4 import type { SnapshotStateOptions } from '@vitest/snapshot' 5 5 import type { SerializedDiffOptions } from '@vitest/utils/diff' 6 6 import type { AliasOptions, ConfigEnv, DepOptimizationConfig, ServerOptions, UserConfig as ViteUserConfig } from 'vite' 7 - import type { ViteNodeServerOptions } from 'vite-node' 8 7 import type { ChaiConfig } from '../../integrations/chai/config' 9 8 import type { SerializedConfig } from '../../runtime/config' 10 9 import type { Arrayable, LabelColor, ParsedStack, ProvidedContext, TestError } from '../../types/general' ··· 135 134 enabled?: boolean 136 135 } 137 136 138 - export interface TransformModePatterns { 139 - /** 140 - * Use SSR transform pipeline for all modules inside specified tests. 141 - * Vite plugins will receive `ssr: true` flag when processing those files. 142 - * 143 - * @default tests with node or edge environment 144 - */ 145 - ssr?: string[] 146 - /** 147 - * First do a normal transform pipeline (targeting browser), 148 - * then then do a SSR rewrite to run the code in Node. 149 - * Vite plugins will receive `ssr: false` flag when processing those files. 150 - * 151 - * @default tests with jsdom or happy-dom environment 152 - */ 153 - web?: string[] 154 - } 155 - 156 137 interface DepsOptions { 157 138 /** 158 139 * Enable dependency optimization. This can improve the performance of your tests. 159 140 */ 160 - optimizer?: { 161 - web?: DepsOptimizationOptions 162 - ssr?: DepsOptimizationOptions 163 - } 141 + optimizer?: Partial<Record<'client' | 'ssr' | ({} & string), DepsOptimizationOptions>> 164 142 web?: { 165 143 /** 166 144 * Should Vitest process assets (.png, .svg, .jpg, etc) files and resolve them like Vite does in the browser. ··· 193 171 */ 194 172 transformGlobPattern?: RegExp | RegExp[] 195 173 } 196 - /** 197 - * Externalize means that Vite will bypass the package to native Node. 198 - * 199 - * Externalized dependencies will not be applied Vite's transformers and resolvers. 200 - * And does not support HMR on reload. 201 - * 202 - * Typically, packages under `node_modules` are externalized. 203 - * 204 - * @deprecated If you rely on vite-node directly, use `server.deps.external` instead. Otherwise, consider using `deps.optimizer.{web,ssr}.exclude`. 205 - */ 206 - external?: (string | RegExp)[] 207 - /** 208 - * Vite will process inlined modules. 209 - * 210 - * This could be helpful to handle packages that ship `.js` in ESM format (that Node can't handle). 211 - * 212 - * If `true`, every dependency will be inlined 213 - * 214 - * @deprecated If you rely on vite-node directly, use `server.deps.inline` instead. Otherwise, consider using `deps.optimizer.{web,ssr}.include`. 215 - */ 216 - inline?: (string | RegExp)[] | true 217 174 218 175 /** 219 176 * Interpret CJS module's default as named exports ··· 221 178 * @default true 222 179 */ 223 180 interopDefault?: boolean 224 - 225 - /** 226 - * When a dependency is a valid ESM package, try to guess the cjs version based on the path. 227 - * This will significantly improve the performance in huge repo, but might potentially 228 - * cause some misalignment if a package have different logic in ESM and CJS mode. 229 - * 230 - * @default false 231 - * 232 - * @deprecated Use `server.deps.fallbackCJS` instead. 233 - */ 234 - fallbackCJS?: boolean 235 181 236 182 /** 237 183 * A list of directories relative to the config file that should be treated as module directories. ··· 297 243 */ 298 244 deps?: DepsOptions 299 245 300 - /** 301 - * Vite-node server options 302 - */ 303 - server?: Omit<ViteNodeServerOptions, 'transformMode'> 246 + server?: { 247 + deps?: ServerDepsOptions 248 + } 304 249 305 250 /** 306 251 * Base directory to scan for the test files ··· 559 504 * @default '/__vitest__/' 560 505 */ 561 506 uiBase?: string 562 - 563 - /** 564 - * Determine the transform method for all modules imported inside a test that matches the glob pattern. 565 - */ 566 - testTransformMode?: TransformModePatterns 567 507 568 508 /** 569 509 * Format options for snapshot testing. ··· 1107 1047 | 'minWorkers' 1108 1048 | 'fileParallelism' 1109 1049 | 'watchTriggerPatterns' 1050 + 1051 + export interface ServerDepsOptions { 1052 + /** 1053 + * Externalize means that Vite will bpass the package to native Node. 1054 + * 1055 + * Externalized dependencies will not be applied Vite's transformers and resolvers. 1056 + * And does not support HMR on reload. 1057 + * 1058 + * Typically, packages under `node_modules` are externalized. 1059 + */ 1060 + external?: (string | RegExp)[] 1061 + /** 1062 + * Vite will process inlined modules. 1063 + * 1064 + * This could be helpful to handle packages that ship `.js` in ESM format (that Node can't handle). 1065 + * 1066 + * If `true`, every dependency will be inlined 1067 + */ 1068 + inline?: (string | RegExp)[] | true 1069 + /** 1070 + * Try to guess the CJS version of a package when it's invalid ESM 1071 + * @default false 1072 + */ 1073 + fallbackCJS?: boolean 1074 + } 1110 1075 1111 1076 export type ProjectConfig = Omit< 1112 1077 InlineConfig,
+1 -1
packages/vitest/src/node/types/coverage.ts
··· 59 59 } 60 60 61 61 export interface CoverageModuleLoader extends RuntimeCoverageModuleLoader { 62 - executeId: (id: string) => Promise<{ default: CoverageProviderModule }> 62 + import: (id: string) => Promise<{ default: CoverageProviderModule }> 63 63 } 64 64 65 65 export interface CoverageProviderModule extends RuntimeCoverageProviderModule {
+49
packages/vitest/src/runtime/moduleRunner/cachedResolver.ts
··· 1 + import type { WorkerGlobalState } from '../../types/worker' 2 + import { pathToFileURL } from 'node:url' 3 + import { join, normalize } from 'pathe' 4 + import { distDir } from '../../paths' 5 + 6 + const bareVitestRegexp = /^@?vitest(?:\/|$)/ 7 + const normalizedDistDir = normalize(distDir) 8 + const relativeIds: Record<string, string> = {} 9 + const externalizeMap = new Map<string, string>() 10 + 11 + // all Vitest imports always need to be externalized 12 + export function getCachedVitestImport( 13 + id: string, 14 + state: () => WorkerGlobalState, 15 + ): null | { externalize: string; type: 'module' } { 16 + if (id.startsWith('/@fs/') || id.startsWith('\\@fs\\')) { 17 + id = id.slice(process.platform === 'win32' ? 5 : 4) 18 + } 19 + 20 + if (externalizeMap.has(id)) { 21 + return { externalize: externalizeMap.get(id)!, type: 'module' } 22 + } 23 + // always externalize Vitest because we import from there before running tests 24 + // so we already have it cached by Node.js 25 + const root = state().config.root 26 + const relativeRoot = relativeIds[root] ?? (relativeIds[root] = normalizedDistDir.slice(root.length)) 27 + if (id.includes(distDir) || id.includes(normalizedDistDir)) { 28 + const externalize = id.startsWith('file://') 29 + ? id 30 + : pathToFileURL(id).toString() 31 + externalizeMap.set(id, externalize) 32 + return { externalize, type: 'module' } 33 + } 34 + if ( 35 + // "relative" to root path: 36 + // /node_modules/.pnpm/vitest/dist 37 + (relativeRoot && relativeRoot !== '/' && id.startsWith(relativeRoot)) 38 + ) { 39 + const path = join(root, id) 40 + const externalize = pathToFileURL(path).toString() 41 + externalizeMap.set(id, externalize) 42 + return { externalize, type: 'module' } 43 + } 44 + if (bareVitestRegexp.test(id)) { 45 + externalizeMap.set(id, id) 46 + return { externalize: id, type: 'module' } 47 + } 48 + return null 49 + }
+41
packages/vitest/src/runtime/moduleRunner/errorCatcher.ts
··· 1 + import type { WorkerGlobalState } from '../../types/worker' 2 + import { processError } from '@vitest/utils/error' 3 + 4 + const dispose: (() => void)[] = [] 5 + 6 + export function listenForErrors(state: () => WorkerGlobalState): void { 7 + dispose.forEach(fn => fn()) 8 + dispose.length = 0 9 + 10 + function catchError(err: unknown, type: string, event: 'uncaughtException' | 'unhandledRejection') { 11 + const worker = state() 12 + 13 + const listeners = process.listeners(event as 'uncaughtException') 14 + // if there is another listener, assume that it's handled by user code 15 + // one is Vitest's own listener 16 + if (listeners.length > 1) { 17 + return 18 + } 19 + 20 + const error = processError(err) 21 + if (typeof error === 'object' && error != null) { 22 + error.VITEST_TEST_NAME = worker.current?.type === 'test' ? worker.current.name : undefined 23 + if (worker.filepath) { 24 + error.VITEST_TEST_PATH = worker.filepath 25 + } 26 + error.VITEST_AFTER_ENV_TEARDOWN = worker.environmentTeardownRun 27 + } 28 + state().rpc.onUnhandledError(error, type) 29 + } 30 + 31 + const uncaughtException = (e: Error) => catchError(e, 'Uncaught Exception', 'uncaughtException') 32 + const unhandledRejection = (e: Error) => catchError(e, 'Unhandled Rejection', 'unhandledRejection') 33 + 34 + process.on('uncaughtException', uncaughtException) 35 + process.on('unhandledRejection', unhandledRejection) 36 + 37 + dispose.push(() => { 38 + process.off('uncaughtException', uncaughtException) 39 + process.off('unhandledRejection', unhandledRejection) 40 + }) 41 + }
+61
packages/vitest/src/runtime/moduleRunner/moduleDebug.ts
··· 1 + export type ModuleExecutionInfo = Map<string, ModuleExecutionInfoEntry> 2 + 3 + export interface ModuleExecutionInfoEntry { 4 + startOffset: number 5 + 6 + /** The duration that was spent executing the module. */ 7 + duration: number 8 + 9 + /** The time that was spent executing the module itself and externalized imports. */ 10 + selfTime: number 11 + } 12 + 13 + /** Stack to track nested module execution for self-time calculation. */ 14 + export type ExecutionStack = Array<{ 15 + /** The file that is being executed. */ 16 + filename: string 17 + 18 + /** The start time of this module's execution. */ 19 + startTime: number 20 + 21 + /** Accumulated time spent importing all sub-imports. */ 22 + subImportTime: number 23 + }> 24 + 25 + const performanceNow = performance.now.bind(performance) 26 + 27 + export class ModuleDebug { 28 + private executionStack: ExecutionStack = [] 29 + 30 + startCalculateModuleExecutionInfo(filename: string, startOffset: number): () => ModuleExecutionInfoEntry { 31 + const startTime = performanceNow() 32 + 33 + this.executionStack.push({ 34 + filename, 35 + startTime, 36 + subImportTime: 0, 37 + }) 38 + 39 + return () => { 40 + const duration = performanceNow() - startTime 41 + 42 + const currentExecution = this.executionStack.pop() 43 + 44 + if (currentExecution == null) { 45 + throw new Error('Execution stack is empty, this should never happen') 46 + } 47 + 48 + const selfTime = duration - currentExecution.subImportTime 49 + 50 + if (this.executionStack.length > 0) { 51 + this.executionStack.at(-1)!.subImportTime += duration 52 + } 53 + 54 + return { 55 + startOffset, 56 + duration, 57 + selfTime, 58 + } 59 + } 60 + } 61 + }
+496
packages/vitest/src/runtime/moduleRunner/moduleEvaluator.ts
··· 1 + import type { 2 + EvaluatedModuleNode, 3 + ModuleEvaluator, 4 + ModuleRunnerContext, 5 + ModuleRunnerImportMeta, 6 + } from 'vite/module-runner' 7 + import type { ModuleExecutionInfo } from './moduleDebug' 8 + import type { VitestVmOptions } from './moduleRunner' 9 + import { createRequire, isBuiltin } from 'node:module' 10 + import { pathToFileURL } from 'node:url' 11 + import vm from 'node:vm' 12 + import { isAbsolute } from 'pathe' 13 + import { 14 + ssrDynamicImportKey, 15 + ssrExportAllKey, 16 + ssrImportKey, 17 + ssrImportMetaKey, 18 + ssrModuleExportsKey, 19 + } from 'vite/module-runner' 20 + import { ModuleDebug } from './moduleDebug' 21 + 22 + const isWindows = process.platform === 'win32' 23 + 24 + export interface VitestModuleEvaluatorOptions { 25 + interopDefault?: boolean | undefined 26 + moduleExecutionInfo?: ModuleExecutionInfo 27 + getCurrentTestFilepath?: () => string | undefined 28 + compiledFunctionArgumentsNames?: string[] 29 + compiledFunctionArgumentsValues?: unknown[] 30 + } 31 + 32 + export class VitestModuleEvaluator implements ModuleEvaluator { 33 + public stubs: Record<string, any> = {} 34 + public env: ModuleRunnerImportMeta['env'] = createImportMetaEnvProxy() 35 + private vm: VitestVmOptions | undefined 36 + 37 + private compiledFunctionArgumentsNames?: string[] 38 + private compiledFunctionArgumentsValues: unknown[] = [] 39 + 40 + private primitives: { 41 + Object: typeof Object 42 + Proxy: typeof Proxy 43 + Reflect: typeof Reflect 44 + } 45 + 46 + private debug = new ModuleDebug() 47 + 48 + constructor( 49 + vmOptions?: VitestVmOptions | undefined, 50 + private options: VitestModuleEvaluatorOptions = {}, 51 + ) { 52 + this.vm = vmOptions 53 + this.stubs = getDefaultRequestStubs(vmOptions?.context) 54 + if (options.compiledFunctionArgumentsNames) { 55 + this.compiledFunctionArgumentsNames = options.compiledFunctionArgumentsNames 56 + } 57 + if (options.compiledFunctionArgumentsValues) { 58 + this.compiledFunctionArgumentsValues = options.compiledFunctionArgumentsValues 59 + } 60 + if (vmOptions) { 61 + this.primitives = vm.runInContext( 62 + '({ Object, Proxy, Reflect })', 63 + vmOptions.context, 64 + ) 65 + } 66 + else { 67 + this.primitives = { 68 + Object, 69 + Proxy, 70 + Reflect, 71 + } 72 + } 73 + } 74 + 75 + private convertIdToImportUrl(id: string) { 76 + // TODO: vitest returns paths for external modules, but Vite returns file:// 77 + // unfortunetly, there is a bug in Vite where ID is resolved incorrectly, so we can't return files until the fix is merged 78 + // https://github.com/vitejs/vite/pull/20449 79 + if (!isWindows || isBuiltin(id) || /^(?:node:|data:|http:|https:|file:)/.test(id)) { 80 + return id 81 + } 82 + const [filepath, query] = id.split('?') 83 + if (query) { 84 + return `${pathToFileURL(filepath).toString()}?${query}` 85 + } 86 + return pathToFileURL(filepath).toString() 87 + } 88 + 89 + async runExternalModule(id: string): Promise<any> { 90 + if (id in this.stubs) { 91 + return this.stubs[id] 92 + } 93 + 94 + const file = this.convertIdToImportUrl(id) 95 + 96 + const namespace = this.vm 97 + ? await this.vm.externalModulesExecutor.import(file) 98 + : await import(file) 99 + 100 + if (!this.shouldInterop(file, namespace)) { 101 + return namespace 102 + } 103 + 104 + const { mod, defaultExport } = interopModule(namespace) 105 + const { Proxy, Reflect } = this.primitives 106 + 107 + const proxy = new Proxy(mod, { 108 + get(mod, prop) { 109 + if (prop === 'default') { 110 + return defaultExport 111 + } 112 + return mod[prop] ?? defaultExport?.[prop] 113 + }, 114 + has(mod, prop) { 115 + if (prop === 'default') { 116 + return defaultExport !== undefined 117 + } 118 + return prop in mod || (defaultExport && prop in defaultExport) 119 + }, 120 + getOwnPropertyDescriptor(mod, prop) { 121 + const descriptor = Reflect.getOwnPropertyDescriptor(mod, prop) 122 + if (descriptor) { 123 + return descriptor 124 + } 125 + if (prop === 'default' && defaultExport !== undefined) { 126 + return { 127 + value: defaultExport, 128 + enumerable: true, 129 + configurable: true, 130 + } 131 + } 132 + }, 133 + }) 134 + return proxy 135 + } 136 + 137 + async runInlinedModule( 138 + context: ModuleRunnerContext, 139 + code: string, 140 + module: Readonly<EvaluatedModuleNode>, 141 + ): Promise<any> { 142 + context.__vite_ssr_import_meta__.env = this.env 143 + 144 + const { Reflect, Proxy, Object } = this.primitives 145 + 146 + const exportsObject = context[ssrModuleExportsKey] 147 + const SYMBOL_NOT_DEFINED = Symbol('not defined') 148 + let moduleExports: unknown = SYMBOL_NOT_DEFINED 149 + // this proxy is triggered only on exports.{name} and module.exports access 150 + // inside the module itself. imported module is always "exports" 151 + const cjsExports = new Proxy(exportsObject, { 152 + get: (target, p, receiver) => { 153 + if (Reflect.has(target, p)) { 154 + return Reflect.get(target, p, receiver) 155 + } 156 + return Reflect.get(Object.prototype, p, receiver) 157 + }, 158 + getPrototypeOf: () => Object.prototype, 159 + set: (_, p, value) => { 160 + // treat "module.exports =" the same as "exports.default =" to not have nested "default.default", 161 + // so "exports.default" becomes the actual module 162 + if ( 163 + p === 'default' 164 + && this.shouldInterop(module.file, { default: value }) 165 + && cjsExports !== value 166 + ) { 167 + exportAll(cjsExports, value) 168 + exportsObject.default = value 169 + return true 170 + } 171 + 172 + if (!Reflect.has(exportsObject, 'default')) { 173 + exportsObject.default = {} 174 + } 175 + 176 + // returns undefined, when accessing named exports, if default is not an object 177 + // but is still present inside hasOwnKeys, this is Node behaviour for CJS 178 + if ( 179 + moduleExports !== SYMBOL_NOT_DEFINED 180 + && isPrimitive(moduleExports) 181 + ) { 182 + defineExport(exportsObject, p, () => undefined) 183 + return true 184 + } 185 + 186 + if (!isPrimitive(exportsObject.default)) { 187 + exportsObject.default[p] = value 188 + } 189 + 190 + if (p !== 'default') { 191 + defineExport(exportsObject, p, () => value) 192 + } 193 + 194 + return true 195 + }, 196 + }) 197 + 198 + const moduleProxy = { 199 + set exports(value) { 200 + exportAll(cjsExports, value) 201 + exportsObject.default = value 202 + moduleExports = value 203 + }, 204 + get exports() { 205 + return cjsExports 206 + }, 207 + } 208 + 209 + const meta = context[ssrImportMetaKey] 210 + 211 + const testFilepath = this.options.getCurrentTestFilepath?.() 212 + if (testFilepath === module.file) { 213 + const globalNamespace = this.vm?.context || globalThis 214 + Object.defineProperty(meta, 'vitest', { 215 + // @ts-expect-error injected untyped global 216 + get: () => globalNamespace.__vitest_index__, 217 + }) 218 + } 219 + 220 + const filename = meta.filename 221 + const dirname = meta.dirname 222 + 223 + const require = this.createRequire(filename) 224 + 225 + const argumentsList = [ 226 + ssrModuleExportsKey, 227 + ssrImportMetaKey, 228 + ssrImportKey, 229 + ssrDynamicImportKey, 230 + ssrExportAllKey, 231 + // vite 7 support 232 + '__vite_ssr_exportName__', 233 + 234 + // TODO@discuss deprecate in Vitest 5, remove in Vitest 6(?) 235 + // backwards compat for vite-node 236 + '__filename', 237 + '__dirname', 238 + 'module', 239 + 'exports', 240 + 'require', 241 + ] 242 + 243 + if (this.compiledFunctionArgumentsNames) { 244 + argumentsList.push(...this.compiledFunctionArgumentsNames) 245 + } 246 + 247 + // add 'use strict' since ESM enables it by default 248 + const codeDefinition = `'use strict';async (${argumentsList.join( 249 + ',', 250 + )})=>{{` 251 + const wrappedCode = `${codeDefinition}${code}\n}}` 252 + const options = { 253 + // we are using a normalized file name by default because this is what 254 + // Vite expects in the source maps handler 255 + filename: module.file || filename, 256 + lineOffset: 0, 257 + columnOffset: -codeDefinition.length, 258 + } 259 + 260 + const finishModuleExecutionInfo = this.debug.startCalculateModuleExecutionInfo(filename, codeDefinition.length) 261 + 262 + try { 263 + const initModule = this.vm 264 + ? vm.runInContext(wrappedCode, this.vm.context, options) 265 + : vm.runInThisContext(wrappedCode, options) 266 + 267 + const dynamicRequest = async (dep: string, options: ImportCallOptions) => { 268 + dep = String(dep) 269 + // TODO: support more edge cases? 270 + // vite doesn't support dynamic modules by design, but we have to 271 + if (dep[0] === '#') { 272 + return context[ssrDynamicImportKey](wrapId(dep), options) 273 + } 274 + return context[ssrDynamicImportKey](dep, options) 275 + } 276 + 277 + await initModule( 278 + context[ssrModuleExportsKey], 279 + context[ssrImportMetaKey], 280 + context[ssrImportKey], 281 + dynamicRequest, 282 + context[ssrExportAllKey], 283 + // vite 7 support, remove when vite 7+ is supported 284 + (context as any).__vite_ssr_exportName__ 285 + || ((name: string, getter: () => unknown) => Object.defineProperty(exportsObject, name, { 286 + enumerable: true, 287 + configurable: true, 288 + get: getter, 289 + })), 290 + 291 + filename, 292 + dirname, 293 + moduleProxy, 294 + cjsExports, 295 + require, 296 + 297 + ...this.compiledFunctionArgumentsValues, 298 + ) 299 + } 300 + finally { 301 + // moduleExecutionInfo needs to use Node filename instead of the normalized one 302 + // because we rely on this behaviour in coverage-v8, for example 303 + this.options.moduleExecutionInfo?.set(filename, finishModuleExecutionInfo()) 304 + } 305 + } 306 + 307 + private createRequire(filename: string) { 308 + // \x00 is a rollup convention for virtual files, 309 + // it is not allowed in actual file names 310 + if (filename.startsWith('\x00') || !isAbsolute(filename)) { 311 + return () => ({}) 312 + } 313 + return this.vm 314 + ? this.vm.externalModulesExecutor.createRequire(filename) 315 + : createRequire(filename) 316 + } 317 + 318 + private shouldInterop(path: string, mod: any): boolean { 319 + if (this.options.interopDefault === false) { 320 + return false 321 + } 322 + // never interop ESM modules 323 + // TODO: should also skip for `.js` with `type="module"` 324 + return !path.endsWith('.mjs') && 'default' in mod 325 + } 326 + } 327 + 328 + export function createImportMetaEnvProxy(): ModuleRunnerImportMeta['env'] { 329 + // packages/vitest/src/node/plugins/index.ts:146 330 + const booleanKeys = ['DEV', 'PROD', 'SSR'] 331 + return new Proxy(process.env, { 332 + get(_, key) { 333 + if (typeof key !== 'string') { 334 + return undefined 335 + } 336 + if (booleanKeys.includes(key)) { 337 + return !!process.env[key] 338 + } 339 + return process.env[key] 340 + }, 341 + set(_, key, value) { 342 + if (typeof key !== 'string') { 343 + return true 344 + } 345 + 346 + if (booleanKeys.includes(key)) { 347 + process.env[key] = value ? '1' : '' 348 + } 349 + else { 350 + process.env[key] = value 351 + } 352 + 353 + return true 354 + }, 355 + }) as ModuleRunnerImportMeta['env'] 356 + } 357 + 358 + function updateStyle(id: string, css: string) { 359 + if (typeof document === 'undefined') { 360 + return 361 + } 362 + 363 + const element = document.querySelector(`[data-vite-dev-id="${id}"]`) 364 + if (element) { 365 + element.textContent = css 366 + return 367 + } 368 + 369 + const head = document.querySelector('head') 370 + const style = document.createElement('style') 371 + style.setAttribute('type', 'text/css') 372 + style.setAttribute('data-vite-dev-id', id) 373 + style.textContent = css 374 + head?.appendChild(style) 375 + } 376 + 377 + function removeStyle(id: string) { 378 + if (typeof document === 'undefined') { 379 + return 380 + } 381 + const sheet = document.querySelector(`[data-vite-dev-id="${id}"]`) 382 + if (sheet) { 383 + document.head.removeChild(sheet) 384 + } 385 + } 386 + 387 + const defaultClientStub = { 388 + injectQuery: (id: string) => id, 389 + createHotContext: () => { 390 + return { 391 + accept: () => {}, 392 + prune: () => {}, 393 + dispose: () => {}, 394 + decline: () => {}, 395 + invalidate: () => {}, 396 + on: () => {}, 397 + send: () => {}, 398 + } 399 + }, 400 + updateStyle: () => {}, 401 + removeStyle: () => {}, 402 + } 403 + 404 + export function getDefaultRequestStubs(context?: vm.Context): Record<string, any> { 405 + if (!context) { 406 + const clientStub = { 407 + ...defaultClientStub, 408 + updateStyle, 409 + removeStyle, 410 + } 411 + return { 412 + '/@vite/client': clientStub, 413 + } 414 + } 415 + const clientStub = vm.runInContext( 416 + `(defaultClient) => ({ ...defaultClient, updateStyle: ${updateStyle.toString()}, removeStyle: ${removeStyle.toString()} })`, 417 + context, 418 + )(defaultClientStub) 419 + return { 420 + '/@vite/client': clientStub, 421 + } 422 + } 423 + 424 + function exportAll(exports: any, sourceModule: any) { 425 + // #1120 when a module exports itself it causes 426 + // call stack error 427 + if (exports === sourceModule) { 428 + return 429 + } 430 + 431 + if ( 432 + isPrimitive(sourceModule) 433 + || Array.isArray(sourceModule) 434 + || sourceModule instanceof Promise 435 + ) { 436 + return 437 + } 438 + 439 + for (const key in sourceModule) { 440 + if (key !== 'default' && !(key in exports)) { 441 + try { 442 + defineExport(exports, key, () => sourceModule[key]) 443 + } 444 + catch {} 445 + } 446 + } 447 + } 448 + 449 + // keep consistency with Vite on how exports are defined 450 + function defineExport(exports: any, key: string | symbol, value: () => any) { 451 + Object.defineProperty(exports, key, { 452 + enumerable: true, 453 + configurable: true, 454 + get: value, 455 + }) 456 + } 457 + 458 + export function isPrimitive(v: any): boolean { 459 + const isObject = typeof v === 'object' || typeof v === 'function' 460 + return !isObject || v == null 461 + } 462 + 463 + function interopModule(mod: any) { 464 + if (isPrimitive(mod)) { 465 + return { 466 + mod: { default: mod }, 467 + defaultExport: mod, 468 + } 469 + } 470 + 471 + let defaultExport = 'default' in mod ? mod.default : mod 472 + 473 + if (!isPrimitive(defaultExport) && '__esModule' in defaultExport) { 474 + mod = defaultExport 475 + if ('default' in defaultExport) { 476 + defaultExport = defaultExport.default 477 + } 478 + } 479 + 480 + return { mod, defaultExport } 481 + } 482 + 483 + const VALID_ID_PREFIX = `/@id/` 484 + const NULL_BYTE_PLACEHOLDER = `__x00__` 485 + 486 + export function wrapId(id: string): string { 487 + return id.startsWith(VALID_ID_PREFIX) 488 + ? id 489 + : VALID_ID_PREFIX + id.replace('\0', NULL_BYTE_PLACEHOLDER) 490 + } 491 + 492 + export function unwrapId(id: string): string { 493 + return id.startsWith(VALID_ID_PREFIX) 494 + ? id.slice(VALID_ID_PREFIX.length).replace(NULL_BYTE_PLACEHOLDER, '\0') 495 + : id 496 + }
+544
packages/vitest/src/runtime/moduleRunner/moduleMocker.ts
··· 1 + import type { ManualMockedModule, MockedModule, MockedModuleType } from '@vitest/mocker' 2 + import type { EvaluatedModuleNode } from 'vite/module-runner' 3 + import type { MockFactory, MockOptions, PendingSuiteMock } from '../../types/mocker' 4 + import type { VitestModuleRunner } from './moduleRunner' 5 + import { isAbsolute, resolve } from 'node:path' 6 + import vm from 'node:vm' 7 + import { AutomockedModule, MockerRegistry, mockObject, RedirectedModule } from '@vitest/mocker' 8 + import { findMockRedirect } from '@vitest/mocker/redirect' 9 + import { highlight } from '@vitest/utils' 10 + import { distDir } from '../../paths' 11 + 12 + const spyModulePath = resolve(distDir, 'spy.js') 13 + 14 + interface MockContext { 15 + /** 16 + * When mocking with a factory, this refers to the module that imported the mock. 17 + */ 18 + callstack: null | string[] 19 + } 20 + 21 + export interface VitestMockerOptions { 22 + context?: vm.Context 23 + 24 + root: string 25 + moduleDirectories: string[] 26 + resolveId: (id: string, importer?: string) => Promise<{ 27 + id: string 28 + file: string 29 + url: string 30 + } | null> 31 + getCurrentTestFilepath: () => string | undefined 32 + } 33 + 34 + export class VitestMocker { 35 + static pendingIds: PendingSuiteMock[] = [] 36 + private spyModule?: typeof import('@vitest/spy') 37 + private primitives: { 38 + Object: typeof Object 39 + Function: typeof Function 40 + RegExp: typeof RegExp 41 + Array: typeof Array 42 + Map: typeof Map 43 + Error: typeof Error 44 + Symbol: typeof Symbol 45 + } 46 + 47 + private filterPublicKeys: (symbol | string)[] 48 + 49 + private registries = new Map<string, MockerRegistry>() 50 + 51 + private mockContext: MockContext = { 52 + callstack: null, 53 + } 54 + 55 + constructor(public moduleRunner: VitestModuleRunner, private options: VitestMockerOptions) { 56 + const context = this.options.context 57 + if (context) { 58 + this.primitives = vm.runInContext( 59 + '({ Object, Error, Function, RegExp, Symbol, Array, Map })', 60 + context, 61 + ) 62 + } 63 + else { 64 + this.primitives = { 65 + Object, 66 + Error, 67 + Function, 68 + RegExp, 69 + Symbol: globalThis.Symbol, 70 + Array, 71 + Map, 72 + } 73 + } 74 + 75 + const Symbol = this.primitives.Symbol 76 + 77 + this.filterPublicKeys = [ 78 + '__esModule', 79 + Symbol.asyncIterator, 80 + Symbol.hasInstance, 81 + Symbol.isConcatSpreadable, 82 + Symbol.iterator, 83 + Symbol.match, 84 + Symbol.matchAll, 85 + Symbol.replace, 86 + Symbol.search, 87 + Symbol.split, 88 + Symbol.species, 89 + Symbol.toPrimitive, 90 + Symbol.toStringTag, 91 + Symbol.unscopables, 92 + ] 93 + } 94 + 95 + private get root() { 96 + return this.options.root 97 + } 98 + 99 + private get evaluatedModules() { 100 + return this.moduleRunner.evaluatedModules 101 + } 102 + 103 + private get moduleDirectories() { 104 + return this.options.moduleDirectories || [] 105 + } 106 + 107 + public async initializeSpyModule(): Promise<void> { 108 + if (this.spyModule) { 109 + return 110 + } 111 + 112 + this.spyModule = await this.moduleRunner.import(spyModulePath) 113 + } 114 + 115 + private getMockerRegistry() { 116 + const suite = this.getSuiteFilepath() 117 + if (!this.registries.has(suite)) { 118 + this.registries.set(suite, new MockerRegistry()) 119 + } 120 + return this.registries.get(suite)! 121 + } 122 + 123 + public reset(): void { 124 + this.registries.clear() 125 + } 126 + 127 + private invalidateModuleById(id: string) { 128 + const mockId = this.getMockPath(id) 129 + const node = this.evaluatedModules.getModuleById(mockId) 130 + if (node) { 131 + this.evaluatedModules.invalidateModule(node) 132 + node.mockedExports = undefined 133 + } 134 + } 135 + 136 + private isModuleDirectory(path: string) { 137 + return this.moduleDirectories.some(dir => path.includes(dir)) 138 + } 139 + 140 + public getSuiteFilepath(): string { 141 + return this.options.getCurrentTestFilepath() || 'global' 142 + } 143 + 144 + private createError(message: string, codeFrame?: string) { 145 + const Error = this.primitives.Error 146 + const error = new Error(message) 147 + Object.assign(error, { codeFrame }) 148 + return error 149 + } 150 + 151 + public async resolveId(rawId: string, importer?: string): Promise<{ 152 + id: string 153 + url: string 154 + external: string | null 155 + }> { 156 + const result = await this.options.resolveId(rawId, importer) 157 + if (!result) { 158 + const id = normalizeModuleId(rawId) 159 + return { 160 + id, 161 + url: rawId, 162 + external: id, 163 + } 164 + } 165 + // external is node_module or unresolved module 166 + // for example, some people mock "vscode" and don't have it installed 167 + const external 168 + = !isAbsolute(result.file) || this.isModuleDirectory(result.file) ? normalizeModuleId(rawId) : null 169 + return { 170 + ...result, 171 + id: normalizeModuleId(result.id), 172 + external, 173 + } 174 + } 175 + 176 + public async resolveMocks(): Promise<void> { 177 + if (!VitestMocker.pendingIds.length) { 178 + return 179 + } 180 + 181 + await Promise.all( 182 + VitestMocker.pendingIds.map(async (mock) => { 183 + const { id, url, external } = await this.resolveId( 184 + mock.id, 185 + mock.importer, 186 + ) 187 + if (mock.action === 'unmock') { 188 + this.unmockPath(id) 189 + } 190 + if (mock.action === 'mock') { 191 + this.mockPath( 192 + mock.id, 193 + id, 194 + url, 195 + external, 196 + mock.type, 197 + mock.factory, 198 + ) 199 + } 200 + }), 201 + ) 202 + 203 + VitestMocker.pendingIds = [] 204 + } 205 + 206 + private ensureModule(id: string, url: string) { 207 + const node = this.evaluatedModules.ensureModule(id, url) 208 + // TODO 209 + node.meta = { id, url, code: '', file: null, invalidate: false } 210 + return node 211 + } 212 + 213 + private async callFunctionMock(id: string, url: string, mock: ManualMockedModule) { 214 + const node = this.ensureModule(id, url) 215 + if (node.exports) { 216 + return node.exports 217 + } 218 + const exports = await mock.resolve() 219 + 220 + const moduleExports = new Proxy(exports, { 221 + get: (target, prop) => { 222 + const val = target[prop] 223 + 224 + // 'then' can exist on non-Promise objects, need nested instanceof check for logic to work 225 + if (prop === 'then') { 226 + if (target instanceof Promise) { 227 + return target.then.bind(target) 228 + } 229 + } 230 + else if (!(prop in target)) { 231 + if (this.filterPublicKeys.includes(prop)) { 232 + return undefined 233 + } 234 + throw this.createError( 235 + `[vitest] No "${String(prop)}" export is defined on the "${mock.raw}" mock. ` 236 + + 'Did you forget to return it from "vi.mock"?' 237 + + '\nIf you need to partially mock a module, you can use "importOriginal" helper inside:\n', 238 + highlight(`vi.mock(import("${mock.raw}"), async (importOriginal) => { 239 + const actual = await importOriginal() 240 + return { 241 + ...actual, 242 + // your mocked methods 243 + } 244 + })`), 245 + ) 246 + } 247 + 248 + return val 249 + }, 250 + }) 251 + 252 + node.exports = moduleExports 253 + 254 + return moduleExports 255 + } 256 + 257 + // public method to avoid circular dependency 258 + public getMockContext(): MockContext { 259 + return this.mockContext 260 + } 261 + 262 + // path used to store mocked dependencies 263 + public getMockPath(dep: string) { 264 + return `mock:${dep}` 265 + } 266 + 267 + public getDependencyMock(id: string): MockedModule | undefined { 268 + const registry = this.getMockerRegistry() 269 + return registry.getById(fixLeadingSlashes(id)) 270 + } 271 + 272 + public findMockRedirect(mockPath: string, external: string | null): string | null { 273 + return findMockRedirect(this.root, mockPath, external) 274 + } 275 + 276 + public mockObject( 277 + object: Record<string | symbol, any>, 278 + mockExports: Record<string | symbol, any> = {}, 279 + behavior: MockedModuleType = 'automock', 280 + ): Record<string | symbol, any> { 281 + const spyOn = this.spyModule?.spyOn 282 + if (!spyOn) { 283 + throw this.createError( 284 + '[vitest] `spyModule` is not defined. This is a Vitest error. Please open a new issue with reproduction.', 285 + ) 286 + } 287 + return mockObject( 288 + { 289 + globalConstructors: this.primitives, 290 + spyOn, 291 + type: behavior, 292 + }, 293 + object, 294 + mockExports, 295 + ) 296 + } 297 + 298 + public unmockPath(id: string): void { 299 + const registry = this.getMockerRegistry() 300 + 301 + registry.deleteById(id) 302 + this.invalidateModuleById(id) 303 + } 304 + 305 + public mockPath( 306 + originalId: string, 307 + id: string, 308 + url: string, 309 + external: string | null, 310 + mockType: MockedModuleType | undefined, 311 + factory: MockFactory | undefined, 312 + ): void { 313 + const registry = this.getMockerRegistry() 314 + 315 + if (mockType === 'manual') { 316 + registry.register('manual', originalId, id, url, factory!) 317 + } 318 + else if (mockType === 'autospy') { 319 + registry.register('autospy', originalId, id, url) 320 + } 321 + else { 322 + const redirect = this.findMockRedirect(id, external) 323 + if (redirect) { 324 + registry.register('redirect', originalId, id, url, redirect) 325 + } 326 + else { 327 + registry.register('automock', originalId, id, url) 328 + } 329 + } 330 + 331 + // every time the mock is registered, we remove the previous one from the cache 332 + this.invalidateModuleById(id) 333 + } 334 + 335 + public async importActual<T>( 336 + rawId: string, 337 + importer: string, 338 + callstack?: string[] | null, 339 + ): Promise<T> { 340 + const { url } = await this.resolveId(rawId, importer) 341 + const node = await this.moduleRunner.fetchModule(url, importer) 342 + const result = await this.moduleRunner.cachedRequest( 343 + node.url, 344 + node, 345 + callstack || [importer], 346 + undefined, 347 + true, 348 + ) 349 + return result as T 350 + } 351 + 352 + public async importMock(rawId: string, importer: string): Promise<any> { 353 + const { id, url, external } = await this.resolveId(rawId, importer) 354 + 355 + let mock = this.getDependencyMock(id) 356 + 357 + if (!mock) { 358 + const redirect = this.findMockRedirect(id, external) 359 + if (redirect) { 360 + mock = new RedirectedModule(rawId, id, rawId, redirect) 361 + } 362 + else { 363 + mock = new AutomockedModule(rawId, id, rawId) 364 + } 365 + } 366 + 367 + if (mock.type === 'automock' || mock.type === 'autospy') { 368 + const node = await this.moduleRunner.fetchModule(url, importer) 369 + const mod = await this.moduleRunner.cachedRequest(url, node, [importer], undefined, true) 370 + const Object = this.primitives.Object 371 + return this.mockObject(mod, Object.create(Object.prototype), mock.type) 372 + } 373 + 374 + if (mock.type === 'manual') { 375 + return this.callFunctionMock(id, url, mock) 376 + } 377 + const node = await this.moduleRunner.fetchModule(mock.redirect) 378 + return this.moduleRunner.cachedRequest( 379 + mock.redirect, 380 + node, 381 + [importer], 382 + undefined, 383 + true, 384 + ) 385 + } 386 + 387 + public async requestWithMockedModule( 388 + url: string, 389 + evaluatedNode: EvaluatedModuleNode, 390 + callstack: string[], 391 + mock: MockedModule, 392 + ): Promise<any> { 393 + const mockId = this.getMockPath(evaluatedNode.id) 394 + 395 + if (mock.type === 'automock' || mock.type === 'autospy') { 396 + const cache = this.evaluatedModules.getModuleById(mockId) 397 + if (cache && cache.mockedExports) { 398 + return cache.mockedExports 399 + } 400 + const Object = this.primitives.Object 401 + // we have to define a separate object that will copy all properties into itself 402 + // and can't just use the same `exports` define automatically by Vite before the evaluator 403 + const exports = Object.create(null) 404 + Object.defineProperty(exports, Symbol.toStringTag, { 405 + value: 'Module', 406 + configurable: true, 407 + writable: true, 408 + }) 409 + const node = this.ensureModule(mockId, this.getMockPath(evaluatedNode.url)) 410 + node.meta = evaluatedNode.meta 411 + node.file = evaluatedNode.file 412 + node.mockedExports = exports 413 + 414 + const mod = await this.moduleRunner.cachedRequest( 415 + url, 416 + node, 417 + callstack, 418 + undefined, 419 + true, 420 + ) 421 + this.mockObject(mod, exports, mock.type) 422 + return exports 423 + } 424 + if ( 425 + mock.type === 'manual' 426 + && !callstack.includes(mockId) 427 + && !callstack.includes(url) 428 + ) { 429 + try { 430 + callstack.push(mockId) 431 + // this will not work if user does Promise.all(import(), import()) 432 + // we can also use AsyncLocalStorage to store callstack, but this won't work in the browser 433 + // maybe we should improve mock API in the future? 434 + this.mockContext.callstack = callstack 435 + return await this.callFunctionMock(mockId, this.getMockPath(url), mock) 436 + } 437 + finally { 438 + this.mockContext.callstack = null 439 + const indexMock = callstack.indexOf(mockId) 440 + callstack.splice(indexMock, 1) 441 + } 442 + } 443 + else if (mock.type === 'redirect' && !callstack.includes(mock.redirect)) { 444 + return mock.redirect 445 + } 446 + } 447 + 448 + public async mockedRequest(url: string, evaluatedNode: EvaluatedModuleNode, callstack: string[]): Promise<any> { 449 + const mock = this.getDependencyMock(evaluatedNode.id) 450 + 451 + if (!mock) { 452 + return 453 + } 454 + 455 + return this.requestWithMockedModule(url, evaluatedNode, callstack, mock) 456 + } 457 + 458 + public queueMock( 459 + id: string, 460 + importer: string, 461 + factoryOrOptions?: MockFactory | MockOptions, 462 + ): void { 463 + const mockType = getMockType(factoryOrOptions) 464 + VitestMocker.pendingIds.push({ 465 + action: 'mock', 466 + id, 467 + importer, 468 + factory: typeof factoryOrOptions === 'function' ? factoryOrOptions : undefined, 469 + type: mockType, 470 + }) 471 + } 472 + 473 + public queueUnmock(id: string, importer: string): void { 474 + VitestMocker.pendingIds.push({ 475 + action: 'unmock', 476 + id, 477 + importer, 478 + }) 479 + } 480 + } 481 + 482 + declare module 'vite/module-runner' { 483 + interface EvaluatedModuleNode { 484 + mockedExports?: Record<string, any> 485 + } 486 + } 487 + 488 + function getMockType(factoryOrOptions?: MockFactory | MockOptions): MockedModuleType { 489 + if (!factoryOrOptions) { 490 + return 'automock' 491 + } 492 + if (typeof factoryOrOptions === 'function') { 493 + return 'manual' 494 + } 495 + return factoryOrOptions.spy ? 'autospy' : 'automock' 496 + } 497 + 498 + // unique id that is not available as "$bare_import" like "test" 499 + // https://nodejs.org/api/modules.html#built-in-modules-with-mandatory-node-prefix 500 + const prefixedBuiltins = new Set([ 501 + 'node:sea', 502 + 'node:sqlite', 503 + 'node:test', 504 + 'node:test/reporters', 505 + ]) 506 + 507 + const isWindows = process.platform === 'win32' 508 + 509 + // transform file url to id 510 + // virtual:custom -> virtual:custom 511 + // \0custom -> \0custom 512 + // /root/id -> /id 513 + // /root/id.js -> /id.js 514 + // C:/root/id.js -> /id.js 515 + // C:\root\id.js -> /id.js 516 + // TODO: expose this in vite/module-runner 517 + function normalizeModuleId(file: string): string { 518 + if (prefixedBuiltins.has(file)) { 519 + return file 520 + } 521 + 522 + // unix style, but Windows path still starts with the drive letter to check the root 523 + const unixFile = slash(file) 524 + .replace(/^\/@fs\//, isWindows ? '' : '/') 525 + .replace(/^node:/, '') 526 + .replace(/^\/+/, '/') 527 + 528 + // if it's not in the root, keep it as a path, not a URL 529 + return unixFile.replace(/^file:\//, '/') 530 + } 531 + 532 + const windowsSlashRE = /\\/g 533 + function slash(p: string): string { 534 + return p.replace(windowsSlashRE, '/') 535 + } 536 + 537 + const multipleSlashRe = /^\/+/ 538 + // module-runner incorrectly replaces file:///path with `///path` 539 + function fixLeadingSlashes(id: string): string { 540 + if (id.startsWith('//')) { 541 + return id.replace(multipleSlashRe, '/') 542 + } 543 + return id 544 + }
+151
packages/vitest/src/runtime/moduleRunner/moduleRunner.ts
··· 1 + import type { MockedModule } from '@vitest/mocker' 2 + import type vm from 'node:vm' 3 + import type { EvaluatedModuleNode, EvaluatedModules, SSRImportMetadata } from 'vite/module-runner' 4 + import type { WorkerGlobalState } from '../../types/worker' 5 + import type { ExternalModulesExecutor } from '../external-executor' 6 + import type { ModuleExecutionInfo } from './moduleDebug' 7 + import type { VitestModuleEvaluator } from './moduleEvaluator' 8 + import type { VitestTransportOptions } from './moduleTransport' 9 + import { ModuleRunner } from 'vite/module-runner' 10 + import { VitestMocker } from './moduleMocker' 11 + import { VitestTransport } from './moduleTransport' 12 + 13 + // @ts-expect-error overriding private method 14 + export class VitestModuleRunner extends ModuleRunner { 15 + public mocker: VitestMocker 16 + public moduleExecutionInfo: ModuleExecutionInfo 17 + 18 + constructor(private options: VitestModuleRunnerOptions) { 19 + const transport = new VitestTransport(options.transport) 20 + const evaluatedModules = options.evaluatedModules 21 + super( 22 + { 23 + transport, 24 + hmr: false, 25 + evaluatedModules, 26 + sourcemapInterceptor: 'prepareStackTrace', 27 + }, 28 + options.evaluator, 29 + ) 30 + this.moduleExecutionInfo = options.getWorkerState().moduleExecutionInfo 31 + this.mocker = options.mocker || new VitestMocker(this, { 32 + context: options.vm?.context, 33 + resolveId: options.transport.resolveId, 34 + get root() { 35 + return options.getWorkerState().config.root 36 + }, 37 + get moduleDirectories() { 38 + return options.getWorkerState().config.deps.moduleDirectories || [] 39 + }, 40 + getCurrentTestFilepath() { 41 + return options.getWorkerState().filepath 42 + }, 43 + }) 44 + 45 + if (options.vm) { 46 + options.vm.context.__vitest_mocker__ = this.mocker 47 + } 48 + else { 49 + Object.defineProperty(globalThis, '__vitest_mocker__', { 50 + configurable: true, 51 + writable: true, 52 + value: this.mocker, 53 + }) 54 + } 55 + } 56 + 57 + public async import(rawId: string): Promise<any> { 58 + const resolved = await this.options.transport.resolveId(rawId) 59 + if (!resolved) { 60 + return super.import(rawId) 61 + } 62 + return super.import(resolved.url) 63 + } 64 + 65 + public async fetchModule(url: string, importer?: string): Promise<EvaluatedModuleNode> { 66 + const module = await (this as any).cachedModule(url, importer) 67 + return module 68 + } 69 + 70 + private _cachedRequest( 71 + url: string, 72 + module: EvaluatedModuleNode, 73 + callstack: string[] = [], 74 + metadata?: SSRImportMetadata, 75 + ) { 76 + // @ts-expect-error "cachedRequest" is private 77 + return super.cachedRequest(url, module, callstack, metadata) 78 + } 79 + 80 + /** 81 + * @internal 82 + */ 83 + public async cachedRequest( 84 + url: string, 85 + mod: EvaluatedModuleNode, 86 + callstack: string[] = [], 87 + metadata?: SSRImportMetadata, 88 + ignoreMock = false, 89 + ): Promise<any> { 90 + if (ignoreMock) { 91 + return this._cachedRequest(url, mod, callstack, metadata) 92 + } 93 + 94 + let mocked: any 95 + if (mod.meta && 'mockedModule' in mod.meta) { 96 + mocked = await this.mocker.requestWithMockedModule( 97 + url, 98 + mod, 99 + callstack, 100 + mod.meta.mockedModule as MockedModule, 101 + ) 102 + } 103 + else { 104 + mocked = await this.mocker.mockedRequest(url, mod, callstack) 105 + } 106 + 107 + if (typeof mocked === 'string') { 108 + const node = await this.fetchModule(mocked) 109 + return this._cachedRequest(mocked, node, callstack, metadata) 110 + } 111 + if (mocked != null && typeof mocked === 'object') { 112 + return mocked 113 + } 114 + return this._cachedRequest(url, mod, callstack, metadata) 115 + } 116 + 117 + /** @internal */ 118 + public _invalidateSubTreeById(ids: string[], invalidated = new Set<string>()): void { 119 + for (const id of ids) { 120 + if (invalidated.has(id)) { 121 + continue 122 + } 123 + const node = this.evaluatedModules.getModuleById(id) 124 + if (!node) { 125 + continue 126 + } 127 + invalidated.add(id) 128 + const subIds = Array.from(this.evaluatedModules.idToModuleMap) 129 + .filter(([, mod]) => mod.importers.has(id)) 130 + .map(([key]) => key) 131 + if (subIds.length) { 132 + this._invalidateSubTreeById(subIds, invalidated) 133 + } 134 + this.evaluatedModules.invalidateModule(node) 135 + } 136 + } 137 + } 138 + 139 + export interface VitestModuleRunnerOptions { 140 + transport: VitestTransportOptions 141 + evaluator: VitestModuleEvaluator 142 + evaluatedModules: EvaluatedModules 143 + getWorkerState: () => WorkerGlobalState 144 + mocker?: VitestMocker 145 + vm?: VitestVmOptions 146 + } 147 + 148 + export interface VitestVmOptions { 149 + context: vm.Context 150 + externalModulesExecutor: ExternalModulesExecutor 151 + }
+31
packages/vitest/src/runtime/moduleRunner/moduleTransport.ts
··· 1 + import type { FetchFunction, ModuleRunnerTransport } from 'vite/module-runner' 2 + import type { ResolveFunctionResult } from '../../types/general' 3 + 4 + export interface VitestTransportOptions { 5 + fetchModule: FetchFunction 6 + resolveId: (id: string, importer?: string) => Promise<ResolveFunctionResult | null> 7 + } 8 + 9 + export class VitestTransport implements ModuleRunnerTransport { 10 + constructor(private options: VitestTransportOptions) {} 11 + 12 + async invoke(event: any): Promise<{ result: any } | { error: any }> { 13 + if (event.type !== 'custom') { 14 + return { error: new Error(`Vitest Module Runner doesn't support Vite HMR events.`) } 15 + } 16 + if (event.event !== 'vite:invoke') { 17 + return { error: new Error(`Vitest Module Runner doesn't support ${event.event} event.`) } 18 + } 19 + const { name, data } = event.data 20 + if (name !== 'fetchModule') { 21 + return { error: new Error(`Unknown method: ${name}. Expected "fetchModule".`) } 22 + } 23 + try { 24 + const result = await this.options.fetchModule(...data as Parameters<FetchFunction>) 25 + return { result } 26 + } 27 + catch (error) { 28 + return { error } 29 + } 30 + } 31 + }
+179
packages/vitest/src/runtime/moduleRunner/startModuleRunner.ts
··· 1 + import type vm from 'node:vm' 2 + import type { EvaluatedModules } from 'vite/module-runner' 3 + import type { WorkerGlobalState } from '../../types/worker' 4 + import type { ExternalModulesExecutor } from '../external-executor' 5 + import fs from 'node:fs' 6 + import { isBuiltin } from 'node:module' 7 + import { isBareImport } from '@vitest/utils' 8 + import { getCachedVitestImport } from './cachedResolver' 9 + import { listenForErrors } from './errorCatcher' 10 + import { unwrapId, VitestModuleEvaluator } from './moduleEvaluator' 11 + import { VitestMocker } from './moduleMocker' 12 + import { VitestModuleRunner } from './moduleRunner' 13 + 14 + const { readFileSync } = fs 15 + 16 + const browserExternalId = '__vite-browser-external' 17 + const browserExternalLength = browserExternalId.length + 1 // 1 is ":" 18 + 19 + export const VITEST_VM_CONTEXT_SYMBOL: string = '__vitest_vm_context__' 20 + 21 + export interface ContextModuleRunnerOptions { 22 + evaluatedModules: EvaluatedModules 23 + mocker?: VitestMocker 24 + evaluator?: VitestModuleEvaluator 25 + context?: vm.Context 26 + externalModulesExecutor?: ExternalModulesExecutor 27 + state: WorkerGlobalState 28 + } 29 + 30 + const cwd = process.cwd() 31 + const isWindows = process.platform === 'win32' 32 + 33 + export async function startVitestModuleRunner(options: ContextModuleRunnerOptions): Promise<VitestModuleRunner> { 34 + const state = (): WorkerGlobalState => 35 + // @ts-expect-error injected untyped global 36 + globalThis.__vitest_worker__ || options.state 37 + const rpc = () => state().rpc 38 + 39 + process.exit = (code = process.exitCode || 0): never => { 40 + throw new Error(`process.exit unexpectedly called with "${code}"`) 41 + } 42 + 43 + listenForErrors(state) 44 + 45 + const environment = () => { 46 + const environment = state().environment 47 + return environment.viteEnvironment || environment.name 48 + } 49 + 50 + const vm = options.context && options.externalModulesExecutor 51 + ? { 52 + context: options.context, 53 + externalModulesExecutor: options.externalModulesExecutor, 54 + } 55 + : undefined 56 + 57 + const evaluator = options.evaluator || new VitestModuleEvaluator( 58 + vm, 59 + { 60 + get moduleExecutionInfo() { 61 + return state().moduleExecutionInfo 62 + }, 63 + get interopDefault() { 64 + return state().config.deps.interopDefault 65 + }, 66 + getCurrentTestFilepath: () => state().filepath, 67 + }, 68 + ) 69 + 70 + const moduleRunner: VitestModuleRunner = new VitestModuleRunner({ 71 + evaluatedModules: options.evaluatedModules, 72 + evaluator, 73 + mocker: options.mocker, 74 + transport: { 75 + async fetchModule(id, importer, options) { 76 + const resolvingModules = state().resolvingModules 77 + 78 + if (isWindows) { 79 + if (id[1] === ':') { 80 + // The drive letter is different for whatever reason, we need to normalize it to CWD 81 + if (id[0] !== cwd[0] && id[0].toUpperCase() === cwd[0].toUpperCase()) { 82 + const isUpperCase = cwd[0].toUpperCase() === cwd[0] 83 + id = (isUpperCase ? id[0].toUpperCase() : id[0].toLowerCase()) + id.slice(1) 84 + } 85 + // always mark absolute windows paths, otherwise Vite will externalize it 86 + id = `/@id/${id}` 87 + } 88 + } 89 + 90 + const vitest = getCachedVitestImport(id, state) 91 + if (vitest) { 92 + return vitest 93 + } 94 + 95 + const rawId = unwrapId(id) 96 + resolvingModules.add(rawId) 97 + 98 + try { 99 + if (VitestMocker.pendingIds.length) { 100 + await moduleRunner.mocker.resolveMocks() 101 + } 102 + 103 + const resolvedMock = moduleRunner.mocker.getDependencyMock(rawId) 104 + if (resolvedMock?.type === 'manual' || resolvedMock?.type === 'redirect') { 105 + return { 106 + code: '', 107 + file: null, 108 + id, 109 + url: id, 110 + invalidate: false, 111 + mockedModule: resolvedMock, 112 + } 113 + } 114 + 115 + if (isBuiltin(rawId) || rawId.startsWith(browserExternalId)) { 116 + return { externalize: toBuiltin(rawId), type: 'builtin' } 117 + } 118 + 119 + const result = await rpc().fetch( 120 + id, 121 + importer, 122 + environment(), 123 + options, 124 + ) 125 + if ('cached' in result) { 126 + const code = readFileSync(result.tmp, 'utf-8') 127 + return { code, ...result } 128 + } 129 + return result 130 + } 131 + catch (cause: any) { 132 + // rethrow vite error if it cannot load the module because it's not resolved 133 + if ( 134 + (typeof cause === 'object' && cause != null && cause.code === 'ERR_LOAD_URL') 135 + || (typeof cause?.message === 'string' && cause.message.includes('Failed to load url')) 136 + || (typeof cause?.message === 'string' && cause.message.startsWith('Cannot find module \'')) 137 + ) { 138 + const error = new Error( 139 + `Cannot find ${isBareImport(id) ? 'package' : 'module'} '${id}'${importer ? ` imported from '${importer}'` : ''}`, 140 + { cause }, 141 + ) as Error & { code: string } 142 + error.code = 'ERR_MODULE_NOT_FOUND' 143 + throw error 144 + } 145 + 146 + throw cause 147 + } 148 + finally { 149 + resolvingModules.delete(rawId) 150 + } 151 + }, 152 + resolveId(id, importer) { 153 + return rpc().resolve( 154 + id, 155 + importer, 156 + environment(), 157 + ) 158 + }, 159 + }, 160 + getWorkerState: state, 161 + vm, 162 + }) 163 + 164 + await moduleRunner.import('/@vite/env') 165 + await moduleRunner.mocker.initializeSpyModule() 166 + 167 + return moduleRunner 168 + } 169 + 170 + export function toBuiltin(id: string): string { 171 + if (id.startsWith(browserExternalId)) { 172 + id = id.slice(browserExternalLength) 173 + } 174 + 175 + if (!id.startsWith('node:')) { 176 + id = `node:${id}` 177 + } 178 + return id 179 + }
+7 -3
packages/vitest/src/runtime/runners/benchmark.ts
··· 5 5 VitestRunner, 6 6 VitestRunnerImportSource, 7 7 } from '@vitest/runner' 8 + import type { ModuleRunner } from 'vite/module-runner' 8 9 import type { SerializedConfig } from '../config' 9 10 // import type { VitestExecutor } from '../execute' 10 11 import type { ··· 150 151 } 151 152 152 153 export class NodeBenchmarkRunner implements VitestRunner { 153 - private __vitest_executor!: any 154 + private moduleRunner!: ModuleRunner 154 155 155 156 constructor(public config: SerializedConfig) {} 156 157 ··· 160 161 161 162 importFile(filepath: string, source: VitestRunnerImportSource): unknown { 162 163 if (source === 'setup') { 163 - getWorkerState().moduleCache.delete(filepath) 164 + const moduleNode = getWorkerState().evaluatedModules.getModuleById(filepath) 165 + if (moduleNode) { 166 + getWorkerState().evaluatedModules.invalidateModule(moduleNode) 167 + } 164 168 } 165 - return this.__vitest_executor.executeId(filepath) 169 + return this.moduleRunner.import(filepath) 166 170 } 167 171 168 172 async runSuite(suite: Suite): Promise<void> {
+15 -14
packages/vitest/src/runtime/runners/index.ts
··· 1 1 import type { VitestRunner, VitestRunnerConstructor } from '@vitest/runner' 2 2 import type { SerializedConfig } from '../config' 3 - import type { VitestExecutor } from '../execute' 4 - import { resolve } from 'node:path' 3 + import type { VitestModuleRunner } from '../moduleRunner/moduleRunner' 4 + import { join, resolve } from 'node:path' 5 5 import { takeCoverageInsideWorker } from '../../integrations/coverage' 6 6 import { distDir } from '../../paths' 7 7 import { rpc } from '../rpc' ··· 12 12 13 13 async function getTestRunnerConstructor( 14 14 config: SerializedConfig, 15 - executor: VitestExecutor, 15 + moduleRunner: VitestModuleRunner, 16 16 ): Promise<VitestRunnerConstructor> { 17 17 if (!config.runner) { 18 - const { VitestTestRunner, NodeBenchmarkRunner } 19 - = await executor.executeFile(runnersFile) 18 + const { VitestTestRunner, NodeBenchmarkRunner } = await moduleRunner.import( 19 + join('/@fs/', runnersFile), 20 + ) 20 21 return ( 21 22 config.mode === 'test' ? VitestTestRunner : NodeBenchmarkRunner 22 23 ) as VitestRunnerConstructor 23 24 } 24 - const mod = await executor.executeId(config.runner) 25 + const mod = await moduleRunner.import(config.runner) 25 26 if (!mod.default && typeof mod.default !== 'function') { 26 27 throw new Error( 27 28 `Runner must export a default function, but got ${typeof mod.default} imported from ${ ··· 34 35 35 36 export async function resolveTestRunner( 36 37 config: SerializedConfig, 37 - executor: VitestExecutor, 38 + moduleRunner: VitestModuleRunner, 38 39 ): Promise<VitestRunner> { 39 - const TestRunner = await getTestRunnerConstructor(config, executor) 40 + const TestRunner = await getTestRunnerConstructor(config, moduleRunner) 40 41 const testRunner = new TestRunner(config) 41 42 42 43 // inject private executor to every runner 43 - Object.defineProperty(testRunner, '__vitest_executor', { 44 - value: executor, 44 + Object.defineProperty(testRunner, 'moduleRunner', { 45 + value: moduleRunner, 45 46 enumerable: false, 46 47 configurable: false, 47 48 }) ··· 55 56 } 56 57 57 58 const [diffOptions] = await Promise.all([ 58 - loadDiffConfig(config, executor), 59 - loadSnapshotSerializers(config, executor), 59 + loadDiffConfig(config, moduleRunner), 60 + loadSnapshotSerializers(config, moduleRunner), 60 61 ]) 61 62 testRunner.config.diffOptions = diffOptions 62 63 ··· 100 101 const originalOnAfterRun = testRunner.onAfterRunFiles 101 102 testRunner.onAfterRunFiles = async (files) => { 102 103 const state = getWorkerState() 103 - const coverage = await takeCoverageInsideWorker(config.coverage, executor) 104 + const coverage = await takeCoverageInsideWorker(config.coverage, moduleRunner) 104 105 105 106 if (coverage) { 106 107 rpc().onAfterSuiteRun({ 107 108 coverage, 108 109 testFiles: files.map(file => file.name).sort(), 109 - transformMode: state.environment.transformMode, 110 + environment: state.environment.viteEnvironment || state.environment.name, 110 111 projectName: state.ctx.projectName, 111 112 }) 112 113 }
+7 -3
packages/vitest/src/runtime/runners/test.ts
··· 10 10 VitestRunner, 11 11 VitestRunnerImportSource, 12 12 } from '@vitest/runner' 13 + import type { ModuleRunner } from 'vite/module-runner' 13 14 import type { SerializedConfig } from '../config' 14 15 // import type { VitestExecutor } from '../execute' 15 16 import { getState, GLOBAL_EXPECT, setState } from '@vitest/expect' ··· 29 30 export class VitestTestRunner implements VitestRunner { 30 31 private snapshotClient = getSnapshotClient() 31 32 private workerState = getWorkerState() 32 - private __vitest_executor!: any 33 + private moduleRunner!: ModuleRunner 33 34 private cancelRun = false 34 35 35 36 private assertionsErrors = new WeakMap<Readonly<Task>, Error>() ··· 40 41 41 42 importFile(filepath: string, source: VitestRunnerImportSource): unknown { 42 43 if (source === 'setup') { 43 - this.workerState.moduleCache.delete(filepath) 44 + const moduleNode = this.workerState.evaluatedModules.getModuleById(filepath) 45 + if (moduleNode) { 46 + this.workerState.evaluatedModules.invalidateModule(moduleNode) 47 + } 44 48 } 45 - return this.__vitest_executor.executeId(filepath) 49 + return this.moduleRunner.import(filepath) 46 50 } 47 51 48 52 onCollectStart(file: File): void {
+3 -4
packages/vitest/src/runtime/vm/commonjs-executor.ts
··· 1 1 import type { FileMap } from './file-map' 2 2 import type { ImportModuleDynamically, VMSyntheticModule } from './types' 3 - import { Module as _Module, createRequire } from 'node:module' 3 + import { Module as _Module, createRequire, isBuiltin } from 'node:module' 4 4 import vm from 'node:vm' 5 5 import { basename, dirname, extname } from 'pathe' 6 - import { isNodeBuiltin } from 'vite-node/utils' 7 6 import { interopCommonJsModule, SyntheticModule } from './utils' 8 7 9 8 interface CommonjsExecutorOptions { ··· 206 205 const require = ((id: string) => { 207 206 const resolved = _require.resolve(id) 208 207 const ext = extname(resolved) 209 - if (ext === '.node' || isNodeBuiltin(resolved)) { 208 + if (ext === '.node' || isBuiltin(resolved)) { 210 209 return this.requireCoreModule(resolved) 211 210 } 212 211 const module = new this.Module(resolved) ··· 358 357 359 358 public require(identifier: string): any { 360 359 const ext = extname(identifier) 361 - if (ext === '.node' || isNodeBuiltin(identifier)) { 360 + if (ext === '.node' || isBuiltin(identifier)) { 362 361 return this.requireCoreModule(identifier) 363 362 } 364 363 const module = new this.Module(identifier)
+5 -1
packages/vitest/src/runtime/vm/utils.ts
··· 1 1 import type { VMSourceTextModule, VMSyntheticModule } from './types' 2 2 import vm from 'node:vm' 3 - import { isPrimitive } from 'vite-node/utils' 4 3 5 4 export function interopCommonJsModule( 6 5 interopDefault: boolean | undefined, ··· 43 42 moduleExports: mod, 44 43 defaultExport: mod, 45 44 } 45 + } 46 + 47 + function isPrimitive(obj: unknown): boolean { 48 + const isObject = obj != null && (typeof obj === 'object' || typeof obj === 'function') 49 + return !isObject 46 50 } 47 51 48 52 export const SyntheticModule: typeof VMSyntheticModule = (vm as any)
+5 -29
packages/vitest/src/runtime/vm/vite-executor.ts
··· 4 4 import type { EsmExecutor } from './esm-executor' 5 5 import type { VMModule } from './types' 6 6 import { pathToFileURL } from 'node:url' 7 - import { normalize } from 'pathe' 8 - import { CSS_LANGS_RE, KNOWN_ASSET_RE } from 'vite-node/constants' 9 - import { toArray } from 'vite-node/utils' 7 + import { CSS_LANGS_RE, KNOWN_ASSET_RE, toArray } from '@vitest/utils' 10 8 import { SyntheticModule } from './utils' 11 9 12 10 interface ViteExecutorOptions { ··· 26 24 this.esm = options.esmExecutor 27 25 } 28 26 29 - public resolve = (identifier: string, parent: string): string | undefined => { 27 + public resolve = (identifier: string): string | undefined => { 30 28 if (identifier === CLIENT_ID) { 31 - if (this.workerState.environment.transformMode === 'web') { 32 - return identifier 33 - } 34 - const packageName = this.getPackageName(parent) 35 - throw new Error( 36 - `[vitest] Vitest cannot handle ${CLIENT_ID} imported in ${parent} when running in SSR environment. Add "${packageName}" to "ssr.noExternal" if you are using Vite SSR, or to "server.deps.inline" if you are using Vite Node.`, 37 - ) 29 + return identifier 38 30 } 39 31 } 40 32 ··· 42 34 return this.options.context.__vitest_worker__ 43 35 } 44 36 45 - private getPackageName(modulePath: string) { 46 - const path = normalize(modulePath) 47 - let name = path.split('/node_modules/').pop() || '' 48 - if (name?.startsWith('@')) { 49 - name = name.split('/').slice(0, 2).join('/') 50 - } 51 - else { 52 - name = name.split('/')[0] 53 - } 54 - return name 55 - } 56 - 57 37 public async createViteModule(fileUrl: string): Promise<VMModule> { 58 - if (fileUrl === CLIENT_FILE) { 38 + if (fileUrl === CLIENT_FILE || fileUrl === CLIENT_ID) { 59 39 return this.createViteClientModule() 60 40 } 61 41 const cached = this.esm.resolveCachedModule(fileUrl) ··· 64 44 } 65 45 return this.esm.createEsModule(fileUrl, async () => { 66 46 try { 67 - const result = await this.options.transform(fileUrl, 'web') 47 + const result = await this.options.transform(fileUrl) 68 48 if (result.code) { 69 49 return result.code 70 50 } ··· 112 92 } 113 93 114 94 public canResolve = (fileUrl: string): boolean => { 115 - const transformMode = this.workerState.environment.transformMode 116 - if (transformMode !== 'web') { 117 - return false 118 - } 119 95 if (fileUrl === CLIENT_FILE) { 120 96 return true 121 97 }
+27 -18
packages/vitest/src/runtime/workers/base.ts
··· 1 1 import type { WorkerGlobalState } from '../../types/worker' 2 - import type { ContextExecutorOptions, VitestExecutor } from '../execute' 3 - import { ModuleCacheMap } from 'vite-node/client' 4 - import { getDefaultRequestStubs, startVitestExecutor } from '../execute' 2 + import type { VitestModuleRunner } from '../moduleRunner/moduleRunner' 3 + import type { ContextModuleRunnerOptions } from '../moduleRunner/startModuleRunner' 4 + import { EvaluatedModules } from 'vite/module-runner' 5 + import { startVitestModuleRunner } from '../moduleRunner/startModuleRunner' 5 6 import { provideWorkerState } from '../utils' 6 7 7 - let _viteNode: VitestExecutor 8 + let _moduleRunner: VitestModuleRunner 8 9 9 - const moduleCache = new ModuleCacheMap() 10 + const evaluatedModules = new EvaluatedModules() 10 11 const moduleExecutionInfo = new Map() 11 12 12 - async function startViteNode(options: ContextExecutorOptions) { 13 - if (_viteNode) { 14 - return _viteNode 13 + async function startModuleRunner(options: ContextModuleRunnerOptions) { 14 + if (_moduleRunner) { 15 + return _moduleRunner 15 16 } 16 17 17 - _viteNode = await startVitestExecutor(options) 18 - return _viteNode 18 + _moduleRunner = await startVitestModuleRunner(options) 19 + return _moduleRunner 19 20 } 20 21 21 22 export async function runBaseTests(method: 'run' | 'collect', state: WorkerGlobalState): Promise<void> { 22 23 const { ctx } = state 23 24 // state has new context, but we want to reuse existing ones 24 - state.moduleCache = moduleCache 25 + state.evaluatedModules = evaluatedModules 25 26 state.moduleExecutionInfo = moduleExecutionInfo 26 27 27 28 provideWorkerState(globalThis, state) 28 29 29 30 if (ctx.invalidates) { 30 - ctx.invalidates.forEach((fsPath) => { 31 - moduleCache.delete(fsPath) 32 - moduleCache.delete(`mock:${fsPath}`) 31 + ctx.invalidates.forEach((filepath) => { 32 + const modules = state.evaluatedModules.fileToModulesMap.get(filepath) || [] 33 + modules.forEach((module) => { 34 + state.evaluatedModules.invalidateModule(module) 35 + }) 36 + // evaluatedModules.delete(fsPath) 37 + // evaluatedModules.delete(`mock:${fsPath}`) 33 38 }) 34 39 } 35 - ctx.files.forEach(i => state.moduleCache.delete( 36 - typeof i === 'string' ? i : i.filepath, 37 - )) 40 + ctx.files.forEach((i) => { 41 + const filepath = typeof i === 'string' ? i : i.filepath 42 + const modules = state.evaluatedModules.fileToModulesMap.get(filepath) || [] 43 + modules.forEach((module) => { 44 + state.evaluatedModules.invalidateModule(module) 45 + }) 46 + }) 38 47 39 48 const [executor, { run }] = await Promise.all([ 40 - startViteNode({ state, requestStubs: getDefaultRequestStubs() }), 49 + startModuleRunner({ state, evaluatedModules: state.evaluatedModules }), 41 50 import('../runBaseTests'), 42 51 ]) 43 52 const fileSpecs = ctx.files.map(f =>
+16 -8
packages/vitest/src/runtime/workers/vm.ts
··· 1 1 import type { Context } from 'node:vm' 2 - import type { ModuleCacheMap } from 'vite-node' 3 2 import type { WorkerGlobalState } from '../../types/worker' 4 3 import { pathToFileURL } from 'node:url' 5 4 import { isContext } from 'node:vm' 6 5 import { resolve } from 'pathe' 7 6 import { distDir } from '../../paths' 8 7 import { createCustomConsole } from '../console' 9 - import { getDefaultRequestStubs, startVitestExecutor } from '../execute' 10 8 import { ExternalModulesExecutor } from '../external-executor' 9 + import { getDefaultRequestStubs } from '../moduleRunner/moduleEvaluator' 10 + import { startVitestModuleRunner, VITEST_VM_CONTEXT_SYMBOL } from '../moduleRunner/startModuleRunner' 11 11 import { provideWorkerState } from '../utils' 12 12 import { FileMap } from '../vm/file-map' 13 13 ··· 75 75 viteClientModule: stubs['/@vite/client'], 76 76 }) 77 77 78 - const executor = await startVitestExecutor({ 78 + const moduleRunner = await startVitestModuleRunner({ 79 79 context, 80 - moduleCache: state.moduleCache as ModuleCacheMap, 80 + evaluatedModules: state.evaluatedModules, 81 81 state, 82 82 externalModulesExecutor, 83 - requestStubs: stubs, 84 83 }) 85 84 86 - context.__vitest_mocker__ = executor.mocker 85 + Object.defineProperty(context, VITEST_VM_CONTEXT_SYMBOL, { 86 + value: { 87 + context, 88 + externalModulesExecutor, 89 + }, 90 + configurable: true, 91 + enumerable: false, 92 + writable: false, 93 + }) 94 + context.__vitest_mocker__ = moduleRunner.mocker 87 95 88 - const { run } = (await executor.importExternalModule( 96 + const { run } = (await moduleRunner.import( 89 97 entryFile, 90 98 )) as typeof import('../runVmTests') 91 99 const fileSpecs = ctx.files.map(f => ··· 99 107 method, 100 108 fileSpecs, 101 109 ctx.config, 102 - executor, 110 + moduleRunner, 103 111 ) 104 112 } 105 113 finally {
+1 -1
test/cli/fixtures/config-loader/browser/vitest.config.ts
··· 1 1 import { defineConfig } from "vitest/config" 2 - import "@vitest/test-dep-linked/ts"; 2 + import "@test/test-dep-linked/ts"; 3 3 4 4 export default defineConfig({ 5 5 test: {
+1 -1
test/cli/fixtures/config-loader/node/vitest.config.ts
··· 1 1 import { defineConfig } from "vitest/config" 2 - import "@vitest/test-dep-linked/ts"; 2 + import "@test/test-dep-linked/ts"; 3 3 4 4 export default defineConfig({})
+3 -10
test/cli/fixtures/custom-pool/pool/custom-pool.ts
··· 1 - import type { 2 - RunnerTestFile, 3 - RunnerTestCase, 4 - RunnerTestSuite, 5 - RunnerTaskResultPack, 6 - RunnerTaskEventPack, 7 - RunnerTask 8 - } from 'vitest' 1 + import type { RunnerTestCase } from 'vitest' 9 2 import type { ProcessPool, Vitest } from 'vitest/node' 10 - import { createFileTask, generateFileHash } from '@vitest/runner/utils' 11 - import { normalize, relative } from 'pathe' 3 + import { createFileTask } from '@vitest/runner/utils' 4 + import { normalize } from 'pathe' 12 5 13 6 export default (vitest: Vitest): ProcessPool => { 14 7 const options = vitest.config.poolOptions?.custom as any
+3 -3
packages/vitest/src/integrations/snapshot/environments/resolveSnapshotEnvironment.ts
··· 1 1 import type { SnapshotEnvironment } from '@vitest/snapshot/environment' 2 2 import type { SerializedConfig } from '../../../runtime/config' 3 - import type { VitestExecutor } from '../../../runtime/execute' 3 + import type { VitestModuleRunner } from '../../../runtime/moduleRunner/moduleRunner' 4 4 5 5 export async function resolveSnapshotEnvironment( 6 6 config: SerializedConfig, 7 - executor: VitestExecutor, 7 + executor: VitestModuleRunner, 8 8 ): Promise<SnapshotEnvironment> { 9 9 if (!config.snapshotEnvironment) { 10 10 const { VitestNodeSnapshotEnvironment } = await import('./node') 11 11 return new VitestNodeSnapshotEnvironment() 12 12 } 13 13 14 - const mod = await executor.executeId(config.snapshotEnvironment) 14 + const mod = await executor.import(config.snapshotEnvironment) 15 15 if (typeof mod.default !== 'object' || !mod.default) { 16 16 throw new Error( 17 17 'Snapshot environment module must have a default export object with a shape of `SnapshotEnvironment`',