From 4bb6db890d39c1f44f21d3e33412b2cf106d83f2 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 19 Dec 2024 18:00:32 +0100 Subject: [PATCH 01/11] chore: strict node env --- .vscode/settings.json | 3 +- packages/utils/src/source-map.ts | 36 +++++++++---------- packages/vitest/rollup.config.js | 1 + .../vitest/src/node/config/serializeConfig.ts | 1 + packages/vitest/src/node/pools/threads.ts | 2 +- packages/vitest/src/node/types/config.ts | 2 ++ packages/vitest/src/runtime/config.ts | 1 + packages/vitest/src/runtime/worker.ts | 2 ++ packages/vitest/src/runtime/workers/base.ts | 4 +-- .../vitest/src/runtime/workers/strictNode.ts | 32 +++++++++++++++++ pnpm-lock.yaml | 3 ++ test/core/package.json | 1 + test/core/test/basic.test.ts | 2 +- test/core/test/setup.js | 7 ++++ test/core/test/setup.ts | 3 -- test/core/vite.config.ts | 9 ++++- 16 files changed, 82 insertions(+), 27 deletions(-) create mode 100644 packages/vitest/src/runtime/workers/strictNode.ts create mode 100644 test/core/test/setup.js delete mode 100644 test/core/test/setup.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 8da1bd8263a0..f7ee9a14c200 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -45,5 +45,6 @@ "json", "jsonc", "yaml" - ] + ], + "testing.automaticallyOpenTestResults": "neverOpen" } diff --git a/packages/utils/src/source-map.ts b/packages/utils/src/source-map.ts index 0ca0382536ce..4033cc393522 100644 --- a/packages/utils/src/source-map.ts +++ b/packages/utils/src/source-map.ts @@ -22,25 +22,25 @@ const CHROME_IE_STACK_REGEXP = /^\s*at .*(?:\S:\d+|\(native\))/m const SAFARI_NATIVE_CODE_REGEXP = /^(?:eval@)?(?:\[native code\])?$/ const stackIgnorePatterns = [ - 'node:internal', - /\/packages\/\w+\/dist\//, - /\/@vitest\/\w+\/dist\//, - '/vitest/dist/', - '/vitest/src/', - '/vite-node/dist/', - '/vite-node/src/', - '/node_modules/chai/', - '/node_modules/tinypool/', - '/node_modules/tinyspy/', - // browser related deps - '/deps/chunk-', - '/deps/@vitest', - '/deps/loupe', + // 'node:internal', + // /\/packages\/\w+\/dist\//, + // /\/@vitest\/\w+\/dist\//, + // '/vitest/dist/', + // '/vitest/src/', + // '/vite-node/dist/', + // '/vite-node/src/', + // '/node_modules/chai/', + // '/node_modules/tinypool/', + // '/node_modules/tinyspy/', + // // browser related deps + // '/deps/chunk-', + // '/deps/@vitest', + // '/deps/loupe', '/deps/chai', - /node:\w+/, - /__vitest_test__/, - /__vitest_browser__/, - /\/deps\/vitest_/, + // /node:\w+/, + // /__vitest_test__/, + // /__vitest_browser__/, + // /\/deps\/vitest_/, ] function extractLocation(urlLike: string) { diff --git a/packages/vitest/rollup.config.js b/packages/vitest/rollup.config.js index 5218a10769e0..e604306c53ab 100644 --- a/packages/vitest/rollup.config.js +++ b/packages/vitest/rollup.config.js @@ -39,6 +39,7 @@ const entries = { 'workers/threads': 'src/runtime/workers/threads.ts', 'workers/vmThreads': 'src/runtime/workers/vmThreads.ts', 'workers/vmForks': 'src/runtime/workers/vmForks.ts', + 'workers/strictNode': 'src/runtime/workers/strictNode.ts', 'workers/runVmTests': 'src/runtime/runVmTests.ts', diff --git a/packages/vitest/src/node/config/serializeConfig.ts b/packages/vitest/src/node/config/serializeConfig.ts index 234e67a3be34..54db5d7ae98d 100644 --- a/packages/vitest/src/node/config/serializeConfig.ts +++ b/packages/vitest/src/node/config/serializeConfig.ts @@ -21,6 +21,7 @@ export function serializeConfig( logHeapUsage: config.logHeapUsage, runner: config.runner, bail: config.bail, + preloads: config.preloads || [], defines: config.defines, chaiConfig: config.chaiConfig, setupFiles: config.setupFiles, diff --git a/packages/vitest/src/node/pools/threads.ts b/packages/vitest/src/node/pools/threads.ts index e800ee14d0a4..32f4b4c82951 100644 --- a/packages/vitest/src/node/pools/threads.ts +++ b/packages/vitest/src/node/pools/threads.ts @@ -59,7 +59,7 @@ export function createThreadsPool( const minThreads = poolOptions.minThreads ?? ctx.config.minWorkers ?? threadsCount - const worker = resolve(ctx.distPath, 'workers/threads.js') + const worker = resolve(ctx.distPath, 'workers/strictNode.js') const options: TinypoolOptions = { filename: resolve(ctx.distPath, 'worker.js'), diff --git a/packages/vitest/src/node/types/config.ts b/packages/vitest/src/node/types/config.ts index 6e3ddbd85302..1313cf7855e1 100644 --- a/packages/vitest/src/node/types/config.ts +++ b/packages/vitest/src/node/types/config.ts @@ -470,6 +470,8 @@ export interface InlineConfig { */ setupFiles?: string | string[] + preloads?: string[] + /** * Path to global setup files */ diff --git a/packages/vitest/src/runtime/config.ts b/packages/vitest/src/runtime/config.ts index ef9b8d0b4f56..6c997ed7d15c 100644 --- a/packages/vitest/src/runtime/config.ts +++ b/packages/vitest/src/runtime/config.ts @@ -18,6 +18,7 @@ export interface SerializedConfig { isolate: boolean mode: 'test' | 'benchmark' bail: number | undefined + preloads: string[] environmentOptions?: Record root: string setupFiles: string[] diff --git a/packages/vitest/src/runtime/worker.ts b/packages/vitest/src/runtime/worker.ts index 4ccfa155e408..7a87627bd1da 100644 --- a/packages/vitest/src/runtime/worker.ts +++ b/packages/vitest/src/runtime/worker.ts @@ -41,6 +41,8 @@ async function execute(method: 'run' | 'collect', ctx: ContextRPC) { process.env.VITEST_POOL_ID = String(poolId) try { + await Promise.all(ctx.config.preloads?.map(file => import(file))) + // worker is a filepath or URL to a file that exposes a default export with "getRpcOptions" and "runTests" methods if (ctx.worker[0] === '.') { throw new Error( diff --git a/packages/vitest/src/runtime/workers/base.ts b/packages/vitest/src/runtime/workers/base.ts index a8824ef8cdd3..469ff1b1a648 100644 --- a/packages/vitest/src/runtime/workers/base.ts +++ b/packages/vitest/src/runtime/workers/base.ts @@ -17,7 +17,7 @@ async function startViteNode(options: ContextExecutorOptions) { return _viteNode } -export async function runBaseTests(method: 'run' | 'collect', state: WorkerGlobalState) { +export async function runBaseTests(method: 'run' | 'collect', state: WorkerGlobalState, executor_?: VitestExecutor) { const { ctx } = state // state has new context, but we want to reuse existing ones state.moduleCache = moduleCache @@ -35,7 +35,7 @@ export async function runBaseTests(method: 'run' | 'collect', state: WorkerGloba )) const [executor, { run }] = await Promise.all([ - startViteNode({ state, requestStubs: getDefaultRequestStubs() }), + executor_ || startViteNode({ state, requestStubs: getDefaultRequestStubs() }), import('../runBaseTests'), ]) const fileSpecs = ctx.files.map(f => diff --git a/packages/vitest/src/runtime/workers/strictNode.ts b/packages/vitest/src/runtime/workers/strictNode.ts new file mode 100644 index 000000000000..d676789fa33c --- /dev/null +++ b/packages/vitest/src/runtime/workers/strictNode.ts @@ -0,0 +1,32 @@ +import type { Awaitable } from '@vitest/utils' +import type { WorkerContext } from '../../node/types/worker' +import type { ContextRPC, WorkerGlobalState } from '../../types/worker' +import type { VitestWorker } from './types' +import { resolve } from 'node:path' +import { VitestMocker } from '../mocker' +import { runBaseTests } from './base' +import { createThreadsRpcOptions } from './utils' + +class StrictNodeThreadsWorker implements VitestWorker { + getRpcOptions(ctx: ContextRPC) { + return createThreadsRpcOptions(ctx as WorkerContext) + } + + runTests(state: WorkerGlobalState): Awaitable { + const executor = { + executeId: (id: string) => import(resolve(state.config.root, id)), + executeFile: (id: string) => import(resolve(state.config.root, id)), + options: { + context: undefined, + }, + } as any + executor.mocker = new VitestMocker(executor) + return runBaseTests('run', state, executor) + } + + collectTests(_state: WorkerGlobalState): Awaitable { + throw new Error('Method not implemented.') + } +} + +export default new StrictNodeThreadsWorker() diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c39719d8a27f..cd3d8dc501bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1204,6 +1204,9 @@ importers: tinyspy: specifier: ^1.0.2 version: 1.0.2 + tsx: + specifier: ^4.19.2 + version: 4.19.2 url: specifier: ^0.11.0 version: 0.11.0 diff --git a/test/core/package.json b/test/core/package.json index 8a058771180d..508d27a51435 100644 --- a/test/core/package.json +++ b/test/core/package.json @@ -31,6 +31,7 @@ "sweetalert2": "^11.6.16", "tinyrainbow": "^1.2.0", "tinyspy": "^1.0.2", + "tsx": "^4.19.2", "url": "^0.11.0", "vite-node": "workspace:*", "vitest": "workspace:*", diff --git a/test/core/test/basic.test.ts b/test/core/test/basic.test.ts index 9c6aac68f8eb..d29de87d7b85 100644 --- a/test/core/test/basic.test.ts +++ b/test/core/test/basic.test.ts @@ -32,7 +32,7 @@ test('JSON', () => { test('mode and NODE_ENV is test by default', () => { expect(process.env.NODE_ENV).toBe('test') - expect(import.meta.env.MODE).toBe('test') + // expect(import.meta.env.MODE).toBe('test') TODO: support in strict mode(?) }) test('assertion is callable', () => { diff --git a/test/core/test/setup.js b/test/core/test/setup.js new file mode 100644 index 000000000000..1e9322e8d1dd --- /dev/null +++ b/test/core/test/setup.js @@ -0,0 +1,7 @@ +import { register as requireRegister } from 'tsx/cjs/api'; import { register as esmRegister } from 'tsx/esm/api' + +esmRegister(); requireRegister() + +// import { vi } from 'vitest' + +// vi.mock('../src/global-mock', () => ({ mocked: true })) diff --git a/test/core/test/setup.ts b/test/core/test/setup.ts deleted file mode 100644 index 898dc0bdc30c..000000000000 --- a/test/core/test/setup.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { vi } from 'vitest' - -vi.mock('../src/global-mock', () => ({ mocked: true })) diff --git a/test/core/vite.config.ts b/test/core/vite.config.ts index cd0bf996e990..7219f2f31c6e 100644 --- a/test/core/vite.config.ts +++ b/test/core/vite.config.ts @@ -1,6 +1,11 @@ +import { createRequire } from 'node:module' +import { pathToFileURL } from 'node:url' import { basename, dirname, join, resolve } from 'pathe' import { defaultExclude, defineConfig } from 'vitest/config' +const require = createRequire(import.meta.url) +const tsxApi = require.resolve('tsx/esm/api') + export default defineConfig({ plugins: [ { @@ -60,8 +65,10 @@ export default defineConfig({ exclude: ['**/fixtures/**', ...defaultExclude], slowTestThreshold: 1000, testTimeout: process.env.CI ? 10_000 : 5_000, + preloads: [`data:text/javascript,import { register } from "${pathToFileURL(tsxApi)}";register();`], setupFiles: [ - './test/setup.ts', + // 'tsx', + // './test/setup.js', ], testNamePattern: '^((?!does not include test that).)*$', coverage: { From 2c5fb8001b4d97703667d6f03b16f4869f213482 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 17 Jan 2025 12:10:41 +0100 Subject: [PATCH 02/11] refactor: add "nativeImport" flag --- packages/vitest/rollup.config.js | 1 - .../vitest/src/node/config/resolveConfig.ts | 10 ++++++ .../vitest/src/node/config/serializeConfig.ts | 3 +- packages/vitest/src/node/pools/threads.ts | 2 +- packages/vitest/src/node/types/config.ts | 19 +++++++++-- packages/vitest/src/runtime/config.ts | 3 +- packages/vitest/src/runtime/worker.ts | 6 ++-- packages/vitest/src/runtime/workers/base.ts | 21 ++++++++++-- .../vitest/src/runtime/workers/strictNode.ts | 32 ------------------- packages/vitest/src/runtime/workers/vm.ts | 5 +++ test/core/test/basic.test.ts | 4 +-- test/core/vite.config.ts | 12 ++++--- 12 files changed, 69 insertions(+), 49 deletions(-) delete mode 100644 packages/vitest/src/runtime/workers/strictNode.ts diff --git a/packages/vitest/rollup.config.js b/packages/vitest/rollup.config.js index e604306c53ab..5218a10769e0 100644 --- a/packages/vitest/rollup.config.js +++ b/packages/vitest/rollup.config.js @@ -39,7 +39,6 @@ const entries = { 'workers/threads': 'src/runtime/workers/threads.ts', 'workers/vmThreads': 'src/runtime/workers/vmThreads.ts', 'workers/vmForks': 'src/runtime/workers/vmForks.ts', - 'workers/strictNode': 'src/runtime/workers/strictNode.ts', 'workers/runVmTests': 'src/runtime/runVmTests.ts', diff --git a/packages/vitest/src/node/config/resolveConfig.ts b/packages/vitest/src/node/config/resolveConfig.ts index 4255f9a12c35..481ac543cf74 100644 --- a/packages/vitest/src/node/config/resolveConfig.ts +++ b/packages/vitest/src/node/config/resolveConfig.ts @@ -863,6 +863,16 @@ export function resolveConfig( resolved.testTimeout ??= resolved.browser.enabled ? 15000 : 5000 resolved.hookTimeout ??= resolved.browser.enabled ? 30000 : 10000 + resolved.experimental ??= {} + if (resolved.experimental.preload) { + resolved.experimental.preload = toArray(resolved.experimental.preload).map((preload) => { + if (preload[0] === '.') { + return resolve(resolved.root, preload) + } + return preload + }) + } + return resolved } diff --git a/packages/vitest/src/node/config/serializeConfig.ts b/packages/vitest/src/node/config/serializeConfig.ts index 54db5d7ae98d..8e7ef659bbce 100644 --- a/packages/vitest/src/node/config/serializeConfig.ts +++ b/packages/vitest/src/node/config/serializeConfig.ts @@ -21,7 +21,6 @@ export function serializeConfig( logHeapUsage: config.logHeapUsage, runner: config.runner, bail: config.bail, - preloads: config.preloads || [], defines: config.defines, chaiConfig: config.chaiConfig, setupFiles: config.setupFiles, @@ -165,5 +164,7 @@ export function serializeConfig( benchmark: config.benchmark && { includeSamples: config.benchmark.includeSamples, }, + experimentalNativeImport: config.experimental?.nativeImport ?? false, + experimentalPreload: config.experimental?.preload ?? [], } } diff --git a/packages/vitest/src/node/pools/threads.ts b/packages/vitest/src/node/pools/threads.ts index b87b66726d49..9a8ae875cb6a 100644 --- a/packages/vitest/src/node/pools/threads.ts +++ b/packages/vitest/src/node/pools/threads.ts @@ -59,7 +59,7 @@ export function createThreadsPool( const minThreads = poolOptions.minThreads ?? ctx.config.minWorkers ?? threadsCount - const worker = resolve(ctx.distPath, 'workers/strictNode.js') + const worker = resolve(ctx.distPath, 'workers/threads.js') const options: TinypoolOptions = { filename: resolve(ctx.distPath, 'worker.js'), diff --git a/packages/vitest/src/node/types/config.ts b/packages/vitest/src/node/types/config.ts index 97d1a2921c8a..80cdd72ee356 100644 --- a/packages/vitest/src/node/types/config.ts +++ b/packages/vitest/src/node/types/config.ts @@ -470,8 +470,6 @@ export interface InlineConfig { */ setupFiles?: string | string[] - preloads?: string[] - /** * Path to global setup files */ @@ -838,6 +836,23 @@ export interface InlineConfig { * @default false */ includeTaskLocation?: boolean + + experimental?: { + /** + * Should Vitest use the native "import" instead of Vite Node. + * This can break certain functionality, like `import.meta` + * @experimental + */ + nativeImport?: boolean + /** + * Natively import files before Vitest resolves the environment. + * This is the first import of the test file, you can define loaders here. + * The file is resoled relative to the root of the project. + * `data:*` modules are left as is. + * @experimental + */ + preload?: string[] + } } export interface TypecheckConfig { diff --git a/packages/vitest/src/runtime/config.ts b/packages/vitest/src/runtime/config.ts index 6c997ed7d15c..75d97730430d 100644 --- a/packages/vitest/src/runtime/config.ts +++ b/packages/vitest/src/runtime/config.ts @@ -18,7 +18,6 @@ export interface SerializedConfig { isolate: boolean mode: 'test' | 'benchmark' bail: number | undefined - preloads: string[] environmentOptions?: Record root: string setupFiles: string[] @@ -134,6 +133,8 @@ export interface SerializedConfig { benchmark?: { includeSamples: boolean } + experimentalNativeImport: boolean + experimentalPreload: string[] } export interface SerializedCoverageConfig { diff --git a/packages/vitest/src/runtime/worker.ts b/packages/vitest/src/runtime/worker.ts index 7a87627bd1da..29c8999bf2b2 100644 --- a/packages/vitest/src/runtime/worker.ts +++ b/packages/vitest/src/runtime/worker.ts @@ -41,8 +41,6 @@ async function execute(method: 'run' | 'collect', ctx: ContextRPC) { process.env.VITEST_POOL_ID = String(poolId) try { - await Promise.all(ctx.config.preloads?.map(file => import(file))) - // worker is a filepath or URL to a file that exposes a default export with "getRpcOptions" and "runTests" methods if (ctx.worker[0] === '.') { throw new Error( @@ -50,6 +48,10 @@ async function execute(method: 'run' | 'collect', ctx: ContextRPC) { ) } + if (ctx.config.experimentalPreload) { + await Promise.all(ctx.config.experimentalPreload.map(file => import(file))) + } + const file = ctx.worker.startsWith('file:') ? ctx.worker : pathToFileURL(ctx.worker).toString() diff --git a/packages/vitest/src/runtime/workers/base.ts b/packages/vitest/src/runtime/workers/base.ts index 469ff1b1a648..a25ab7916ba8 100644 --- a/packages/vitest/src/runtime/workers/base.ts +++ b/packages/vitest/src/runtime/workers/base.ts @@ -1,7 +1,9 @@ import type { WorkerGlobalState } from '../../types/worker' import type { ContextExecutorOptions, VitestExecutor } from '../execute' +import { resolve } from 'node:path' import { ModuleCacheMap } from 'vite-node/client' import { getDefaultRequestStubs, startVitestExecutor } from '../execute' +import { VitestMocker } from '../mocker' import { provideWorkerState } from '../utils' let _viteNode: VitestExecutor @@ -17,7 +19,7 @@ async function startViteNode(options: ContextExecutorOptions) { return _viteNode } -export async function runBaseTests(method: 'run' | 'collect', state: WorkerGlobalState, executor_?: VitestExecutor) { +export async function runBaseTests(method: 'run' | 'collect', state: WorkerGlobalState) { const { ctx } = state // state has new context, but we want to reuse existing ones state.moduleCache = moduleCache @@ -35,7 +37,7 @@ export async function runBaseTests(method: 'run' | 'collect', state: WorkerGloba )) const [executor, { run }] = await Promise.all([ - executor_ || startViteNode({ state, requestStubs: getDefaultRequestStubs() }), + resolveExecutor(state), import('../runBaseTests'), ]) const fileSpecs = ctx.files.map(f => @@ -52,3 +54,18 @@ export async function runBaseTests(method: 'run' | 'collect', state: WorkerGloba executor, ) } + +async function resolveExecutor(state: WorkerGlobalState): Promise { + if (state.config.experimentalNativeImport) { + const executor = { + executeId: (id: string) => import(resolve(state.config.root, id)), + executeFile: (id: string) => import(resolve(state.config.root, id)), + options: { + context: undefined, + }, + } as any // TODO: this is a hack for now, build an actual executor + executor.mocker = new VitestMocker(executor) + return executor + } + return startViteNode({ state, requestStubs: getDefaultRequestStubs() }) +} diff --git a/packages/vitest/src/runtime/workers/strictNode.ts b/packages/vitest/src/runtime/workers/strictNode.ts deleted file mode 100644 index d676789fa33c..000000000000 --- a/packages/vitest/src/runtime/workers/strictNode.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { Awaitable } from '@vitest/utils' -import type { WorkerContext } from '../../node/types/worker' -import type { ContextRPC, WorkerGlobalState } from '../../types/worker' -import type { VitestWorker } from './types' -import { resolve } from 'node:path' -import { VitestMocker } from '../mocker' -import { runBaseTests } from './base' -import { createThreadsRpcOptions } from './utils' - -class StrictNodeThreadsWorker implements VitestWorker { - getRpcOptions(ctx: ContextRPC) { - return createThreadsRpcOptions(ctx as WorkerContext) - } - - runTests(state: WorkerGlobalState): Awaitable { - const executor = { - executeId: (id: string) => import(resolve(state.config.root, id)), - executeFile: (id: string) => import(resolve(state.config.root, id)), - options: { - context: undefined, - }, - } as any - executor.mocker = new VitestMocker(executor) - return runBaseTests('run', state, executor) - } - - collectTests(_state: WorkerGlobalState): Awaitable { - throw new Error('Method not implemented.') - } -} - -export default new StrictNodeThreadsWorker() diff --git a/packages/vitest/src/runtime/workers/vm.ts b/packages/vitest/src/runtime/workers/vm.ts index dd3df091ef73..f13e32e25e85 100644 --- a/packages/vitest/src/runtime/workers/vm.ts +++ b/packages/vitest/src/runtime/workers/vm.ts @@ -16,6 +16,11 @@ const fileMap = new FileMap() const packageCache = new Map() export async function runVmTests(method: 'run' | 'collect', state: WorkerGlobalState) { + // TODO: support? + if (state.config.experimentalNativeImport) { + throw new Error(`"experimental.nativeImport" is not supported in VM environments.`) + } + const { environment, ctx, rpc } = state if (!environment.setupVM) { diff --git a/test/core/test/basic.test.ts b/test/core/test/basic.test.ts index d29de87d7b85..35648664d8d9 100644 --- a/test/core/test/basic.test.ts +++ b/test/core/test/basic.test.ts @@ -1,6 +1,6 @@ import { assert, expect, it, suite, test } from 'vitest' -import { two } from '../src/submodule' -import { timeout } from '../src/timeout' +import { two } from '../src/submodule.ts' +import { timeout } from '../src/timeout.ts' const testPath = expect.getState().testPath if (!testPath || !testPath.includes('basic.test.ts')) { diff --git a/test/core/vite.config.ts b/test/core/vite.config.ts index c2a7e5cc6752..8cb4db0354dd 100644 --- a/test/core/vite.config.ts +++ b/test/core/vite.config.ts @@ -1,10 +1,10 @@ -import { createRequire } from 'node:module' -import { pathToFileURL } from 'node:url' +// import { createRequire } from 'node:module' import { basename, dirname, join, resolve } from 'pathe' import { defaultExclude, defineConfig } from 'vitest/config' -const require = createRequire(import.meta.url) -const tsxApi = require.resolve('tsx/esm/api') +// const require = createRequire(import.meta.url) +// const tsxApi = require.resolve('tsx/esm/api') +// preloads: [`data:text/javascript,import { register } from "${pathToFileURL(tsxApi)}";register();`], export default defineConfig({ plugins: [ @@ -65,7 +65,6 @@ export default defineConfig({ exclude: ['**/fixtures/**', ...defaultExclude], slowTestThreshold: 1000, testTimeout: process.env.CI ? 10_000 : 5_000, - preloads: [`data:text/javascript,import { register } from "${pathToFileURL(tsxApi)}";register();`], setupFiles: [ // 'tsx', // './test/setup.js', @@ -142,5 +141,8 @@ export default defineConfig({ return false } }, + experimental: { + nativeImport: true, + }, }, }) From 6dafddb9e4365738c15f657a12716fce7a486a9d Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 17 Jan 2025 12:41:25 +0100 Subject: [PATCH 03/11] chore: add native-executor --- .../vitest/src/runtime/native-executor.ts | 38 +++++++++++++++++++ packages/vitest/src/runtime/workers/base.ts | 13 +------ test/core/test/setup.js | 7 ---- test/core/test/setup.ts | 3 ++ test/core/vite.config.ts | 3 +- 5 files changed, 44 insertions(+), 20 deletions(-) create mode 100644 packages/vitest/src/runtime/native-executor.ts delete mode 100644 test/core/test/setup.js create mode 100644 test/core/test/setup.ts diff --git a/packages/vitest/src/runtime/native-executor.ts b/packages/vitest/src/runtime/native-executor.ts new file mode 100644 index 000000000000..487d2ed265d5 --- /dev/null +++ b/packages/vitest/src/runtime/native-executor.ts @@ -0,0 +1,38 @@ +import type { WorkerGlobalState } from '../types/worker' +import type { ExecuteOptions } from './execute' +import { resolve } from 'node:path' +import { VitestMocker } from './mocker' + +export class NativeExecutor { + public mocker: VitestMocker + + public options: ExecuteOptions + + constructor(state: WorkerGlobalState) { + this.options = { + state, + root: state.config.root, + async fetchModule() { + throw new Error(`fetchModule is not implemented in native executor`) + }, + } + this.mocker = new VitestMocker(this as any) + Object.defineProperty(globalThis, '__vitest_mocker__', { + value: this.mocker, + configurable: true, + }) + } + + executeId(id: string) { + return import(resolve(this.state.config.root, id)) + } + + executeFile(id: string) { + return import(resolve(this.state.config.root, id)) + } + + get state(): WorkerGlobalState { + // @ts-expect-error injected untyped global + return globalThis.__vitest_worker__ || this.options.state + } +} diff --git a/packages/vitest/src/runtime/workers/base.ts b/packages/vitest/src/runtime/workers/base.ts index a25ab7916ba8..a7df6f1fc623 100644 --- a/packages/vitest/src/runtime/workers/base.ts +++ b/packages/vitest/src/runtime/workers/base.ts @@ -1,9 +1,8 @@ import type { WorkerGlobalState } from '../../types/worker' import type { ContextExecutorOptions, VitestExecutor } from '../execute' -import { resolve } from 'node:path' import { ModuleCacheMap } from 'vite-node/client' import { getDefaultRequestStubs, startVitestExecutor } from '../execute' -import { VitestMocker } from '../mocker' +import { NativeExecutor } from '../native-executor' import { provideWorkerState } from '../utils' let _viteNode: VitestExecutor @@ -57,15 +56,7 @@ export async function runBaseTests(method: 'run' | 'collect', state: WorkerGloba async function resolveExecutor(state: WorkerGlobalState): Promise { if (state.config.experimentalNativeImport) { - const executor = { - executeId: (id: string) => import(resolve(state.config.root, id)), - executeFile: (id: string) => import(resolve(state.config.root, id)), - options: { - context: undefined, - }, - } as any // TODO: this is a hack for now, build an actual executor - executor.mocker = new VitestMocker(executor) - return executor + return new NativeExecutor(state) as VitestExecutor } return startViteNode({ state, requestStubs: getDefaultRequestStubs() }) } diff --git a/test/core/test/setup.js b/test/core/test/setup.js deleted file mode 100644 index 1e9322e8d1dd..000000000000 --- a/test/core/test/setup.js +++ /dev/null @@ -1,7 +0,0 @@ -import { register as requireRegister } from 'tsx/cjs/api'; import { register as esmRegister } from 'tsx/esm/api' - -esmRegister(); requireRegister() - -// import { vi } from 'vitest' - -// vi.mock('../src/global-mock', () => ({ mocked: true })) diff --git a/test/core/test/setup.ts b/test/core/test/setup.ts new file mode 100644 index 000000000000..898dc0bdc30c --- /dev/null +++ b/test/core/test/setup.ts @@ -0,0 +1,3 @@ +import { vi } from 'vitest' + +vi.mock('../src/global-mock', () => ({ mocked: true })) diff --git a/test/core/vite.config.ts b/test/core/vite.config.ts index 8cb4db0354dd..d221917e0f3f 100644 --- a/test/core/vite.config.ts +++ b/test/core/vite.config.ts @@ -66,8 +66,7 @@ export default defineConfig({ slowTestThreshold: 1000, testTimeout: process.env.CI ? 10_000 : 5_000, setupFiles: [ - // 'tsx', - // './test/setup.js', + './test/setup.ts', ], reporters: [['default', { summary: true }], 'hanging-process'], testNamePattern: '^((?!does not include test that).)*$', From c1346f81f7ef650478a7349f5b480c45a4f6a64b Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 17 Jan 2025 13:02:31 +0100 Subject: [PATCH 04/11] chore: disable mocker for now --- packages/vitest/src/node/types/config.ts | 1 - .../vitest/src/runtime/native-executor.ts | 32 ++++++++++++++++--- packages/vitest/src/runtime/workers/base.ts | 2 +- test/core/vite.config.ts | 3 -- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/packages/vitest/src/node/types/config.ts b/packages/vitest/src/node/types/config.ts index 80cdd72ee356..dd8c05597dd5 100644 --- a/packages/vitest/src/node/types/config.ts +++ b/packages/vitest/src/node/types/config.ts @@ -269,7 +269,6 @@ export interface InlineConfig { /** * Handling for dependencies inlining or externalizing - * */ deps?: DepsOptions diff --git a/packages/vitest/src/runtime/native-executor.ts b/packages/vitest/src/runtime/native-executor.ts index 487d2ed265d5..289943b20329 100644 --- a/packages/vitest/src/runtime/native-executor.ts +++ b/packages/vitest/src/runtime/native-executor.ts @@ -1,10 +1,13 @@ import type { WorkerGlobalState } from '../types/worker' import type { ExecuteOptions } from './execute' import { resolve } from 'node:path' +import { pathToFileURL } from 'node:url' +import { ModuleCacheMap } from 'vite-node/client' import { VitestMocker } from './mocker' export class NativeExecutor { public mocker: VitestMocker + public moduleCache = new ModuleCacheMap() public options: ExecuteOptions @@ -17,10 +20,11 @@ export class NativeExecutor { }, } this.mocker = new VitestMocker(this as any) - Object.defineProperty(globalThis, '__vitest_mocker__', { - value: this.mocker, - configurable: true, - }) + // TODO: don't support mocker for now + // Object.defineProperty(globalThis, '__vitest_mocker__', { + // value: this.mocker, + // configurable: true, + // }) } executeId(id: string) { @@ -31,6 +35,26 @@ export class NativeExecutor { return import(resolve(this.state.config.root, id)) } + cachedRequest(id: string) { + return import(id) + } + + // used by vi.importActual + async originalResolveUrl(id: string, importer?: string) { + try { + const path = import.meta.resolve(id, importer ? pathToFileURL(importer) : undefined) + return [path, path] + } + catch (error: any) { + if (error.code === 'ERR_MODULE_NOT_FOUND') { + Object.defineProperty(error, Symbol.for('vitest.error.not_found.data'), { + value: { id }, + }) + } + throw error + } + } + get state(): WorkerGlobalState { // @ts-expect-error injected untyped global return globalThis.__vitest_worker__ || this.options.state diff --git a/packages/vitest/src/runtime/workers/base.ts b/packages/vitest/src/runtime/workers/base.ts index a7df6f1fc623..a13f502f0db7 100644 --- a/packages/vitest/src/runtime/workers/base.ts +++ b/packages/vitest/src/runtime/workers/base.ts @@ -56,7 +56,7 @@ export async function runBaseTests(method: 'run' | 'collect', state: WorkerGloba async function resolveExecutor(state: WorkerGlobalState): Promise { if (state.config.experimentalNativeImport) { - return new NativeExecutor(state) as VitestExecutor + return new NativeExecutor(state) as unknown as VitestExecutor } return startViteNode({ state, requestStubs: getDefaultRequestStubs() }) } diff --git a/test/core/vite.config.ts b/test/core/vite.config.ts index d221917e0f3f..386c0259e86a 100644 --- a/test/core/vite.config.ts +++ b/test/core/vite.config.ts @@ -140,8 +140,5 @@ export default defineConfig({ return false } }, - experimental: { - nativeImport: true, - }, }, }) From 22a6e4dec05df1773caca4618086208f949b45cb Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 17 Jan 2025 14:10:23 +0100 Subject: [PATCH 05/11] test: add more tests --- packages/vitest/src/node/core.ts | 9 +++++++-- packages/vitest/src/node/nativeRunner.ts | 12 ++++++++++++ packages/vitest/src/node/plugins/workspace.ts | 7 ++++++- packages/vitest/src/node/project.ts | 9 +++++++-- test/cli/fixtures/native/globalSetup.ts | 8 ++++++++ test/cli/fixtures/native/package.json | 11 +++++++++++ test/cli/fixtures/native/setup.ts | 1 + test/cli/fixtures/native/src/two.ts | 1 + test/cli/fixtures/native/test/basic.test.ts | 12 ++++++++++++ test/cli/fixtures/native/tsconfig.json | 7 +++++++ test/cli/fixtures/native/vitest.config.ts | 11 +++++++++++ test/cli/test/native.test.ts | 16 ++++++++++++++++ test/core/test/basic.test.ts | 4 ++-- 13 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 packages/vitest/src/node/nativeRunner.ts create mode 100644 test/cli/fixtures/native/globalSetup.ts create mode 100644 test/cli/fixtures/native/package.json create mode 100644 test/cli/fixtures/native/setup.ts create mode 100644 test/cli/fixtures/native/src/two.ts create mode 100644 test/cli/fixtures/native/test/basic.test.ts create mode 100644 test/cli/fixtures/native/tsconfig.json create mode 100644 test/cli/fixtures/native/vitest.config.ts create mode 100644 test/cli/test/native.test.ts diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 5b77a4412cb7..5c1365903d0e 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -2,6 +2,7 @@ import type { CancelReason, File } from '@vitest/runner' import type { Awaitable } from '@vitest/utils' import type { Writable } from 'node:stream' import type { ViteDevServer } from 'vite' +import type { ViteNodeRunnerOptions } from 'vite-node' import type { defineWorkspace } from 'vitest/config' import type { SerializedCoverageConfig } from '../runtime/config' import type { ArgumentsType, ProvidedContext, UserConsoleLog } from '../types/general' @@ -30,6 +31,7 @@ import { VitestCache } from './cache' import { resolveConfig } from './config/resolveConfig' import { FilesNotFoundError } from './errors' import { Logger } from './logger' +import { NativeRunner } from './nativeRunner' import { VitestPackageInstaller } from './packageInstaller' import { createPool } from './pool' import { TestProject } from './project' @@ -227,7 +229,7 @@ export class Vitest { this.vitenode = new ViteNodeServer(server, this.config.server) const node = this.vitenode - this.runner = new ViteNodeRunner({ + const viteNodeOptions: ViteNodeRunnerOptions = { root: server.config.root, base: server.config.base, fetchModule(id: string) { @@ -236,7 +238,10 @@ export class Vitest { resolveId(id: string, importer?: string) { return node.resolveId(id, importer) }, - }) + } + this.runner = resolved.experimental.nativeImport + ? new NativeRunner(viteNodeOptions) + : new ViteNodeRunner(viteNodeOptions) if (this.config.watch) { // hijack server restart diff --git a/packages/vitest/src/node/nativeRunner.ts b/packages/vitest/src/node/nativeRunner.ts new file mode 100644 index 000000000000..a8b0f73bca97 --- /dev/null +++ b/packages/vitest/src/node/nativeRunner.ts @@ -0,0 +1,12 @@ +import { resolve } from 'node:path' +import { ViteNodeRunner } from 'vite-node/client' + +export class NativeRunner extends ViteNodeRunner { + override executeFile(file: string): Promise { + return import(resolve(this.options.root, file)) + } + + override executeId(rawId: string): Promise { + return import(resolve(this.options.root, rawId)) + } +} diff --git a/packages/vitest/src/node/plugins/workspace.ts b/packages/vitest/src/node/plugins/workspace.ts index a3e91bdcf29e..3fc3fef8d9e4 100644 --- a/packages/vitest/src/node/plugins/workspace.ts +++ b/packages/vitest/src/node/plugins/workspace.ts @@ -111,7 +111,12 @@ export function WorkspaceVitestPlugin( test: { name, }, - }; + } + + // TODO: inherit one by one, not all of it + if (project.vitest.config.experimental && !config.test?.experimental) { + config.test!.experimental = project.vitest.config.experimental + } (config.test as ResolvedConfig).defines = defines diff --git a/packages/vitest/src/node/project.ts b/packages/vitest/src/node/project.ts index 195dd3f736bc..ec9c5fe4c2b0 100644 --- a/packages/vitest/src/node/project.ts +++ b/packages/vitest/src/node/project.ts @@ -4,6 +4,7 @@ import type { ViteDevServer, InlineConfig as ViteInlineConfig, } from 'vite' +import type { ViteNodeRunnerOptions } from 'vite-node' import type { Typechecker } from '../typecheck/typechecker' import type { ProvidedContext } from '../types/general' import type { OnTestsRerunHandler, Vitest } from './core' @@ -30,6 +31,7 @@ import { setup } from '../api/setup' import { isBrowserEnabled, resolveConfig } from './config/resolveConfig' import { serializeConfig } from './config/serializeConfig' import { loadGlobalSetupFiles } from './globalSetup' +import { NativeRunner } from './nativeRunner' import { CoverageTransform } from './plugins/coverageTransform' import { MocksPlugins } from './plugins/mocks' import { WorkspaceVitestPlugin } from './plugins/workspace' @@ -600,7 +602,7 @@ export class TestProject { this.vitenode = new ViteNodeServer(server, this.config.server) const node = this.vitenode - this.runner = new ViteNodeRunner({ + const viteNodeOptions: ViteNodeRunnerOptions = { root: server.config.root, base: server.config.base, fetchModule(id: string) { @@ -609,7 +611,10 @@ export class TestProject { resolveId(id: string, importer?: string) { return node.resolveId(id, importer) }, - }) + } + this.runner = this._config.experimental.nativeImport + ? new NativeRunner(viteNodeOptions) + : new ViteNodeRunner(viteNodeOptions) } private _serializeOverriddenConfig(): SerializedConfig { diff --git a/test/cli/fixtures/native/globalSetup.ts b/test/cli/fixtures/native/globalSetup.ts new file mode 100644 index 000000000000..98a3a6950d01 --- /dev/null +++ b/test/cli/fixtures/native/globalSetup.ts @@ -0,0 +1,8 @@ +import type { TestProject } from 'vitest/node' + +export default (project: TestProject) => { + project.vitest.logger.log('global setup') + return () => { + project.vitest.logger.log('global teardown') + } +} diff --git a/test/cli/fixtures/native/package.json b/test/cli/fixtures/native/package.json new file mode 100644 index 000000000000..eda6a60b988d --- /dev/null +++ b/test/cli/fixtures/native/package.json @@ -0,0 +1,11 @@ +{ + "name": "@vitest/test-native", + "type": "module", + "private": true, + "scripts": { + "test": "vitest" + }, + "devDependencies": { + "vitest": "workspace:*" + } +} diff --git a/test/cli/fixtures/native/setup.ts b/test/cli/fixtures/native/setup.ts new file mode 100644 index 000000000000..a3136846cd3e --- /dev/null +++ b/test/cli/fixtures/native/setup.ts @@ -0,0 +1 @@ +Reflect.set(globalThis, '__TEST_SETUP_FILE__', true) diff --git a/test/cli/fixtures/native/src/two.ts b/test/cli/fixtures/native/src/two.ts new file mode 100644 index 000000000000..9e5f56922020 --- /dev/null +++ b/test/cli/fixtures/native/src/two.ts @@ -0,0 +1 @@ +export const two = 2 diff --git a/test/cli/fixtures/native/test/basic.test.ts b/test/cli/fixtures/native/test/basic.test.ts new file mode 100644 index 000000000000..426db3c06591 --- /dev/null +++ b/test/cli/fixtures/native/test/basic.test.ts @@ -0,0 +1,12 @@ +import { assert, expect, test } from 'vitest' +import { two } from '../src/two.ts' + +test('Math.sqrt()', () => { + assert.equal(Math.sqrt(4), two) + assert.equal(Math.sqrt(2), Math.SQRT2) + expect(Math.sqrt(144)).toStrictEqual(12) +}) + +test('setup file works', () => { + expect(Reflect.get(globalThis, '__TEST_SETUP_FILE__')).toBe(true) +}) diff --git a/test/cli/fixtures/native/tsconfig.json b/test/cli/fixtures/native/tsconfig.json new file mode 100644 index 000000000000..a026d35becfd --- /dev/null +++ b/test/cli/fixtures/native/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "allowImportingTsExtensions": true + }, + "include": ["./test/**/*.ts", "./src/**/*.ts"] +} diff --git a/test/cli/fixtures/native/vitest.config.ts b/test/cli/fixtures/native/vitest.config.ts new file mode 100644 index 000000000000..8771bcb5178e --- /dev/null +++ b/test/cli/fixtures/native/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + setupFiles: ['./setup.ts'], + globalSetup: './globalSetup.ts', + experimental: { + nativeImport: true, + }, + }, +}) diff --git a/test/cli/test/native.test.ts b/test/cli/test/native.test.ts new file mode 100644 index 000000000000..f58706dfc555 --- /dev/null +++ b/test/cli/test/native.test.ts @@ -0,0 +1,16 @@ +import { expect, test } from 'vitest' +import { runVitest } from '../../test-utils' + +// TODO run only in Node >23 +// TODO: add a test with a loader +test.runIf(process.version.split('.')[0] === '23')('can run custom pools with Vitest', async () => { + const { stderr, stdout } = await runVitest({ + root: './fixtures/native', + }) + + expect(stderr).toBe('') + + expect(stdout).toContain('global setup') + expect(stdout).toContain('global teardown') + expect(stdout).toContain('✓ test/basic.test.ts') +}) diff --git a/test/core/test/basic.test.ts b/test/core/test/basic.test.ts index 35648664d8d9..d29de87d7b85 100644 --- a/test/core/test/basic.test.ts +++ b/test/core/test/basic.test.ts @@ -1,6 +1,6 @@ import { assert, expect, it, suite, test } from 'vitest' -import { two } from '../src/submodule.ts' -import { timeout } from '../src/timeout.ts' +import { two } from '../src/submodule' +import { timeout } from '../src/timeout' const testPath = expect.getState().testPath if (!testPath || !testPath.includes('basic.test.ts')) { From d9f934ba10cd10713d5ed57e382d6584ef2ab78b Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 17 Jan 2025 14:11:50 +0100 Subject: [PATCH 06/11] chore: add more tests --- test/cli/fixtures/native/vitest.config.ts | 8 ++++++++ test/cli/test/native.test.ts | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/test/cli/fixtures/native/vitest.config.ts b/test/cli/fixtures/native/vitest.config.ts index 8771bcb5178e..c764c36bfe60 100644 --- a/test/cli/fixtures/native/vitest.config.ts +++ b/test/cli/fixtures/native/vitest.config.ts @@ -1,6 +1,14 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ + plugins: [ + { + name: 'vitest:break', + load(id) { + throw new Error(`should not load any files, loaded ${id} anyway`) + } + } + ], test: { setupFiles: ['./setup.ts'], globalSetup: './globalSetup.ts', diff --git a/test/cli/test/native.test.ts b/test/cli/test/native.test.ts index f58706dfc555..5e0da04f8bc5 100644 --- a/test/cli/test/native.test.ts +++ b/test/cli/test/native.test.ts @@ -3,7 +3,7 @@ import { runVitest } from '../../test-utils' // TODO run only in Node >23 // TODO: add a test with a loader -test.runIf(process.version.split('.')[0] === '23')('can run custom pools with Vitest', async () => { +test.runIf(process.version.split('.')[0] === 'v23')('can run custom pools with Vitest', async () => { const { stderr, stdout } = await runVitest({ root: './fixtures/native', }) From 82df806774f5a62b59b7be1261af2920b84e58cf Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 17 Jan 2025 14:22:20 +0100 Subject: [PATCH 07/11] chore: cleanup --- packages/utils/src/source-map.ts | 36 +++++++++++----------- packages/vitest/src/node/cli/cli-config.ts | 1 + tsconfig.check.json | 3 +- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/packages/utils/src/source-map.ts b/packages/utils/src/source-map.ts index 903e34d27ae7..7182d46e7eb5 100644 --- a/packages/utils/src/source-map.ts +++ b/packages/utils/src/source-map.ts @@ -24,25 +24,25 @@ const CHROME_IE_STACK_REGEXP = /^\s*at .*(?:\S:\d+|\(native\))/m const SAFARI_NATIVE_CODE_REGEXP = /^(?:eval@)?(?:\[native code\])?$/ const stackIgnorePatterns = [ - // 'node:internal', - // /\/packages\/\w+\/dist\//, - // /\/@vitest\/\w+\/dist\//, - // '/vitest/dist/', - // '/vitest/src/', - // '/vite-node/dist/', - // '/vite-node/src/', - // '/node_modules/chai/', - // '/node_modules/tinypool/', - // '/node_modules/tinyspy/', - // // browser related deps - // '/deps/chunk-', - // '/deps/@vitest', - // '/deps/loupe', + 'node:internal', + /\/packages\/\w+\/dist\//, + /\/@vitest\/\w+\/dist\//, + '/vitest/dist/', + '/vitest/src/', + '/vite-node/dist/', + '/vite-node/src/', + '/node_modules/chai/', + '/node_modules/tinypool/', + '/node_modules/tinyspy/', + // browser related deps + '/deps/chunk-', + '/deps/@vitest', + '/deps/loupe', '/deps/chai', - // /node:\w+/, - // /__vitest_test__/, - // /__vitest_browser__/, - // /\/deps\/vitest_/, + /node:\w+/, + /__vitest_test__/, + /__vitest_browser__/, + /\/deps\/vitest_/, ] function extractLocation(urlLike: string) { diff --git a/packages/vitest/src/node/cli/cli-config.ts b/packages/vitest/src/node/cli/cli-config.ts index 725a6750130a..2fc0149f0fd5 100644 --- a/packages/vitest/src/node/cli/cli-config.ts +++ b/packages/vitest/src/node/cli/cli-config.ts @@ -853,6 +853,7 @@ export const cliOptionsConfig: VitestCLIOptions = { json: null, provide: null, filesOnly: null, + experimental: null, } export const benchCliOptionsConfig: Pick< diff --git a/tsconfig.check.json b/tsconfig.check.json index 278ac220841f..f0057502144f 100644 --- a/tsconfig.check.json +++ b/tsconfig.check.json @@ -4,7 +4,8 @@ "types": [ "node", "vite/client" - ] + ], + "allowImportingTsExtensions": true }, "exclude": [ "**/dist/**", From 3a7b891216786f2173e9f3888fa0f97d53f5278c Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 17 Jan 2025 15:51:13 +0100 Subject: [PATCH 08/11] docs: add "native" draft --- docs/guide/native.md | 86 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 docs/guide/native.md diff --git a/docs/guide/native.md b/docs/guide/native.md new file mode 100644 index 000000000000..72543554366a --- /dev/null +++ b/docs/guide/native.md @@ -0,0 +1,86 @@ +--- +outline: deep +--- + +# Native Mode Experimental {#native-mode} + +By default, Vitest runs tests in a very permissive "vite-node" sandbox powered by the [vite-node package](https://www.npmjs.com/package/vite-node), soon to be Vite's [Environment API](https://vite.dev/guide/api-environment.html#environment-api). Every file is categorized as either an "inline" module or an "external" module. + +All "inline" modules run in the "vite-node" sandbox. It provides `import.meta`, `require`, `__dirname`, `__filename`, static `import`, and has its own module resolution mechanism. This makes it very easy to run code when you don't want to configure the environment and just need to test that the bare JavaScript logic you wrote works as intended. + +All "external" modules run in native mode, meaning they are executed outside of the "vite-node" sandbox. If you are running tests in Node.js, these files are imported with the native `import` keyword and processed by Node.js directly. + +While running browser tests in a permissive fake environment might be justified, running Node.js tests in a non-Node.js environment is counter-productive as it can hide and silence potential errors you may encounter in production. + +## `nativeImports` Flag + +Vitest 3.x comes with an experimental flag called `nativeImports` that you can enabled under `experimental` field: + +```ts +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + experimental: { + nativeImports: true, + }, + }, +}) +``` + +This flag disables all custom file transforms and removes polyfills that Vitest injects in the sandbox environment. Tests are still scheduled to run either in a worker thread or a child process, depending on your [`pool`](/config/#pool) option. This mode is recommended for tools using these environments natively. However, we still recommend running `jsdom`/`happy-dom` tests in a sandbox environment or in [the browser](/guide/browser) as it doesn't reauire any configuration. + +This flag disables _all_ file transforms: + +- test files and your source code are not processed +- your global setup files are not processed +- your custom runner/pool/environment files are not processed +- your config files is still processed by Vite's config resolution mechanism (this happens before Vitest knows the flag) + +### TypeScript + +If you are using TypeScript and Node.js lower than 23.6, then you will need to either: + +- build your test files and source code and run those +- define a [custom loader](https://nodejs.org/api/module.html#customization-hooks) via `experimental.preload` flag + +```ts +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + experimental: { + nativeImports: true, + preload: ['tsx'], + }, + }, +}) +``` + + +::: warning TypeScript with Node 22 +If you are using Node.js 22.6, you can also enable native TypeScript support via `--experimental-strip-types` flag: + +```shell +NODE_OPTIONS=--experimental-strip-types vitest +``` +::: + +If you are using Node.js 23.6 or higher, then TypeScript will be [transformed natively](https://nodejs.org/en/learn/typescript/run-natively) by Node.js. Note that Node.js will print an experimental warning for every test file. + +If you are using Deno, TypeScript files should be processed as they usually are without any additional configurations. + +### Disabled Features + +Some Vitest features rely on files being transformed in some way. Since native mode disables file transformation, these features do not work with `nativeImport` flag: + +- no `import.meta.env` in Node: `import.meta.env` is a Vite feature, use `process.env` instead +- no `import.meta.vitest`: in-source testing requires injecting values to `import.meta` which is not supported by any environment +- no `vi.mock` support: mocking modules is not supported because it relies on code transformations +- no `plugins`: plugins are not applied because there is no transformation phase +- no `alias`: aliases are not applied because there is no transformation/resolution phase +- no watch mode yet (watch mode relies on the Vite module graph) + +::: warning Support is Coming +We are planning to support some of these features by using the [Node.js Loaders API](https://nodejs.org/api/module.html#customization-hooks) in the future. +::: From 65c963b5534eeda039149a995c37ca7c14963aa6 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 17 Jan 2025 16:03:49 +0100 Subject: [PATCH 09/11] docs: fix broken link --- docs/guide/native.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guide/native.md b/docs/guide/native.md index 72543554366a..c369ee0bf6f9 100644 --- a/docs/guide/native.md +++ b/docs/guide/native.md @@ -28,7 +28,7 @@ export default defineConfig({ }) ``` -This flag disables all custom file transforms and removes polyfills that Vitest injects in the sandbox environment. Tests are still scheduled to run either in a worker thread or a child process, depending on your [`pool`](/config/#pool) option. This mode is recommended for tools using these environments natively. However, we still recommend running `jsdom`/`happy-dom` tests in a sandbox environment or in [the browser](/guide/browser) as it doesn't reauire any configuration. +This flag disables all custom file transforms and removes polyfills that Vitest injects in the sandbox environment. Tests are still scheduled to run either in a worker thread or a child process, depending on your [`pool`](/config/#pool) option. This mode is recommended for tools using these environments natively. However, we still recommend running `jsdom`/`happy-dom` tests in a sandbox environment or in [the browser](/guide/browser/) as it doesn't reauire any configuration. This flag disables _all_ file transforms: @@ -58,7 +58,7 @@ export default defineConfig({ ``` -::: warning TypeScript with Node 22 +::: warning TypeScript with Node.js 22 If you are using Node.js 22.6, you can also enable native TypeScript support via `--experimental-strip-types` flag: ```shell From dbb6b4e63211a7de6598a0f8f43c0e5f6eda0d67 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 17 Jan 2025 16:09:03 +0100 Subject: [PATCH 10/11] chore: cleanup --- docs/guide/native.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/guide/native.md b/docs/guide/native.md index c369ee0bf6f9..232fc79af394 100644 --- a/docs/guide/native.md +++ b/docs/guide/native.md @@ -58,15 +58,21 @@ export default defineConfig({ ``` +If you are using Node.js 23.6 or higher, then TypeScript will be [transformed natively](https://nodejs.org/en/learn/typescript/run-natively) by Node.js. + ::: warning TypeScript with Node.js 22 If you are using Node.js 22.6, you can also enable native TypeScript support via `--experimental-strip-types` flag: ```shell -NODE_OPTIONS=--experimental-strip-types vitest +NODE_OPTIONS="--experimental-strip-types" vitest ``` -::: -If you are using Node.js 23.6 or higher, then TypeScript will be [transformed natively](https://nodejs.org/en/learn/typescript/run-natively) by Node.js. Note that Node.js will print an experimental warning for every test file. +Note that Node.js will print an experimental warning for every test file; you can silence the warning by providing `--no-warnings` flag: + +```shell +NODE_OPTIONS="--experimental-strip-types --no-warnings" vitest +``` +::: If you are using Deno, TypeScript files should be processed as they usually are without any additional configurations. From 54dabd2c840c0d4f82c0fedc7e99fb3c6e9ebec6 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 17 Jan 2025 16:40:29 +0100 Subject: [PATCH 11/11] feat: support watch mode --- docs/guide/native.md | 2 +- packages/vitest/src/node/pools/rpc.ts | 16 +++++ .../vitest/src/runtime/native-executor.ts | 6 +- packages/vitest/src/runtime/native-mocker.ts | 66 +++++++++++++++++++ packages/vitest/src/types/rpc.ts | 2 + test/cli/package.json | 3 +- 6 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 packages/vitest/src/runtime/native-mocker.ts diff --git a/docs/guide/native.md b/docs/guide/native.md index 232fc79af394..77359c37c643 100644 --- a/docs/guide/native.md +++ b/docs/guide/native.md @@ -85,7 +85,7 @@ Some Vitest features rely on files being transformed in some way. Since native m - no `vi.mock` support: mocking modules is not supported because it relies on code transformations - no `plugins`: plugins are not applied because there is no transformation phase - no `alias`: aliases are not applied because there is no transformation/resolution phase -- no watch mode yet (watch mode relies on the Vite module graph) + ::: warning Support is Coming We are planning to support some of these features by using the [Node.js Loaders API](https://nodejs.org/api/module.html#customization-hooks) in the future. diff --git a/packages/vitest/src/node/pools/rpc.ts b/packages/vitest/src/node/pools/rpc.ts index 6dedcf73ccba..0a608726eee6 100644 --- a/packages/vitest/src/node/pools/rpc.ts +++ b/packages/vitest/src/node/pools/rpc.ts @@ -3,6 +3,7 @@ import type { RuntimeRPC } from '../../types/rpc' import type { TestProject } from '../project' import type { ResolveSnapshotPathHandlerContext } from '../types/config' import { mkdir, writeFile } from 'node:fs/promises' +import { fileURLToPath } from 'node:url' import { join } from 'pathe' import { hash } from '../hash' @@ -120,6 +121,21 @@ export function createMethodsRPC(project: TestProject, options: MethodsOptions = getCountOfFailedTests() { return ctx.state.getCountOfFailedTests() }, + + // TODO: make sure watcher works + async ensureModuleGraphEntry(id, importer) { + id = id.startsWith('file') ? fileURLToPath(id) : id + importer = importer.startsWith('file') ? fileURLToPath(importer) : importer + + let importerNode = project.vite.moduleGraph.getModuleById(importer) + if (!importerNode) { + importerNode = project.vite.moduleGraph.createFileOnlyEntry(importer) + } + + const moduleNode = project.vite.moduleGraph.getModuleById(id) || project.vite.moduleGraph.createFileOnlyEntry(id) + importerNode.ssrImportedModules.add(moduleNode) + moduleNode.importers.add(importerNode) + }, } } diff --git a/packages/vitest/src/runtime/native-executor.ts b/packages/vitest/src/runtime/native-executor.ts index 289943b20329..5891afd9a395 100644 --- a/packages/vitest/src/runtime/native-executor.ts +++ b/packages/vitest/src/runtime/native-executor.ts @@ -3,10 +3,10 @@ import type { ExecuteOptions } from './execute' import { resolve } from 'node:path' import { pathToFileURL } from 'node:url' import { ModuleCacheMap } from 'vite-node/client' -import { VitestMocker } from './mocker' +import { NativeMocker } from './native-mocker' export class NativeExecutor { - public mocker: VitestMocker + public mocker: NativeMocker public moduleCache = new ModuleCacheMap() public options: ExecuteOptions @@ -19,7 +19,7 @@ export class NativeExecutor { throw new Error(`fetchModule is not implemented in native executor`) }, } - this.mocker = new VitestMocker(this as any) + this.mocker = new NativeMocker(state) // TODO: don't support mocker for now // Object.defineProperty(globalThis, '__vitest_mocker__', { // value: this.mocker, diff --git a/packages/vitest/src/runtime/native-mocker.ts b/packages/vitest/src/runtime/native-mocker.ts new file mode 100644 index 000000000000..2d445ca2797e --- /dev/null +++ b/packages/vitest/src/runtime/native-mocker.ts @@ -0,0 +1,66 @@ +import type { WorkerGlobalState } from '../types/worker' +import module from 'node:module' + +export class NativeMocker { + constructor(private _state: WorkerGlobalState) { + if (typeof (module as any).registerHooks === 'function') { + (module as any).registerHooks({ + resolve: (specifier: string, context: ModuleResolveContext, nextResolve: any): ResolveReturn => { + return this.onResolve(specifier, context, nextResolve) + }, + load: (url: string, context: ModuleLoadContext, nextLoad: any): LoadResult => { + return nextLoad(url, context) + }, + }) + } + else { + console.error('module.register is not supported') + } + } + + reset() { + // noop + } + + onResolve(specifier: string, context: ModuleResolveContext, nextResolve: any): ResolveReturn { + const result = nextResolve(specifier, context) + // TODO: better filter + if (context?.parentURL && !result.url.includes('node_modules')) { + // this makes watch mode possible + this.state.rpc.ensureModuleGraphEntry(result.url, context.parentURL) + } + return result + } + + get state(): WorkerGlobalState { + // @ts-expect-error injected untyped global + return globalThis.__vitest_worker__ || this._state + } +} + +interface ModuleResolveContext { + conditions: string[] + importAttributes: Record + parentURL: string | undefined +} + +interface ResolveReturn { + format?: ModuleFormat | null + importAttributes?: Record + shortCircuit?: boolean + url: string +} + +interface ModuleLoadContext { + conditions: string[] + importAttributes: Record + format: ModuleFormat | null | undefined +} + +interface LoadResult { + format: ModuleFormat + shortCircuit?: boolean + source: string | ArrayBuffer +} + +type ModuleFormat = 'builtin' | 'commonjs' | 'json' | 'module' | 'wasm' diff --git a/packages/vitest/src/types/rpc.ts b/packages/vitest/src/types/rpc.ts index 156fd7dae3c0..bde80a292928 100644 --- a/packages/vitest/src/types/rpc.ts +++ b/packages/vitest/src/types/rpc.ts @@ -46,6 +46,8 @@ export interface RuntimeRPC { snapshotSaved: (snapshot: SnapshotResult) => void resolveSnapshotPath: (testPath: string) => string + + ensureModuleGraphEntry: (specifier: string, importer: string) => Promise } export interface RunnerRPC { diff --git a/test/cli/package.json b/test/cli/package.json index f8fa07c1f3c9..a84e8d4bb8a8 100644 --- a/test/cli/package.json +++ b/test/cli/package.json @@ -3,7 +3,8 @@ "type": "module", "private": true, "scripts": { - "test": "vitest" + "test": "vitest", + "native": "vitest -r ./fixtures/native" }, "devDependencies": { "@types/debug": "^4.1.12",