From 6de38b707f98818964f41884035c4ca911fa1dcf Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 25 Apr 2023 18:57:43 +0200 Subject: [PATCH 01/22] feat: update mock implementation to support ESM runtime, introduce "vi.hoisted" --- packages/browser/src/client/mocker.ts | 25 + packages/browser/src/client/moduleGraph.ts | 23 + packages/vitest/package.json | 1 + .../vitest/src/integrations/browser/server.ts | 3 +- packages/vitest/src/integrations/vi.ts | 13 +- packages/vitest/src/node/config.ts | 3 + packages/vitest/src/node/core.ts | 6 +- packages/vitest/src/node/mock.ts | 197 +++++- .../vitest/src/node/plugins/esm-mocker.ts | 14 + packages/vitest/src/node/plugins/index.ts | 2 + packages/vitest/src/node/plugins/workspace.ts | 2 + packages/vitest/src/node/server.ts | 20 - packages/vitest/src/node/workspace.ts | 8 +- packages/vitest/src/runtime/entry.ts | 2 - packages/vitest/src/runtime/mocker.ts | 5 + packages/vitest/src/types/config.ts | 16 +- pnpm-lock.yaml | 579 ++++++++---------- 17 files changed, 562 insertions(+), 357 deletions(-) create mode 100644 packages/browser/src/client/mocker.ts create mode 100644 packages/browser/src/client/moduleGraph.ts create mode 100644 packages/vitest/src/node/plugins/esm-mocker.ts delete mode 100644 packages/vitest/src/node/server.ts diff --git a/packages/browser/src/client/mocker.ts b/packages/browser/src/client/mocker.ts new file mode 100644 index 000000000000..f076413d52e9 --- /dev/null +++ b/packages/browser/src/client/mocker.ts @@ -0,0 +1,25 @@ +function throwNotImplemented(name: string) { + throw new Error(`[vitest] ${name} is not implemented in browser environment yet.`) +} + +export class VitestBrowserClientMocker { + public importActual() { + throwNotImplemented('importActual') + } + + public importMock() { + throwNotImplemented('importMock') + } + + public queueMock() { + throwNotImplemented('queueMock') + } + + public queueUnmock() { + throwNotImplemented('queueUnmock') + } + + public prepare() { + // TODO: prepare + } +} diff --git a/packages/browser/src/client/moduleGraph.ts b/packages/browser/src/client/moduleGraph.ts new file mode 100644 index 000000000000..302fc6974a7d --- /dev/null +++ b/packages/browser/src/client/moduleGraph.ts @@ -0,0 +1,23 @@ +type ModuleObject = Readonly> +type HijackedModuleObject = Record + +const modules = new WeakMap() + +const moduleCache = new Map() + +// this method receives a module object or "import" promise that it resolves and keeps track of +// and returns a hijacked module object that can be used to mock module exports +export function __vitest_wrap_module__(module: ModuleObject | Promise): HijackedModuleObject | Promise { + if (module instanceof Promise) { + moduleCache.set(module, { promise: module, evaluted: false }) + return module + .then(m => __vitest_wrap_module__(m)) + .finally(() => moduleCache.delete(module)) + } + const cached = modules.get(module) + if (cached) + return cached + const hijacked = Object.assign({}, module) + modules.set(module, hijacked) + return hijacked as HijackedModuleObject +} diff --git a/packages/vitest/package.json b/packages/vitest/package.json index 8ebc85542dda..3ba792543692 100644 --- a/packages/vitest/package.json +++ b/packages/vitest/package.json @@ -167,6 +167,7 @@ "@jridgewell/trace-mapping": "^0.3.17", "@sinonjs/fake-timers": "^10.0.2", "@types/diff": "^5.0.3", + "@types/estree": "^1.0.1", "@types/istanbul-lib-coverage": "^2.0.4", "@types/istanbul-reports": "^3.0.1", "@types/jsdom": "^21.1.1", diff --git a/packages/vitest/src/integrations/browser/server.ts b/packages/vitest/src/integrations/browser/server.ts index ada77fb26364..00f0f8bf86a8 100644 --- a/packages/vitest/src/integrations/browser/server.ts +++ b/packages/vitest/src/integrations/browser/server.ts @@ -42,7 +42,8 @@ export async function createBrowserServer(project: WorkspaceProject, options: Us } config.server = server - config.server.fs = { strict: false } + config.server.fs ??= {} + config.server.fs.strict = false return { resolve: { diff --git a/packages/vitest/src/integrations/vi.ts b/packages/vitest/src/integrations/vi.ts index 1666909a934f..2d971313e252 100644 --- a/packages/vitest/src/integrations/vi.ts +++ b/packages/vitest/src/integrations/vi.ts @@ -1,5 +1,5 @@ import type { FakeTimerInstallOpts } from '@sinonjs/fake-timers' -import { createSimpleStackTrace } from '@vitest/utils' +import { assertTypes, createSimpleStackTrace } from '@vitest/utils' import { parseSingleStack } from '../utils/source-map' import type { VitestMocker } from '../runtime/mocker' import type { ResolvedConfig, RuntimeConfig } from '../types' @@ -30,6 +30,12 @@ interface VitestUtils { spyOn: typeof spyOn fn: typeof fn + /** + * Run the factory before imports are evaluated. You can return a value from the factory + * to reuse it inside your `vi.mock` factory and tests. + */ + hoisted(factory: () => T): T + /** * Makes all `imports` to passed module to be mocked. * - If there is a factory, will return it's result. The call to `vi.mock` is hoisted to the top of the file, @@ -276,6 +282,11 @@ function createVitest(): VitestUtils { spyOn, fn, + hoisted(factory: () => T): T { + assertTypes(factory, '"vi.hoisted" factory', ['function']) + return factory() + }, + mock(path: string, factory?: MockFactoryWithHelper) { const importer = getImporter() _mocker.queueMock( diff --git a/packages/vitest/src/node/config.ts b/packages/vitest/src/node/config.ts index dedbf05be139..f3f99f874666 100644 --- a/packages/vitest/src/node/config.ts +++ b/packages/vitest/src/node/config.ts @@ -277,6 +277,9 @@ export function resolveConfig( port: defaultBrowserPort, } + if (resolved.browser.enabled) + resolved.slowHijackESM ??= true + return resolved } diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 925f22ecdfcd..dcfdbd0bdd0f 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -9,6 +9,7 @@ import { normalizeRequestId } from 'vite-node/utils' import { ViteNodeRunner } from 'vite-node/client' import { SnapshotManager } from '@vitest/snapshot/manager' import type { CancelReason } from '@vitest/runner' +import { ViteNodeServer } from 'vite-node/server' import type { ArgumentsType, CoverageProvider, OnServerRestartHandler, Reporter, ResolvedConfig, UserConfig, UserWorkspaceConfig, VitestRunMode } from '../types' import { hasFailed, noop, slash, toArray } from '../utils' import { getCoverageProvider } from '../integrations/coverage' @@ -22,7 +23,6 @@ import { resolveConfig } from './config' import { Logger } from './logger' import { VitestCache } from './cache' import { WorkspaceProject, initializeProject } from './workspace' -import { VitestServer } from './server' const WATCHER_DEBOUNCE = 100 @@ -40,7 +40,7 @@ export class Vitest { logger: Logger pool: ProcessPool | undefined - vitenode: VitestServer = undefined! + vitenode: ViteNodeServer = undefined! invalidates: Set = new Set() changedTests: Set = new Set() @@ -89,7 +89,7 @@ export class Vitest { if (this.config.watch && this.mode !== 'typecheck') this.registerWatcher() - this.vitenode = new VitestServer(server, this.config) + this.vitenode = new ViteNodeServer(server, this.config) const node = this.vitenode this.runner = new ViteNodeRunner({ root: server.config.root, diff --git a/packages/vitest/src/node/mock.ts b/packages/vitest/src/node/mock.ts index 5de2338a28cd..400a278c82be 100644 --- a/packages/vitest/src/node/mock.ts +++ b/packages/vitest/src/node/mock.ts @@ -1,9 +1,31 @@ import MagicString from 'magic-string' +import { Parser } from 'acorn' +import { findNodeAround, simple as walk } from 'acorn-walk' +import type { CallExpression, Expression, Identifier, ImportDeclaration, ImportExpression, VariableDeclaration } from 'estree' import type { DecodedSourceMap, RawSourceMap } from '@ampproject/remapping' import type { SourceMap } from 'rollup' -import type { TransformResult } from 'vite' +import type { TransformResult, ViteDevServer } from 'vite' import remapping from '@ampproject/remapping' -import { getCallLastIndex } from '../utils' +import { getCallLastIndex, toArray } from '../utils' +import type { WorkspaceProject } from './workspace' +import type { Vitest } from './core' + +type Positioned = T & { + start: number + end: number +} + +const parsers = new WeakMap() + +function getAcornParser(server: ViteDevServer) { + const acornPlugins = server.pluginContainer.options.acornInjectPlugins || [] + let parser = parsers.get(server)! + if (!parser) { + parser = Parser.extend(...toArray(acornPlugins) as any) + parsers.set(server, parser) + } + return parser +} const hoistRegexp = /^[ \t]*\b(?:__vite_ssr_import_\d+__\.)?((?:vitest|vi)\s*.\s*(mock|unmock)\(["`'\s]+(.*[@\w_-]+)["`'\s]+)[),]{1};?/gm @@ -14,6 +36,9 @@ To fix this issue you can either: - import the mocks API directly from 'vitest' - enable the 'globals' options` +const API_NOT_FOUND_CHECK = 'if (typeof globalThis.vi === "undefined" && typeof globalThis.vitest === "undefined") ' ++ `{ throw new Error(${JSON.stringify(API_NOT_FOUND_ERROR)}) }\n` + export function hoistModuleMocks(mod: TransformResult, vitestPath: string): TransformResult { if (!mod.code) return mod @@ -233,3 +258,171 @@ function getIndexStatus(code: string, from: number) { insideString: inString !== null, } } + +const regexpHoistable = /^[ \t]*\b(vi|vitest)\s*\.\s*(mock|unmock|hoist)/m +const hashbangRE = /^#!.*\n/ + +function isIdentifier(node: any): node is Positioned { + return node.type === 'Identifier' +} + +function transformImportSpecifiers(node: ImportDeclaration) { + const specifiers = node.specifiers + + if (specifiers.length === 1 && specifiers[0].type === 'ImportNamespaceSpecifier') + return specifiers[0].local.name + + const dynamicImports = node.specifiers.map((specifier) => { + if (specifier.type === 'ImportDefaultSpecifier') + return `default: ${specifier.local.name}` + + if (specifier.type === 'ImportSpecifier') { + const local = specifier.local.name + const imported = specifier.imported.name + if (local === imported) + return local + return `${imported}: ${local}` + } + + return null + }).filter(Boolean).join(', ') + + return `{ ${dynamicImports} }` +} + +export function transformMockableFile(project: WorkspaceProject | Vitest, id: string, source: string, needMap = false) { + const hasMocks = regexpHoistable.test(source) + const hijackEsm = project.config.slowHijackESM ?? false + + // we don't need to constrol __vitest_module__ in Node.js, + // because we control the module resolution directly, + // but we stil need to hoist mocks everywhere + if (!hijackEsm && !hasMocks) + return + + const parser = getAcornParser(project.server) + const hoistIndex = source.match(hashbangRE)?.[0].length ?? 0 + let ast: any + try { + ast = parser.parse(source, { + sourceType: 'module', + ecmaVersion: 'latest', + locations: true, + allowHashBang: true, + }) + } + catch (err) { + console.error(`[vitest] Not able to parse source code of ${id}.`) + console.error(err) + return + } + + const magicString = new MagicString(source) + + let hoistedCalls = '' + // hoist Vitest imports at the very top of the file + let hoistedVitestImports = '' + let idx = 0 + + // this will tranfrom import statements into dynamic ones, if there are imports + // it will keep the import as is, if we don't need to mock anything + // in browser environment it will wrap the module value with "vitest_wrap_module" function + // that returns a proxy to the module so that named exports can be mocked + const transformImportDeclaration = (node: ImportDeclaration) => { + // if we don't hijack ESM and process this file, then we definetly have mocks, + // so we need to transform imports into dynamic ones, so "vi.mock" can be executed before + if (!hijackEsm) + return `const ${transformImportSpecifiers(node)} = await import('${node.source.value}')\n` + + const moduleName = `__vitest_module_${idx++}__` + const destructured = `const ${transformImportSpecifiers(node)} = __vitest_wrap_module__(${moduleName})` + if (hasMocks) + return `const ${moduleName} = await import('${node.source.value}')\n${destructured}` + return `import * as ${moduleName} from '${node.source.value}'\n${destructured}` + } + + walk(ast, { + ImportExpression(_node) { + if (!hijackEsm) + return + const node = _node as any as Positioned + const replace = '__vitest_wrap_module__(import(' + magicString.overwrite(node.start, (node.source as Positioned).start, replace) + magicString.overwrite(node.end - 1, node.end, '))') + }, + + ImportDeclaration(_node) { + const node = _node as any as Positioned + + const start = node.start + const end = node.end + + if (node.source.value === 'vitest') { + hoistedVitestImports += transformImportDeclaration(node) + magicString.remove(start, end) + return + } + + const dynamicImport = transformImportDeclaration(node) + + magicString.overwrite(start, end, dynamicImport) + }, + + CallExpression(_node) { + const node = _node as any as Positioned + + if ( + node.callee.type === 'MemberExpression' + && isIdentifier(node.callee.object) + && (node.callee.object.name === 'vi' || node.callee.object.name === 'vitest') + && isIdentifier(node.callee.property) + ) { + const methodName = node.callee.property.name + if (methodName === 'mock' || methodName === 'unmock') { + hoistedCalls += `${source.slice(node.start, node.end)}\n` + magicString.remove(node.start, node.end) + } + if (methodName === 'hoisted') { + const declarationNode = findNodeAround(ast, node.start, 'VariableDeclaration')?.node as Positioned | undefined + const init = declarationNode?.declarations[0]?.init + if ( + init + && init.type === 'CallExpression' + && init.callee.type === 'MemberExpression' + && isIdentifier(init.callee.object) + && (node.callee.object.name === 'vi' || node.callee.object.name === 'vitest') + && isIdentifier(init.callee.property) + && init.callee.property.name === 'hoisted' + ) { + // hoist const variable = vi.hoisted(() => {}) + hoistedCalls += `${source.slice(declarationNode.start, declarationNode.end)}\n` + magicString.remove(declarationNode.start, declarationNode.end) + } + else { + // hoist vi.hoisted(() => {}) + hoistedCalls += `${source.slice(node.start, node.end)}\n` + magicString.remove(node.start, node.end) + } + } + } + }, + }) + + if (hasMocks) + hoistedCalls += 'await __vitest_mocker__.prepare()\n' + + magicString.appendLeft( + hoistIndex, + hoistedVitestImports + + (hoistedVitestImports ? '' : API_NOT_FOUND_CHECK) + + hoistedCalls, + ) + + const code = magicString.toString() + const map = needMap ? magicString.generateMap({ hires: true }) : null + + return { + code, + map, + } +} diff --git a/packages/vitest/src/node/plugins/esm-mocker.ts b/packages/vitest/src/node/plugins/esm-mocker.ts new file mode 100644 index 000000000000..651142c781f3 --- /dev/null +++ b/packages/vitest/src/node/plugins/esm-mocker.ts @@ -0,0 +1,14 @@ +import type { Plugin } from 'vite' +import { transformMockableFile } from '../mock' +import type { Vitest } from '../core' +import type { WorkspaceProject } from '../workspace' + +export function ESMMockerPlugin(ctx: WorkspaceProject | Vitest): Plugin { + return { + name: 'vitest:mocker-plugin', + enforce: 'post', + transform(code, id) { + return transformMockableFile(ctx, id, code, true) + }, + } +} diff --git a/packages/vitest/src/node/plugins/index.ts b/packages/vitest/src/node/plugins/index.ts index 02655dc17a4c..64a5f6c444f9 100644 --- a/packages/vitest/src/node/plugins/index.ts +++ b/packages/vitest/src/node/plugins/index.ts @@ -11,6 +11,7 @@ import { EnvReplacerPlugin } from './envReplacer' import { GlobalSetupPlugin } from './globalSetup' import { CSSEnablerPlugin } from './cssEnabler' import { CoverageTransform } from './coverageTransform' +import { ESMMockerPlugin } from './esm-mocker' export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest('test')): Promise { const userConfig = deepMerge({}, options) as UserConfig @@ -242,6 +243,7 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest('t options.ui ? await UIPlugin() : null, + ESMMockerPlugin(ctx), ] .filter(notNullish) } diff --git a/packages/vitest/src/node/plugins/workspace.ts b/packages/vitest/src/node/plugins/workspace.ts index 250d8040cc77..bdf7f7daf6f3 100644 --- a/packages/vitest/src/node/plugins/workspace.ts +++ b/packages/vitest/src/node/plugins/workspace.ts @@ -9,6 +9,7 @@ import { CoverageTransform } from './coverageTransform' import { CSSEnablerPlugin } from './cssEnabler' import { EnvReplacerPlugin } from './envReplacer' import { GlobalSetupPlugin } from './globalSetup' +import { ESMMockerPlugin } from './esm-mocker' interface WorkspaceOptions extends UserWorkspaceConfig { root?: string @@ -138,5 +139,6 @@ export function WorkspaceVitestPlugin(project: WorkspaceProject, options: Worksp ...CSSEnablerPlugin(project), CoverageTransform(project.ctx), GlobalSetupPlugin(project, project.ctx.logger), + ESMMockerPlugin(project), ] } diff --git a/packages/vitest/src/node/server.ts b/packages/vitest/src/node/server.ts deleted file mode 100644 index 6b8f0115546e..000000000000 --- a/packages/vitest/src/node/server.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { TransformResult } from 'vite' -import { ViteNodeServer } from 'vite-node/server' -import { hoistModuleMocks } from './mock' - -export class VitestServer extends ViteNodeServer { - private _vitestPath?: string - - private async getVitestPath() { - if (!this._vitestPath) { - const { id } = await this.resolveId('vitest') || { id: 'vitest' } - this._vitestPath = id - } - return this._vitestPath - } - - protected async processTransformResult(id: string, result: TransformResult): Promise { - const vitestId = await this.getVitestPath() - return super.processTransformResult(id, hoistModuleMocks(result, vitestId)) - } -} diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts index c875fe298e59..f373f7c1c50b 100644 --- a/packages/vitest/src/node/workspace.ts +++ b/packages/vitest/src/node/workspace.ts @@ -5,6 +5,7 @@ import { dirname, relative, resolve, toNamespacedPath } from 'pathe' import { createServer } from 'vite' import type { ViteDevServer, InlineConfig as ViteInlineConfig } from 'vite' import { ViteNodeRunner } from 'vite-node/client' +import { ViteNodeServer } from 'vite-node/server' import { createBrowserServer } from '../integrations/browser/server' import type { ArgumentsType, Reporter, ResolvedConfig, UserConfig, UserWorkspaceConfig, Vitest } from '../types' import { deepMerge, hasFailed } from '../utils' @@ -13,10 +14,9 @@ import type { BrowserProvider } from '../types/browser' import { getBrowserProvider } from '../integrations/browser' import { isBrowserEnabled, resolveConfig } from './config' import { WorkspaceVitestPlugin } from './plugins/workspace' -import { VitestServer } from './server' interface InitializeServerOptions { - server?: VitestServer + server?: ViteNodeServer runner?: ViteNodeRunner } @@ -65,7 +65,7 @@ export class WorkspaceProject { config!: ResolvedConfig server!: ViteDevServer - vitenode!: VitestServer + vitenode!: ViteNodeServer runner!: ViteNodeRunner browser: ViteDevServer = undefined! typechecker?: Typechecker @@ -170,7 +170,7 @@ export class WorkspaceProject { this.config = resolveConfig(this.ctx.mode, options, server.config) this.server = server - this.vitenode = params.server ?? new VitestServer(server, this.config) + this.vitenode = params.server ?? new ViteNodeServer(server, this.config) const node = this.vitenode this.runner = params.runner ?? new ViteNodeRunner({ root: server.config.root, diff --git a/packages/vitest/src/runtime/entry.ts b/packages/vitest/src/runtime/entry.ts index 2c16e82e58b1..45586375c4b1 100644 --- a/packages/vitest/src/runtime/entry.ts +++ b/packages/vitest/src/runtime/entry.ts @@ -124,8 +124,6 @@ export async function run(files: string[], config: ResolvedConfig, environment: await startTests([file], runner) - workerState.filepath = undefined - // reset after tests, because user might call `vi.setConfig` in setupFile vi.resetConfig() // mocks should not affect different files diff --git a/packages/vitest/src/runtime/mocker.ts b/packages/vitest/src/runtime/mocker.ts index a3f617113b50..0500a9026162 100644 --- a/packages/vitest/src/runtime/mocker.ts +++ b/packages/vitest/src/runtime/mocker.ts @@ -378,4 +378,9 @@ export class VitestMocker { public queueUnmock(id: string, importer: string) { VitestMocker.pendingIds.push({ type: 'unmock', id, importer }) } + + public async prepare() { + if (VitestMocker.pendingIds.length) + await this.resolveMocks() + } } diff --git a/packages/vitest/src/types/config.ts b/packages/vitest/src/types/config.ts index 858303728c59..4493586825dd 100644 --- a/packages/vitest/src/types/config.ts +++ b/packages/vitest/src/types/config.ts @@ -188,6 +188,7 @@ export interface InlineConfig { /** * Automatically assign environment based on globs. The first match will be used. + * This has effect only when running tests inside Node.js. * * Format: [glob, environment-name] * @@ -209,13 +210,13 @@ export interface InlineConfig { * * @default [] * @example [ - * // all tests in "browser" directory will run in an actual browser - * ['tests/browser/**', 'browser'], + * // all tests in "child_process" directory will run using "child_process" API + * ['tests/child_process/**', 'child_process'], * // all other tests will run based on "threads" option, if you didn't specify other globs * // ... * ] */ - poolMatchGlobs?: [string, VitestPool][] + poolMatchGlobs?: [string, Omit][] /** * Update snapshot @@ -423,6 +424,15 @@ export interface InlineConfig { */ uiBase?: string + /** + * Update ESM imports so they can be spied/stubbed with vi.spyOn. + * Enabled by default when running in browser. + * + * @default false + * @experimental + */ + slowHijackESM?: boolean + /** * Determine the transform method of modules */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40228d85cbf0..9ddd8a6c3815 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -146,7 +146,7 @@ importers: version: 0.0.5(vite-plugin-pwa@0.14.7) '@vitejs/plugin-vue': specifier: latest - version: 4.1.0(vite@4.2.1)(vue@3.2.47) + version: 4.2.0(vite@4.2.1)(vue@3.2.47) esno: specifier: ^0.16.3 version: 0.16.3 @@ -388,7 +388,7 @@ importers: version: link:../../packages/ui happy-dom: specifier: latest - version: 9.1.7 + version: 9.1.9 jsdom: specifier: latest version: 21.1.1 @@ -678,7 +678,7 @@ importers: devDependencies: '@vitejs/plugin-vue': specifier: latest - version: 4.1.0(vite@4.2.1)(vue@3.2.47) + version: 4.2.0(vite@4.2.1)(vue@3.2.47) '@vue/test-utils': specifier: latest version: 2.3.2(vue@3.2.47) @@ -755,7 +755,7 @@ importers: devDependencies: '@vitejs/plugin-vue': specifier: latest - version: 4.1.0(vite@4.2.1)(vue@3.2.47) + version: 4.2.0(vite@4.2.1)(vue@3.2.47) '@vue/test-utils': specifier: ^2.0.2 version: 2.0.2(vue@3.2.47) @@ -764,10 +764,10 @@ importers: version: 21.1.1 unplugin-auto-import: specifier: latest - version: 0.7.2(esbuild@0.17.15)(rollup@3.20.2)(vite@4.2.1) + version: 0.12.1(rollup@3.20.2) unplugin-vue-components: specifier: latest - version: 0.19.6(esbuild@0.17.15)(rollup@3.20.2)(vite@4.2.1)(vue@3.2.47) + version: 0.22.12(rollup@3.20.2)(vue@3.2.47) vite: specifier: ^4.2.1 version: 4.2.1(@types/node@18.15.11) @@ -783,7 +783,7 @@ importers: devDependencies: '@vitejs/plugin-vue': specifier: latest - version: 4.1.0(vite@4.2.1)(vue@3.2.47) + version: 4.2.0(vite@4.2.1)(vue@3.2.47) '@vue/test-utils': specifier: ^2.0.0 version: 2.0.0(vue@3.2.47) @@ -801,7 +801,7 @@ importers: devDependencies: '@vitejs/plugin-vue': specifier: latest - version: 4.1.0(vite@4.2.1)(vue@3.2.47) + version: 4.2.0(vite@4.2.1)(vue@3.2.47) '@vitejs/plugin-vue-jsx': specifier: latest version: 3.0.1(vite@4.2.1)(vue@3.2.47) @@ -1252,6 +1252,9 @@ importers: '@types/diff': specifier: ^5.0.3 version: 5.0.3 + '@types/estree': + specifier: ^1.0.1 + version: 1.0.1 '@types/istanbul-lib-coverage': specifier: ^2.0.4 version: 2.0.4 @@ -1505,7 +1508,7 @@ importers: version: 2.0.4 '@vitejs/plugin-vue': specifier: latest - version: 4.1.0(vite@4.2.1)(vue@3.2.47) + version: 4.2.0(vite@4.2.1)(vue@3.2.47) '@vitest/browser': specifier: workspace:* version: link:../../packages/browser @@ -1514,7 +1517,7 @@ importers: version: 2.3.2(vue@3.2.47) happy-dom: specifier: latest - version: 9.1.7 + version: 9.1.9 istanbul-lib-coverage: specifier: ^3.2.0 version: 3.2.0 @@ -1529,7 +1532,7 @@ importers: version: 3.2.47 webdriverio: specifier: latest - version: 8.7.0(typescript@5.0.3) + version: 8.8.0(typescript@5.0.3) test/css: devDependencies: @@ -1785,7 +1788,7 @@ importers: version: link:../../packages/vitest webdriverio: specifier: latest - version: 8.7.0(typescript@5.0.3) + version: 8.8.0(typescript@5.0.3) test/web-worker: devDependencies: @@ -2165,15 +2168,15 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@ampproject/remapping': 2.2.0 - '@babel/code-frame': 7.18.6 - '@babel/generator': 7.20.7 - '@babel/helper-compilation-targets': 7.20.7(@babel/core@7.20.5) - '@babel/helper-module-transforms': 7.20.11 - '@babel/helpers': 7.20.7 - '@babel/parser': 7.20.7 + '@babel/code-frame': 7.21.4 + '@babel/generator': 7.21.4 + '@babel/helper-compilation-targets': 7.21.4(@babel/core@7.20.5) + '@babel/helper-module-transforms': 7.21.2 + '@babel/helpers': 7.21.0 + '@babel/parser': 7.21.4 '@babel/template': 7.20.7 - '@babel/traverse': 7.20.12 - '@babel/types': 7.21.3 + '@babel/traverse': 7.21.4 + '@babel/types': 7.21.4 convert-source-map: 1.9.0 debug: 4.3.4(supports-color@8.1.1) gensync: 1.0.0-beta.2 @@ -2209,15 +2212,7 @@ packages: resolution: {integrity: sha512-CkPg8ySSPuHTYPJYo7IRALdqyjM9HCbt/3uOBEFbzyGVP6Mn8bwFPB0jX6982JVNBlYzM1nnPkfjuXSOPtQeEQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.21.3 - '@jridgewell/gen-mapping': 0.3.2 - jsesc: 2.5.2 - - /@babel/generator@7.20.7: - resolution: {integrity: sha512-7wqMOJq8doJMZmP4ApXTzLxSr7+oO2jroJURrVEp6XShrQUObV8Tq/D0NCcoYg2uHqUrjzO0zwBjoYzelxK+sw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.21.3 + '@babel/types': 7.21.4 '@jridgewell/gen-mapping': 0.3.2 jsesc: 2.5.2 @@ -2234,7 +2229,7 @@ packages: resolution: {integrity: sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.21.3 + '@babel/types': 7.21.4 dev: true /@babel/helper-builder-binary-assignment-operator-visitor@7.18.9: @@ -2242,7 +2237,7 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/helper-explode-assignable-expression': 7.18.6 - '@babel/types': 7.21.3 + '@babel/types': 7.21.4 dev: true /@babel/helper-compilation-targets@7.18.9(@babel/core@7.18.13): @@ -2257,43 +2252,29 @@ packages: browserslist: 4.21.3 semver: 6.3.0 - /@babel/helper-compilation-targets@7.20.7(@babel/core@7.18.13): - resolution: {integrity: sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==} + /@babel/helper-compilation-targets@7.21.4(@babel/core@7.18.13): + resolution: {integrity: sha512-Fa0tTuOXZ1iL8IeDFUWCzjZcn+sJGd9RZdH9esYVjEejGmzf+FFYQpMi/kZUk2kPy/q1H3/GPw7np8qar/stfg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/compat-data': 7.20.10 + '@babel/compat-data': 7.21.4 '@babel/core': 7.18.13 - '@babel/helper-validator-option': 7.18.6 + '@babel/helper-validator-option': 7.21.0 browserslist: 4.21.3 lru-cache: 5.1.1 semver: 6.3.0 dev: true - /@babel/helper-compilation-targets@7.20.7(@babel/core@7.20.5): - resolution: {integrity: sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==} + /@babel/helper-compilation-targets@7.21.4(@babel/core@7.20.5): + resolution: {integrity: sha512-Fa0tTuOXZ1iL8IeDFUWCzjZcn+sJGd9RZdH9esYVjEejGmzf+FFYQpMi/kZUk2kPy/q1H3/GPw7np8qar/stfg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/compat-data': 7.20.10 + '@babel/compat-data': 7.21.4 '@babel/core': 7.20.5 - '@babel/helper-validator-option': 7.18.6 - browserslist: 4.21.3 - lru-cache: 5.1.1 - semver: 6.3.0 - dev: true - - /@babel/helper-compilation-targets@7.20.7(@babel/core@7.21.4): - resolution: {integrity: sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/compat-data': 7.20.10 - '@babel/core': 7.21.4 - '@babel/helper-validator-option': 7.18.6 + '@babel/helper-validator-option': 7.21.0 browserslist: 4.21.3 lru-cache: 5.1.1 semver: 6.3.0 @@ -2397,10 +2378,10 @@ packages: '@babel/core': ^7.4.0-0 dependencies: '@babel/core': 7.21.4 - '@babel/helper-compilation-targets': 7.20.7(@babel/core@7.21.4) + '@babel/helper-compilation-targets': 7.21.4(@babel/core@7.21.4) '@babel/helper-module-imports': 7.18.6 '@babel/helper-plugin-utils': 7.20.2 - '@babel/traverse': 7.20.12 + '@babel/traverse': 7.21.4 debug: 4.3.4(supports-color@8.1.1) lodash.debounce: 4.0.8 resolve: 1.22.1 @@ -2415,7 +2396,7 @@ packages: '@babel/core': ^7.4.0-0 dependencies: '@babel/core': 7.18.13 - '@babel/helper-compilation-targets': 7.20.7(@babel/core@7.18.13) + '@babel/helper-compilation-targets': 7.21.4(@babel/core@7.18.13) '@babel/helper-plugin-utils': 7.20.2 debug: 4.3.4(supports-color@8.1.1) lodash.debounce: 4.0.8 @@ -2431,7 +2412,7 @@ packages: '@babel/core': ^7.4.0-0 dependencies: '@babel/core': 7.21.4 - '@babel/helper-compilation-targets': 7.20.7(@babel/core@7.21.4) + '@babel/helper-compilation-targets': 7.21.4(@babel/core@7.21.4) '@babel/helper-plugin-utils': 7.20.2 debug: 4.3.4(supports-color@8.1.1) lodash.debounce: 4.0.8 @@ -2449,7 +2430,7 @@ packages: resolution: {integrity: sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.21.3 + '@babel/types': 7.21.4 dev: true /@babel/helper-function-name@7.21.0: @@ -2457,33 +2438,33 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/template': 7.20.7 - '@babel/types': 7.21.3 + '@babel/types': 7.21.4 /@babel/helper-hoist-variables@7.18.6: resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.21.3 + '@babel/types': 7.21.4 /@babel/helper-member-expression-to-functions@7.21.0: resolution: {integrity: sha512-Muu8cdZwNN6mRRNG6lAYErJ5X3bRevgYR2O8wN0yn7jJSnGDu6eG59RfT29JHxGUovyfrh6Pj0XzmR7drNVL3Q==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.21.3 + '@babel/types': 7.21.4 dev: true /@babel/helper-module-imports@7.16.0: resolution: {integrity: sha512-kkH7sWzKPq0xt3H1n+ghb4xEMP8k0U7XV3kkB+ZGy69kDk2ySFW1qPi06sjKzFY3t1j6XbJSqr4mF9L7CYVyhg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.21.3 + '@babel/types': 7.21.4 dev: true /@babel/helper-module-imports@7.18.6: resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.21.3 + '@babel/types': 7.21.4 /@babel/helper-module-transforms@7.18.9: resolution: {integrity: sha512-KYNqY0ICwfv19b31XzvmI/mfcylOzbLtowkw+mfvGPAQ3kfCnMLYbED3YecL5tPd8nAYFQFAd6JHp2LxZk/J1g==} @@ -2495,26 +2476,10 @@ packages: '@babel/helper-split-export-declaration': 7.18.6 '@babel/helper-validator-identifier': 7.19.1 '@babel/template': 7.20.7 - '@babel/traverse': 7.20.12 - '@babel/types': 7.21.3 - transitivePeerDependencies: - - supports-color - - /@babel/helper-module-transforms@7.20.11: - resolution: {integrity: sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-simple-access': 7.20.2 - '@babel/helper-split-export-declaration': 7.18.6 - '@babel/helper-validator-identifier': 7.19.1 - '@babel/template': 7.20.7 - '@babel/traverse': 7.20.12 - '@babel/types': 7.21.3 + '@babel/traverse': 7.21.4 + '@babel/types': 7.21.4 transitivePeerDependencies: - supports-color - dev: true /@babel/helper-module-transforms@7.21.2: resolution: {integrity: sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ==} @@ -2535,7 +2500,7 @@ packages: resolution: {integrity: sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.21.3 + '@babel/types': 7.21.4 dev: true /@babel/helper-plugin-utils@7.10.4: @@ -2556,7 +2521,7 @@ packages: '@babel/helper-annotate-as-pure': 7.18.6 '@babel/helper-environment-visitor': 7.18.9 '@babel/helper-wrap-function': 7.18.11 - '@babel/types': 7.21.3 + '@babel/types': 7.21.4 transitivePeerDependencies: - supports-color dev: true @@ -2571,7 +2536,7 @@ packages: '@babel/helper-annotate-as-pure': 7.18.6 '@babel/helper-environment-visitor': 7.18.9 '@babel/helper-wrap-function': 7.18.11 - '@babel/types': 7.21.3 + '@babel/types': 7.21.4 transitivePeerDependencies: - supports-color dev: true @@ -2584,8 +2549,8 @@ packages: '@babel/helper-member-expression-to-functions': 7.21.0 '@babel/helper-optimise-call-expression': 7.18.6 '@babel/template': 7.20.7 - '@babel/traverse': 7.20.12 - '@babel/types': 7.21.3 + '@babel/traverse': 7.21.4 + '@babel/types': 7.21.4 transitivePeerDependencies: - supports-color dev: true @@ -2594,20 +2559,20 @@ packages: resolution: {integrity: sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.21.3 + '@babel/types': 7.21.4 /@babel/helper-skip-transparent-expression-wrappers@7.20.0: resolution: {integrity: sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.21.3 + '@babel/types': 7.21.4 dev: true /@babel/helper-split-export-declaration@7.18.6: resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.21.3 + '@babel/types': 7.21.4 /@babel/helper-string-parser@7.19.4: resolution: {integrity: sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==} @@ -2631,8 +2596,8 @@ packages: dependencies: '@babel/helper-function-name': 7.21.0 '@babel/template': 7.20.7 - '@babel/traverse': 7.20.12 - '@babel/types': 7.21.3 + '@babel/traverse': 7.21.4 + '@babel/types': 7.21.4 transitivePeerDependencies: - supports-color dev: true @@ -2642,21 +2607,10 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/template': 7.20.7 - '@babel/traverse': 7.20.12 - '@babel/types': 7.21.3 - transitivePeerDependencies: - - supports-color - - /@babel/helpers@7.20.7: - resolution: {integrity: sha512-PBPjs5BppzsGaxHQCDKnZ6Gd9s6xl8bBCluz3vEInLGRJmnZan4F6BYCeqtyXqkk4W5IlPmjK4JlOuZkpJ3xZA==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/template': 7.20.7 - '@babel/traverse': 7.20.12 - '@babel/types': 7.21.3 + '@babel/traverse': 7.21.4 + '@babel/types': 7.21.4 transitivePeerDependencies: - supports-color - dev: true /@babel/helpers@7.21.0: resolution: {integrity: sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA==} @@ -2681,14 +2635,7 @@ packages: engines: {node: '>=6.0.0'} hasBin: true dependencies: - '@babel/types': 7.21.3 - - /@babel/parser@7.20.7: - resolution: {integrity: sha512-T3Z9oHybU+0vZlY9CiDSJQTD5ZapcW18ZctFMi0MOAl/4BjFF4ul7NVSARLdbGO5vDqy9eQiGTV0LtKfvCYvcg==} - engines: {node: '>=6.0.0'} - hasBin: true - dependencies: - '@babel/types': 7.21.3 + '@babel/types': 7.21.4 /@babel/parser@7.21.4: resolution: {integrity: sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==} @@ -3001,9 +2948,9 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.20.10 + '@babel/compat-data': 7.21.4 '@babel/core': 7.18.13 - '@babel/helper-compilation-targets': 7.20.7(@babel/core@7.18.13) + '@babel/helper-compilation-targets': 7.21.4(@babel/core@7.18.13) '@babel/helper-plugin-utils': 7.20.2 '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.18.13) '@babel/plugin-transform-parameters': 7.18.8(@babel/core@7.18.13) @@ -3015,9 +2962,9 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.20.10 + '@babel/compat-data': 7.21.4 '@babel/core': 7.21.4 - '@babel/helper-compilation-targets': 7.20.7(@babel/core@7.21.4) + '@babel/helper-compilation-targets': 7.21.4(@babel/core@7.21.4) '@babel/helper-plugin-utils': 7.20.2 '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.21.4) '@babel/plugin-transform-parameters': 7.18.8(@babel/core@7.21.4) @@ -3808,7 +3755,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.18.13 - '@babel/helper-compilation-targets': 7.20.7(@babel/core@7.18.13) + '@babel/helper-compilation-targets': 7.21.4(@babel/core@7.18.13) '@babel/helper-function-name': 7.21.0 '@babel/helper-plugin-utils': 7.20.2 dev: true @@ -3820,7 +3767,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.21.4 - '@babel/helper-compilation-targets': 7.20.7(@babel/core@7.21.4) + '@babel/helper-compilation-targets': 7.21.4(@babel/core@7.21.4) '@babel/helper-function-name': 7.21.0 '@babel/helper-plugin-utils': 7.20.2 dev: true @@ -3872,7 +3819,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.18.13 - '@babel/helper-module-transforms': 7.20.11 + '@babel/helper-module-transforms': 7.21.2 '@babel/helper-plugin-utils': 7.20.2 babel-plugin-dynamic-import-node: 2.3.3 transitivePeerDependencies: @@ -3886,7 +3833,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.21.4 - '@babel/helper-module-transforms': 7.20.11 + '@babel/helper-module-transforms': 7.21.2 '@babel/helper-plugin-utils': 7.20.2 babel-plugin-dynamic-import-node: 2.3.3 transitivePeerDependencies: @@ -3900,7 +3847,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.18.13 - '@babel/helper-module-transforms': 7.20.11 + '@babel/helper-module-transforms': 7.21.2 '@babel/helper-plugin-utils': 7.20.2 '@babel/helper-simple-access': 7.20.2 babel-plugin-dynamic-import-node: 2.3.3 @@ -3915,7 +3862,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.21.4 - '@babel/helper-module-transforms': 7.20.11 + '@babel/helper-module-transforms': 7.21.2 '@babel/helper-plugin-utils': 7.20.2 '@babel/helper-simple-access': 7.20.2 babel-plugin-dynamic-import-node: 2.3.3 @@ -3931,7 +3878,7 @@ packages: dependencies: '@babel/core': 7.18.13 '@babel/helper-hoist-variables': 7.18.6 - '@babel/helper-module-transforms': 7.20.11 + '@babel/helper-module-transforms': 7.21.2 '@babel/helper-plugin-utils': 7.20.2 '@babel/helper-validator-identifier': 7.19.1 babel-plugin-dynamic-import-node: 2.3.3 @@ -3947,7 +3894,7 @@ packages: dependencies: '@babel/core': 7.21.4 '@babel/helper-hoist-variables': 7.18.6 - '@babel/helper-module-transforms': 7.20.11 + '@babel/helper-module-transforms': 7.21.2 '@babel/helper-plugin-utils': 7.20.2 '@babel/helper-validator-identifier': 7.19.1 babel-plugin-dynamic-import-node: 2.3.3 @@ -3962,7 +3909,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.18.13 - '@babel/helper-module-transforms': 7.20.11 + '@babel/helper-module-transforms': 7.21.2 '@babel/helper-plugin-utils': 7.20.2 transitivePeerDependencies: - supports-color @@ -3975,7 +3922,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.21.4 - '@babel/helper-module-transforms': 7.20.11 + '@babel/helper-module-transforms': 7.21.2 '@babel/helper-plugin-utils': 7.20.2 transitivePeerDependencies: - supports-color @@ -4190,7 +4137,7 @@ packages: '@babel/helper-module-imports': 7.18.6 '@babel/helper-plugin-utils': 7.20.2 '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.18.13) - '@babel/types': 7.21.3 + '@babel/types': 7.21.4 dev: true /@babel/plugin-transform-react-jsx@7.19.0(@babel/core@7.18.13): @@ -4204,7 +4151,7 @@ packages: '@babel/helper-module-imports': 7.18.6 '@babel/helper-plugin-utils': 7.20.2 '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.18.13) - '@babel/types': 7.21.3 + '@babel/types': 7.21.4 dev: true /@babel/plugin-transform-react-jsx@7.19.0(@babel/core@7.21.4): @@ -4218,7 +4165,7 @@ packages: '@babel/helper-module-imports': 7.18.6 '@babel/helper-plugin-utils': 7.20.2 '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.21.4) - '@babel/types': 7.21.3 + '@babel/types': 7.21.4 dev: true /@babel/plugin-transform-react-pure-annotations@7.18.6(@babel/core@7.18.13): @@ -4465,11 +4412,11 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.20.10 + '@babel/compat-data': 7.21.4 '@babel/core': 7.18.13 - '@babel/helper-compilation-targets': 7.20.7(@babel/core@7.18.13) + '@babel/helper-compilation-targets': 7.21.4(@babel/core@7.18.13) '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-validator-option': 7.18.6 + '@babel/helper-validator-option': 7.21.0 '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.18.6(@babel/core@7.18.13) '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.18.9(@babel/core@7.18.13) '@babel/plugin-proposal-async-generator-functions': 7.18.10(@babel/core@7.18.13) @@ -4535,7 +4482,7 @@ packages: '@babel/plugin-transform-unicode-escapes': 7.18.10(@babel/core@7.18.13) '@babel/plugin-transform-unicode-regex': 7.18.6(@babel/core@7.18.13) '@babel/preset-modules': 0.1.5(@babel/core@7.18.13) - '@babel/types': 7.21.3 + '@babel/types': 7.21.4 babel-plugin-polyfill-corejs2: 0.3.2(@babel/core@7.18.13) babel-plugin-polyfill-corejs3: 0.5.3(@babel/core@7.18.13) babel-plugin-polyfill-regenerator: 0.4.0(@babel/core@7.18.13) @@ -4551,11 +4498,11 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.20.10 + '@babel/compat-data': 7.21.4 '@babel/core': 7.21.4 - '@babel/helper-compilation-targets': 7.20.7(@babel/core@7.21.4) + '@babel/helper-compilation-targets': 7.21.4(@babel/core@7.21.4) '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-validator-option': 7.18.6 + '@babel/helper-validator-option': 7.21.0 '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.18.6(@babel/core@7.21.4) '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.18.9(@babel/core@7.21.4) '@babel/plugin-proposal-async-generator-functions': 7.18.10(@babel/core@7.21.4) @@ -4621,7 +4568,7 @@ packages: '@babel/plugin-transform-unicode-escapes': 7.18.10(@babel/core@7.21.4) '@babel/plugin-transform-unicode-regex': 7.18.6(@babel/core@7.21.4) '@babel/preset-modules': 0.1.5(@babel/core@7.21.4) - '@babel/types': 7.21.3 + '@babel/types': 7.21.4 babel-plugin-polyfill-corejs2: 0.3.2(@babel/core@7.21.4) babel-plugin-polyfill-corejs3: 0.5.3(@babel/core@7.21.4) babel-plugin-polyfill-regenerator: 0.4.0(@babel/core@7.21.4) @@ -4639,7 +4586,7 @@ packages: dependencies: '@babel/core': 7.18.13 '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-validator-option': 7.18.6 + '@babel/helper-validator-option': 7.21.0 '@babel/plugin-transform-flow-strip-types': 7.18.9(@babel/core@7.18.13) dev: true @@ -4652,7 +4599,7 @@ packages: '@babel/helper-plugin-utils': 7.20.2 '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.18.13) '@babel/plugin-transform-dotall-regex': 7.18.6(@babel/core@7.18.13) - '@babel/types': 7.21.3 + '@babel/types': 7.21.4 esutils: 2.0.3 dev: true @@ -4665,7 +4612,7 @@ packages: '@babel/helper-plugin-utils': 7.20.2 '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.21.4) '@babel/plugin-transform-dotall-regex': 7.18.6(@babel/core@7.21.4) - '@babel/types': 7.21.3 + '@babel/types': 7.21.4 esutils: 2.0.3 dev: true @@ -4677,7 +4624,7 @@ packages: dependencies: '@babel/core': 7.18.13 '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-validator-option': 7.18.6 + '@babel/helper-validator-option': 7.21.0 '@babel/plugin-transform-react-display-name': 7.18.6(@babel/core@7.18.13) '@babel/plugin-transform-react-jsx': 7.19.0(@babel/core@7.18.13) '@babel/plugin-transform-react-jsx-development': 7.18.6(@babel/core@7.18.13) @@ -4692,7 +4639,7 @@ packages: dependencies: '@babel/core': 7.21.4 '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-validator-option': 7.18.6 + '@babel/helper-validator-option': 7.21.0 '@babel/plugin-transform-react-display-name': 7.18.6(@babel/core@7.21.4) '@babel/plugin-transform-react-jsx': 7.19.0(@babel/core@7.21.4) '@babel/plugin-transform-react-jsx-development': 7.18.6(@babel/core@7.21.4) @@ -4751,47 +4698,30 @@ packages: resolution: {integrity: sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/code-frame': 7.18.6 - '@babel/parser': 7.20.7 - '@babel/types': 7.21.3 + '@babel/code-frame': 7.21.4 + '@babel/parser': 7.21.4 + '@babel/types': 7.21.4 /@babel/template@7.20.7: resolution: {integrity: sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/code-frame': 7.18.6 - '@babel/parser': 7.20.7 - '@babel/types': 7.21.3 + '@babel/code-frame': 7.21.4 + '@babel/parser': 7.21.4 + '@babel/types': 7.21.4 /@babel/traverse@7.18.13: resolution: {integrity: sha512-N6kt9X1jRMLPxxxPYWi7tgvJRH/rtoU+dbKAPDM44RFHiMH8igdsaSBgFeskhSl/kLWLDUvIh1RXCrTmg0/zvA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/code-frame': 7.18.6 - '@babel/generator': 7.20.7 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-function-name': 7.21.0 - '@babel/helper-hoist-variables': 7.18.6 - '@babel/helper-split-export-declaration': 7.18.6 - '@babel/parser': 7.20.7 - '@babel/types': 7.21.3 - debug: 4.3.4(supports-color@8.1.1) - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - - /@babel/traverse@7.20.12: - resolution: {integrity: sha512-MsIbFN0u+raeja38qboyF8TIT7K0BFzz/Yd/77ta4MsUsmP2RAnidIlwq7d5HFQrH/OZJecGV6B71C4zAgpoSQ==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/code-frame': 7.18.6 - '@babel/generator': 7.20.7 + '@babel/code-frame': 7.21.4 + '@babel/generator': 7.21.4 '@babel/helper-environment-visitor': 7.18.9 '@babel/helper-function-name': 7.21.0 '@babel/helper-hoist-variables': 7.18.6 '@babel/helper-split-export-declaration': 7.18.6 - '@babel/parser': 7.20.7 - '@babel/types': 7.21.3 + '@babel/parser': 7.21.4 + '@babel/types': 7.21.4 debug: 4.3.4(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: @@ -4822,14 +4752,6 @@ packages: '@babel/helper-validator-identifier': 7.19.1 to-fast-properties: 2.0.0 - /@babel/types@7.21.3: - resolution: {integrity: sha512-sBGdETxC+/M4o/zKC0sl6sjWv62WFR/uzxrJ6uYyMLZOUlPnwzw0tKgVHOXxaAd5l2g8pEDM5RZ495GPQI77kg==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-string-parser': 7.19.4 - '@babel/helper-validator-identifier': 7.19.1 - to-fast-properties: 2.0.0 - /@babel/types@7.21.4: resolution: {integrity: sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==} engines: {node: '>=6.9.0'} @@ -6367,6 +6289,29 @@ packages: resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==} dev: false + /@puppeteer/browsers@0.4.0(typescript@5.0.3): + resolution: {integrity: sha512-3iB5pWn9Sr55PKKwqFWSWjLsTKCOEhKNI+uV3BZesgXuA3IhsX8I3hW0HI+3ksMIPkh2mVYzKSpvgq3oicjG2Q==} + engines: {node: '>=14.1.0'} + hasBin: true + peerDependencies: + typescript: '>= 4.7.4' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + debug: 4.3.4(supports-color@8.1.1) + extract-zip: 2.0.1(supports-color@8.1.1) + https-proxy-agent: 5.0.1 + progress: 2.0.3 + proxy-from-env: 1.1.0 + tar-fs: 2.1.1 + typescript: 5.0.3 + unbzip2-stream: 1.4.3 + yargs: 17.7.1 + transitivePeerDependencies: + - supports-color + dev: true + /@rollup/plugin-alias@4.0.2(rollup@3.20.2): resolution: {integrity: sha512-1hv7dBOZZwo3SEupxn4UA2N0EDThqSSS+wI1St1TNTBtOZvUchyIClyHcnDcjjrReTPZ47Faedrhblv4n+T5UQ==} engines: {node: '>=14.0.0'} @@ -6525,7 +6470,7 @@ packages: rollup: optional: true dependencies: - '@types/estree': 1.0.0 + '@types/estree': 1.0.1 estree-walker: 2.0.2 picomatch: 2.3.1 rollup: 2.79.1 @@ -6540,7 +6485,7 @@ packages: rollup: optional: true dependencies: - '@types/estree': 1.0.0 + '@types/estree': 1.0.1 estree-walker: 2.0.2 picomatch: 2.3.1 rollup: 3.20.2 @@ -7567,10 +7512,10 @@ packages: /@storybook/mdx1-csf@0.0.1(@babel/core@7.18.13): resolution: {integrity: sha512-4biZIWWzoWlCarMZmTpqcJNgo/RBesYZwGFbQeXiGYsswuvfWARZnW9RE9aUEMZ4XPn7B1N3EKkWcdcWe/K2tg==} dependencies: - '@babel/generator': 7.20.7 - '@babel/parser': 7.20.7 + '@babel/generator': 7.21.4 + '@babel/parser': 7.21.4 '@babel/preset-env': 7.18.10(@babel/core@7.18.13) - '@babel/types': 7.21.3 + '@babel/types': 7.21.4 '@mdx-js/mdx': 1.6.22 '@types/lodash': 4.14.192 js-string-escape: 1.0.1 @@ -7586,10 +7531,10 @@ packages: /@storybook/mdx1-csf@0.0.1(@babel/core@7.21.4): resolution: {integrity: sha512-4biZIWWzoWlCarMZmTpqcJNgo/RBesYZwGFbQeXiGYsswuvfWARZnW9RE9aUEMZ4XPn7B1N3EKkWcdcWe/K2tg==} dependencies: - '@babel/generator': 7.20.7 - '@babel/parser': 7.20.7 + '@babel/generator': 7.21.4 + '@babel/parser': 7.21.4 '@babel/preset-env': 7.18.10(@babel/core@7.21.4) - '@babel/types': 7.21.3 + '@babel/types': 7.21.4 '@mdx-js/mdx': 1.6.22 '@types/lodash': 4.14.192 js-string-escape: 1.0.1 @@ -7605,10 +7550,10 @@ packages: /@storybook/mdx1-csf@0.0.4(@babel/core@7.18.13)(react@17.0.2): resolution: {integrity: sha512-xxUEMy0D+0G1aSYxbeVNbs+XBU5nCqW4I7awpBYSTywXDv/MJWeC6FDRpj5P1pgfq8j8jWDD5ZDvBQ7syFg0LQ==} dependencies: - '@babel/generator': 7.20.7 - '@babel/parser': 7.20.7 + '@babel/generator': 7.21.4 + '@babel/parser': 7.21.4 '@babel/preset-env': 7.18.10(@babel/core@7.18.13) - '@babel/types': 7.21.3 + '@babel/types': 7.21.4 '@mdx-js/mdx': 1.6.22 '@mdx-js/react': 1.6.22(react@17.0.2) '@types/lodash': 4.14.192 @@ -7989,7 +7934,7 @@ packages: resolution: {integrity: sha512-KnH2MnJUzmFNPW6RIKfd+zf2Wue8mEKX0M3cpX6aKl5ZXrJM1/c/Pc8c2xDNYQCnJO48Sm5ITbMXgqTr3h4jxQ==} engines: {node: '>=12'} dependencies: - '@babel/code-frame': 7.18.6 + '@babel/code-frame': 7.21.4 '@babel/runtime': 7.18.9 '@types/aria-query': 4.2.2 aria-query: 5.0.2 @@ -8003,7 +7948,7 @@ packages: resolution: {integrity: sha512-6YWYPPpxG3e/xOo6HIWwB/58HukkwIVTOaZ0VwdMVjhRUX/01E4FtQbck9GazOOj7MXHc5RBzMrU86iBJHbI+A==} engines: {node: '>=12'} dependencies: - '@babel/code-frame': 7.18.6 + '@babel/code-frame': 7.21.4 '@babel/runtime': 7.18.9 '@types/aria-query': 4.2.2 aria-query: 5.0.2 @@ -8238,20 +8183,20 @@ packages: resolution: {integrity: sha512-xryQlOEIe1TduDWAOphR0ihfebKFSWOXpIsk+70JskCfRfW+xALdnJ0r1ZOTo85F9Qsjk6vtlU7edTYHbls9tA==} dependencies: '@types/cheerio': 0.22.31 - '@types/react': 18.0.38 + '@types/react': 18.2.0 dev: true /@types/eslint-scope@3.7.4: resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==} dependencies: '@types/eslint': 8.4.6 - '@types/estree': 1.0.0 + '@types/estree': 1.0.1 dev: true /@types/eslint@8.4.6: resolution: {integrity: sha512-/fqTbjxyFUaYNO7VcW5g+4npmqVACz1bB7RTHYuLj+PRjw9hrCwrUXVQFpChUS0JsyEFvMZ7U/PfmvWgxJhI9g==} dependencies: - '@types/estree': 1.0.0 + '@types/estree': 1.0.1 '@types/json-schema': 7.0.11 dev: true @@ -8263,8 +8208,8 @@ packages: resolution: {integrity: sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==} dev: true - /@types/estree@1.0.0: - resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==} + /@types/estree@1.0.1: + resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} dev: true /@types/fs-extra@11.0.1: @@ -8481,19 +8426,19 @@ packages: /@types/react-dom@18.0.6: resolution: {integrity: sha512-/5OFZgfIPSwy+YuIBP/FgJnQnsxhZhjjrnxudMddeblOouIodEQ75X14Rr4wGSG/bknL+Omy9iWlLo1u/9GzAA==} dependencies: - '@types/react': 18.0.38 + '@types/react': 18.2.0 dev: true /@types/react-dom@18.0.8: resolution: {integrity: sha512-C3GYO0HLaOkk9dDAz3Dl4sbe4AKUGTCfFIZsz3n/82dPNN8Du533HzKatDxeUYWu24wJgMP1xICqkWk1YOLOIw==} dependencies: - '@types/react': 18.0.38 + '@types/react': 18.2.0 dev: true /@types/react-is@17.0.3: resolution: {integrity: sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==} dependencies: - '@types/react': 18.0.38 + '@types/react': 18.2.0 dev: false /@types/react-test-renderer@17.0.2: @@ -8505,7 +8450,7 @@ packages: /@types/react-transition-group@4.4.5: resolution: {integrity: sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==} dependencies: - '@types/react': 18.0.38 + '@types/react': 18.2.0 dev: false /@types/react@17.0.49: @@ -8530,6 +8475,14 @@ packages: '@types/prop-types': 15.7.5 '@types/scheduler': 0.16.2 csstype: 3.1.0 + dev: true + + /@types/react@18.2.0: + resolution: {integrity: sha512-0FLj93y5USLHdnhIhABk83rm8XEGA7kH3cr+YUlvxoUGp1xNt/DINUMvqPxLyOQMzLmZe8i4RTHbvb8MC7NmrA==} + dependencies: + '@types/prop-types': 15.7.5 + '@types/scheduler': 0.16.2 + csstype: 3.1.0 /@types/resolve@1.17.1: resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} @@ -8581,7 +8534,7 @@ packages: /@types/tern@0.23.4: resolution: {integrity: sha512-JAUw1iXGO1qaWwEOzxTKJZ/5JxVeON9kvGZ/osgZaJImBnyjyn0cjovPsf6FNLmyGY8Vw9DoXZCMlfMkMwHRWg==} dependencies: - '@types/estree': 1.0.0 + '@types/estree': 1.0.1 dev: true /@types/testing-library__jest-dom@5.14.5: @@ -9121,6 +9074,17 @@ packages: vue: 3.2.47 dev: true + /@vitejs/plugin-vue@4.2.0(vite@4.2.1)(vue@3.2.47): + resolution: {integrity: sha512-hYaXFvEKEwyTmwHq2ft7GGeLBvyYLwTM3E5R1jpvzxg9gO4m5PQcTVvj1wEPKoPL8PAt+KAlxo3gyJWnmwzaWQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.0.0 + vue: ^3.2.25 + dependencies: + vite: 4.2.1(@types/node@18.15.11) + vue: 3.2.47 + dev: true + /@vitest/coverage-c8@0.24.5: resolution: {integrity: sha512-955yK/SdSBZPYrSXgXB0F+0JnOX5EY9kSL7ywJ4rNajmkFUhwLjuKm13Xb6YKSyIY/g5WvbBnyowqfNRxBJ3ww==} dependencies: @@ -9176,8 +9140,8 @@ packages: '@babel/helper-module-imports': 7.18.6 '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.21.4) '@babel/template': 7.20.7 - '@babel/traverse': 7.20.12 - '@babel/types': 7.21.3 + '@babel/traverse': 7.21.4 + '@babel/types': 7.21.4 '@vue/babel-helper-vue-transform-on': 1.0.2 camelcase: 6.3.0 html-tags: 3.2.0 @@ -9218,14 +9182,14 @@ packages: /@vue/compiler-sfc@2.7.10: resolution: {integrity: sha512-55Shns6WPxlYsz4WX7q9ZJBL77sKE1ZAYNYStLs6GbhIOMrNtjMvzcob6gu3cGlfpCR4bT7NXgyJ3tly2+Hx8Q==} dependencies: - '@babel/parser': 7.20.7 - postcss: 8.4.19 + '@babel/parser': 7.21.4 + postcss: 8.4.21 source-map: 0.6.1 /@vue/compiler-sfc@3.2.39: resolution: {integrity: sha512-fqAQgFs1/BxTUZkd0Vakn3teKUt//J3c420BgnYgEOoVdTwYpBTSXCMJ88GOBCylmUBbtquGPli9tVs7LzsWIA==} dependencies: - '@babel/parser': 7.20.7 + '@babel/parser': 7.21.4 '@vue/compiler-core': 3.2.39 '@vue/compiler-dom': 3.2.39 '@vue/compiler-ssr': 3.2.39 @@ -9233,7 +9197,7 @@ packages: '@vue/shared': 3.2.39 estree-walker: 2.0.2 magic-string: 0.25.9 - postcss: 8.4.19 + postcss: 8.4.21 source-map: 0.6.1 /@vue/compiler-sfc@3.2.47: @@ -9516,13 +9480,13 @@ packages: read-pkg-up: 9.1.0 dev: true - /@wdio/config@8.7.0: - resolution: {integrity: sha512-zGUaM8PVbp0iDDlhUqWzFpwn2V3nko2k6/C80VggYAqNtmoE1R20DcYqknl8wZUZVlRNcpFVx98G3HqWf5kCQw==} + /@wdio/config@8.8.0: + resolution: {integrity: sha512-gm8gXqpiIR0EU9Blkqmxe+xsEoKS2EXpWrKlx2JXyx3Yf7By0UNsZVZHMSO8lLunzUjYIntpWYpmKmBmnlrnKQ==} engines: {node: ^16.13 || >=18} dependencies: '@wdio/logger': 8.6.6 - '@wdio/types': 8.7.0 - '@wdio/utils': 8.7.0 + '@wdio/types': 8.8.0 + '@wdio/utils': 8.8.0 decamelize: 6.0.0 deepmerge-ts: 5.0.0 glob: 9.3.0 @@ -9558,8 +9522,8 @@ packages: '@types/node': 18.16.0 dev: true - /@wdio/types@8.7.0: - resolution: {integrity: sha512-baiWFVR28mOdI7gI9802cnicGmlbfSZvhLWjd0cD2ep8BhvdengEWyj4GG+qqjk9ZfraBgHunDT+cB6hdPIPow==} + /@wdio/types@8.8.0: + resolution: {integrity: sha512-Ai6yIlwWB32FUfvQKCqSa6nSyHIhSF5BOU9OfE7I2XYkLAJTxu8B6NORHQ+rgoppHSWc4D2V9r21y3etF8AGnQ==} engines: {node: ^16.13 || >=18} dependencies: '@types/node': 18.16.0 @@ -9575,12 +9539,12 @@ packages: p-iteration: 1.1.8 dev: true - /@wdio/utils@8.7.0: - resolution: {integrity: sha512-2g2CSNRp5yJEOww2S3cLtTmyJ0PEIEbhff0nL3R0d0bkDDxuO198iUr0Oszay3KmVqg+Jt0mZlhFlNIa593WnQ==} + /@wdio/utils@8.8.0: + resolution: {integrity: sha512-JUl1AwdtrJ3GzwtEmLyLohh29ycKkTKQ9S7K5Tc3p4kC3d9YmFKsifVj9riyJUFFrbICO0d35O63kNzsVMYj/w==} engines: {node: ^16.13 || >=18} dependencies: '@wdio/logger': 8.6.6 - '@wdio/types': 8.7.0 + '@wdio/types': 8.8.0 import-meta-resolve: 2.2.2 p-iteration: 1.1.8 dev: true @@ -10649,7 +10613,7 @@ packages: '@babel/core': 7.20.5 '@babel/helper-module-imports': 7.16.0 '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.20.5) - '@babel/types': 7.21.3 + '@babel/types': 7.21.4 html-entities: 2.3.2 dev: true @@ -10666,7 +10630,7 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.20.10 + '@babel/compat-data': 7.21.4 '@babel/core': 7.18.13 '@babel/helper-define-polyfill-provider': 0.3.2(@babel/core@7.18.13) semver: 6.3.0 @@ -10679,7 +10643,7 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.20.10 + '@babel/compat-data': 7.21.4 '@babel/core': 7.21.4 '@babel/helper-define-polyfill-provider': 0.3.2(@babel/core@7.21.4) semver: 6.3.0 @@ -12799,20 +12763,20 @@ packages: - utf-8-validate dev: true - /devtools@8.7.0(typescript@5.0.3): - resolution: {integrity: sha512-rl6yShHkh624wkCN9WWb7uUxGsgIfUAL2v6fcHQvPmI42BRRkbiXURq2oRds9fup3qyCv2UPeAlYtf4LQerkDQ==} + /devtools@8.8.0(typescript@5.0.3): + resolution: {integrity: sha512-FfvMEald7LtXIA12oo6wStlxSlAFy3NMAkVAHmu23g8jYhuhl2ASQQzVUFlBHKhVqLvbwSF0VuPZzaPRoz3uDQ==} engines: {node: ^16.13 || >=18} dependencies: '@types/node': 18.16.0 - '@wdio/config': 8.7.0 + '@wdio/config': 8.8.0 '@wdio/logger': 8.6.6 '@wdio/protocols': 8.6.6 - '@wdio/types': 8.7.0 - '@wdio/utils': 8.7.0 + '@wdio/types': 8.8.0 + '@wdio/utils': 8.8.0 chrome-launcher: 0.15.1 edge-paths: 3.0.5 import-meta-resolve: 2.2.2 - puppeteer-core: 19.8.3(typescript@5.0.3) + puppeteer-core: 19.8.5(typescript@5.0.3) query-selector-shadow-dom: 1.0.1 ua-parser-js: 1.0.34 uuid: 9.0.0 @@ -15078,7 +15042,7 @@ packages: resolution: {integrity: sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==} engines: {node: '>= 4.0'} os: [darwin] - deprecated: fsevents 1 will break on node v14+ and could be using insecure binaries. Upgrade to fsevents 2. + deprecated: The v1 package contains DANGEROUS / INSECURE binaries. Upgrade to safe fsevents v2 requiresBuild: true dependencies: bindings: 1.5.0 @@ -15485,8 +15449,8 @@ packages: - encoding dev: true - /happy-dom@9.1.7: - resolution: {integrity: sha512-tLkzW0w9EclIsV75hlCFStJa7CYSEUe+OVU8vK+3wzSvzFeXrGnCujuMcYQAPUXDl1CXoQ2ySaTZcqt3ZBJbSw==} + /happy-dom@9.1.9: + resolution: {integrity: sha512-OMbnoknA7iNNG/5fwt1JckCKc53QLLFo2ljzit1pCV9SC1TYwcQj0obq0QUTeqIf2p2skbFG69bo19YoSj/1DA==} dependencies: css.escape: 1.5.1 he: 1.2.0 @@ -16429,7 +16393,7 @@ packages: /is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} dependencies: - '@types/estree': 1.0.0 + '@types/estree': 1.0.1 dev: true /is-regex@1.1.4: @@ -16991,7 +16955,7 @@ packages: resolution: {integrity: sha512-wRMAQt3HrLpxSubdnzOo68QoTfQ+NLXFzU0Heb18ZUzO2S9GgaXNEdQ4rpd0fI9dq2NXkpCk1IUWSqzYKji64A==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@babel/code-frame': 7.18.6 + '@babel/code-frame': 7.21.4 '@jest/types': 29.0.1 '@types/stack-utils': 2.0.1 chalk: 4.1.2 @@ -18480,8 +18444,8 @@ packages: brace-expansion: 2.0.1 dev: true - /minimatch@8.0.4: - resolution: {integrity: sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==} + /minimatch@9.0.0: + resolution: {integrity: sha512-0jJj8AvgKqWN05mrwuqi8QYKx1WmYSUoKSxu5Qhs9prezTz10sxAHGNZe9J9cqIJzta8DWsleh2KaVaLl6Ru2w==} engines: {node: '>=16 || 14 >=14.17'} dependencies: brace-expansion: 2.0.1 @@ -19497,7 +19461,7 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} dependencies: - '@babel/code-frame': 7.18.6 + '@babel/code-frame': 7.21.4 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -19972,14 +19936,6 @@ packages: source-map: 0.6.1 dev: true - /postcss@8.4.19: - resolution: {integrity: sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA==} - engines: {node: ^10 || ^12 || >=14} - dependencies: - nanoid: 3.3.4 - picocolors: 1.0.0 - source-map-js: 1.0.2 - /postcss@8.4.21: resolution: {integrity: sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==} engines: {node: ^10 || ^12 || >=14} @@ -20247,8 +20203,8 @@ packages: - utf-8-validate dev: true - /puppeteer-core@19.8.3(typescript@5.0.3): - resolution: {integrity: sha512-+gPT43K3trUIr+7+tNjCg95vMgM9RkFIeeyck3SXUTBpGkrf6sxfWckncqY8MnzHfRcyapvztKNmqvJ+Ngukyg==} + /puppeteer-core@19.8.5(typescript@5.0.3): + resolution: {integrity: sha512-zoGhim/oBQbkND6h4Xz4X7l5DkWVH9wH7z0mVty5qa/c0P1Yad47t/npVtt2xS10BiQwzztWKx7Pa2nJ5yykdw==} engines: {node: '>=14.14.0'} peerDependencies: typescript: '>= 4.7.4' @@ -20256,6 +20212,7 @@ packages: typescript: optional: true dependencies: + '@puppeteer/browsers': 0.4.0(typescript@5.0.3) chromium-bidi: 0.4.6(devtools-protocol@0.0.1107588) cross-fetch: 3.1.5 debug: 4.3.4(supports-color@8.1.1) @@ -20436,7 +20393,7 @@ packages: hasBin: true dependencies: '@babel/core': 7.21.4 - '@babel/generator': 7.20.7 + '@babel/generator': 7.21.4 '@babel/runtime': 7.18.9 ast-types: 0.14.2 commander: 2.20.3 @@ -20455,7 +20412,7 @@ packages: hasBin: true dependencies: '@babel/core': 7.21.4 - '@babel/generator': 7.20.7 + '@babel/generator': 7.21.4 ast-types: 0.14.2 commander: 2.20.3 doctrine: 3.0.0 @@ -21752,9 +21709,9 @@ packages: peerDependencies: solid-js: ^1.3 dependencies: - '@babel/generator': 7.20.7 + '@babel/generator': 7.21.4 '@babel/helper-module-imports': 7.18.6 - '@babel/types': 7.21.3 + '@babel/types': 7.21.4 solid-js: 1.5.2 dev: true @@ -23148,6 +23105,24 @@ packages: vfile: 4.2.1 dev: true + /unimport@1.3.0(rollup@3.20.2): + resolution: {integrity: sha512-fOkrdxglsHd428yegH0wPH/6IfaSdDeMXtdRGn6en/ccyzc2aaoxiUTMrJyc6Bu+xoa18RJRPMfLUHEzjz8atw==} + dependencies: + '@rollup/pluginutils': 5.0.2(rollup@3.20.2) + escape-string-regexp: 5.0.0 + fast-glob: 3.2.12 + local-pkg: 0.4.3 + magic-string: 0.27.0 + mlly: 1.2.0 + pathe: 1.1.0 + pkg-types: 1.0.2 + scule: 1.0.0 + strip-literal: 1.0.1 + unplugin: 1.3.1 + transitivePeerDependencies: + - rollup + dev: true + /unimport@3.0.5(rollup@3.20.2): resolution: {integrity: sha512-enRW0sVcTBxUZY6R2iumWMUsPbGC5GW9jM89VtF0pFUz9vDPrvEmOAklbSE/a4fqhjW/7dMWBvfLw/IDlBaPdQ==} dependencies: @@ -23334,82 +23309,73 @@ packages: engines: {node: '>= 0.8'} dev: true - /unplugin-auto-import@0.15.2(@vueuse/core@9.13.0)(rollup@3.20.2): - resolution: {integrity: sha512-Wivfu+xccgvEZG8QtZcIvt6napfX9wyOFqM//7FHOtev8+k+dp3ykiqsEl6TODgHmqTTBeQX4Ah1JvRgUNjlkg==} + /unplugin-auto-import@0.12.1(rollup@3.20.2): + resolution: {integrity: sha512-J/3ZORq5YGKG+8D5vLLOgqaHNK77izlVN07mQ752yRLqBNDbJiwPRSnUwwYqH5N6rDay1SqnJCHaUdbJ9QMI2w==} engines: {node: '>=14'} peerDependencies: - '@nuxt/kit': ^3.2.2 '@vueuse/core': '*' peerDependenciesMeta: - '@nuxt/kit': - optional: true '@vueuse/core': optional: true dependencies: '@antfu/utils': 0.7.2 '@rollup/pluginutils': 5.0.2(rollup@3.20.2) - '@vueuse/core': 9.13.0(vue@3.2.47) local-pkg: 0.4.3 - magic-string: 0.30.0 - minimatch: 7.4.5 - unimport: 3.0.5(rollup@3.20.2) + magic-string: 0.27.0 + unimport: 1.3.0(rollup@3.20.2) unplugin: 1.3.1 transitivePeerDependencies: - rollup dev: true - /unplugin-auto-import@0.7.2(esbuild@0.17.15)(rollup@3.20.2)(vite@4.2.1): - resolution: {integrity: sha512-VzaYUa2VByUT70WSFlOXoovyWuwC/8ePKQUC9fhU+BRmvTC7qhCVgChH/NieWMEVgyT+HhacxM+W7xMEOmA+MA==} + /unplugin-auto-import@0.15.2(@vueuse/core@9.13.0)(rollup@3.20.2): + resolution: {integrity: sha512-Wivfu+xccgvEZG8QtZcIvt6napfX9wyOFqM//7FHOtev8+k+dp3ykiqsEl6TODgHmqTTBeQX4Ah1JvRgUNjlkg==} engines: {node: '>=14'} peerDependencies: + '@nuxt/kit': ^3.2.2 '@vueuse/core': '*' peerDependenciesMeta: + '@nuxt/kit': + optional: true '@vueuse/core': optional: true dependencies: - '@antfu/utils': 0.5.2 - '@rollup/pluginutils': 4.2.1 + '@antfu/utils': 0.7.2 + '@rollup/pluginutils': 5.0.2(rollup@3.20.2) + '@vueuse/core': 9.13.0(vue@3.2.47) local-pkg: 0.4.3 - magic-string: 0.26.7 - resolve: 1.22.1 - unplugin: 0.6.3(esbuild@0.17.15)(rollup@3.20.2)(vite@4.2.1) + magic-string: 0.30.0 + minimatch: 7.4.5 + unimport: 3.0.5(rollup@3.20.2) + unplugin: 1.3.1 transitivePeerDependencies: - - esbuild - rollup - - vite - - webpack dev: true - /unplugin-vue-components@0.19.6(esbuild@0.17.15)(rollup@3.20.2)(vite@4.2.1)(vue@3.2.47): - resolution: {integrity: sha512-APvrJ9Hpid1MLT0G4PWerMJgARhNw6dzz0pcCwCxaO2DR7VyvDacMqjOQNC6ukq7FSw3wzD8VH+9i3EFXwkGmw==} + /unplugin-vue-components@0.22.12(rollup@3.20.2)(vue@3.2.47): + resolution: {integrity: sha512-FxyzsuBvMCYPIk+8cgscGBQ345tvwVu+qY5IhE++eorkyvA4Z1TiD/HCiim+Kbqozl10i4K+z+NCa2WO2jexRA==} engines: {node: '>=14'} peerDependencies: '@babel/parser': ^7.15.8 - '@babel/traverse': ^7.15.4 vue: 2 || 3 peerDependenciesMeta: '@babel/parser': optional: true - '@babel/traverse': - optional: true dependencies: - '@antfu/utils': 0.5.2 - '@rollup/pluginutils': 4.2.1 + '@antfu/utils': 0.7.2 + '@rollup/pluginutils': 5.0.2(rollup@3.20.2) chokidar: 3.5.3 debug: 4.3.4(supports-color@8.1.1) fast-glob: 3.2.12 local-pkg: 0.4.3 - magic-string: 0.26.7 + magic-string: 0.27.0 minimatch: 5.1.1 resolve: 1.22.1 - unplugin: 0.6.3(esbuild@0.17.15)(rollup@3.20.2)(vite@4.2.1) + unplugin: 1.3.1 vue: 3.2.47 transitivePeerDependencies: - - esbuild - rollup - supports-color - - vite - - webpack dev: true /unplugin-vue-components@0.24.1(rollup@2.79.1)(vue@3.2.47): @@ -23470,31 +23436,6 @@ packages: - supports-color dev: true - /unplugin@0.6.3(esbuild@0.17.15)(rollup@3.20.2)(vite@4.2.1): - resolution: {integrity: sha512-CoW88FQfCW/yabVc4bLrjikN9HC8dEvMU4O7B6K2jsYMPK0l6iAnd9dpJwqGcmXJKRCU9vwSsy653qg+RK0G6A==} - peerDependencies: - esbuild: '>=0.13' - rollup: ^2.50.0 - vite: ^2.3.0 - webpack: 4 || 5 - peerDependenciesMeta: - esbuild: - optional: true - rollup: - optional: true - vite: - optional: true - webpack: - optional: true - dependencies: - chokidar: 3.5.3 - esbuild: 0.17.15 - rollup: 3.20.2 - vite: 4.2.1(@types/node@18.15.11) - webpack-sources: 3.2.3 - webpack-virtual-modules: 0.4.6 - dev: true - /unplugin@1.3.1: resolution: {integrity: sha512-h4uUTIvFBQRxUKS2Wjys6ivoeofGhxzTe2sRWlooyjHXVttcVfV/JiavNd3d4+jty0SVV0dxGw9AkY9MwiaCEw==} dependencies: @@ -23917,7 +23858,7 @@ packages: dependencies: '@docsearch/css': 3.3.3 '@docsearch/js': 3.3.3(@algolia/client-search@4.14.2) - '@vitejs/plugin-vue': 4.1.0(vite@4.2.1)(vue@3.2.47) + '@vitejs/plugin-vue': 4.2.0(vite@4.2.1)(vue@3.2.47) '@vue/devtools-api': 6.5.0 '@vueuse/core': 9.13.0(vue@3.2.47) body-scroll-lock: 4.0.0-beta.0 @@ -24174,17 +24115,17 @@ packages: - utf-8-validate dev: true - /webdriver@8.7.0: - resolution: {integrity: sha512-UYstwTX6OtYJUnkUv6hI1XmSzrPMPqglHPTcanhS+EI1Ve4JmA8bSmkzL08gmiAroBjgLtqzpEcL5KuyAPEiLw==} + /webdriver@8.8.0: + resolution: {integrity: sha512-LqO06orjZlODkQm5npEkuXtBEdVc+tKZAzX468Wra71U9naUZN7YrMjboHvbtsUuiRLWt0RzByO5VCWRS0o/Zg==} engines: {node: ^16.13 || >=18} dependencies: '@types/node': 18.16.0 '@types/ws': 8.5.4 - '@wdio/config': 8.7.0 + '@wdio/config': 8.8.0 '@wdio/logger': 8.6.6 '@wdio/protocols': 8.6.6 - '@wdio/types': 8.7.0 - '@wdio/utils': 8.7.0 + '@wdio/types': 8.8.0 + '@wdio/utils': 8.8.0 deepmerge-ts: 5.0.0 got: 12.6.0 ky: 0.33.3 @@ -24231,35 +24172,35 @@ packages: - utf-8-validate dev: true - /webdriverio@8.7.0(typescript@5.0.3): - resolution: {integrity: sha512-drZKdS1IdGeYOKxZWAh/AOHgbz9oGAM3YVcgKun3npLTZGNh7jSfsrUYRmJe+px8Xz7Ay5Qz30QgqtbEqk7kBQ==} + /webdriverio@8.8.0(typescript@5.0.3): + resolution: {integrity: sha512-QMce84O2CX/T3GUowO0/4V16RFE5METrQ3fjeWx0oLq/6rvZJe3X97Tdk5Xnlpcma6Ot+zhIsU8zWsMgi07wCA==} engines: {node: ^16.13 || >=18} dependencies: '@types/node': 18.16.0 - '@wdio/config': 8.7.0 + '@wdio/config': 8.8.0 '@wdio/logger': 8.6.6 '@wdio/protocols': 8.6.6 '@wdio/repl': 8.6.6 - '@wdio/types': 8.7.0 - '@wdio/utils': 8.7.0 + '@wdio/types': 8.8.0 + '@wdio/utils': 8.8.0 archiver: 5.3.1 aria-query: 5.0.2 css-shorthand-properties: 1.1.1 css-value: 0.0.1 - devtools: 8.7.0(typescript@5.0.3) + devtools: 8.8.0(typescript@5.0.3) devtools-protocol: 0.0.1124027 grapheme-splitter: 1.0.4 import-meta-resolve: 2.2.2 is-plain-obj: 4.1.0 lodash.clonedeep: 4.5.0 lodash.zip: 4.2.0 - minimatch: 8.0.4 - puppeteer-core: 19.8.3(typescript@5.0.3) + minimatch: 9.0.0 + puppeteer-core: 19.8.5(typescript@5.0.3) query-selector-shadow-dom: 1.0.1 resq: 1.11.0 rgb2hex: 0.2.5 serialize-error: 8.1.0 - webdriver: 8.7.0 + webdriver: 8.8.0 transitivePeerDependencies: - bufferutil - encoding @@ -24349,10 +24290,6 @@ packages: - supports-color dev: true - /webpack-virtual-modules@0.4.6: - resolution: {integrity: sha512-5tyDlKLqPfMqjT3Q9TAqf2YqjwmnUleZwzJi1A5qXnlBCdj2AtOJ6wAWdglTIDOPgOiOrXeBeFcsQ8+aGQ6QbA==} - dev: true - /webpack-virtual-modules@0.5.0: resolution: {integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==} dev: true From b21e0d933c60432f1fae043d10a5068ae03544e2 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 25 Apr 2023 19:00:12 +0200 Subject: [PATCH 02/22] chore: init mocker --- packages/browser/src/client/main.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/browser/src/client/main.ts b/packages/browser/src/client/main.ts index 8e6deddc9b7b..91fff595fd49 100644 --- a/packages/browser/src/client/main.ts +++ b/packages/browser/src/client/main.ts @@ -8,6 +8,7 @@ import { setupConsoleLogSpy } from './logger' import { createSafeRpc, rpc, rpcDone } from './rpc' import { setupDialogsSpy } from './dialog' import { BrowserSnapshotEnvironment } from './snapshot' +import { VitestBrowserClientMocker } from './mocker' // @ts-expect-error mocking some node apis globalThis.process = { env: {}, argv: [], cwd: () => '/', stdout: { write: () => {} }, nextTick: cb => cb() } @@ -80,6 +81,8 @@ ws.addEventListener('open', async () => { prepare: 0, }, } + // @ts-expect-error mocking vitest apis + globalThis.__vitest_mocker__ = new VitestBrowserClientMocker() const paths = getQueryPaths() From f8b31ac836be02cb43a0d2336b0e8ac42d7e2d23 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 25 Apr 2023 19:05:18 +0200 Subject: [PATCH 03/22] chore: throw error on "browser" in poolMatchGlobs --- packages/vitest/src/node/pool.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/vitest/src/node/pool.ts b/packages/vitest/src/node/pool.ts index 35854f4d9fe1..e4912c552f57 100644 --- a/packages/vitest/src/node/pool.ts +++ b/packages/vitest/src/node/pool.ts @@ -42,8 +42,10 @@ export function createPool(ctx: Vitest): ProcessPool { function getPoolName([project, file]: WorkspaceSpec) { for (const [glob, pool] of project.config.poolMatchGlobs || []) { + if (pool === 'browser') + throw new Error('Since Vitest 0.31.0 "browser" pool is not supported in "poolMatchGlobs". You can create a workspace to run some of your tests in browser in paralle. Read more: https://vitest.dev/guide/workspace') if (mm.isMatch(file, glob, { cwd: project.config.root })) - return pool + return pool as VitestPool } return getDefaultPoolName(project) } From 5f72426803336c9184c8ff7f6c7997ead9071b29 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 25 Apr 2023 20:05:38 +0200 Subject: [PATCH 04/22] chore: support "vi.dynamicImportSettled" --- .../vue/test/__snapshots__/basic.test.ts.snap | 2 +- packages/browser/package.json | 1 + packages/browser/src/client/index.html | 24 ++ packages/browser/src/client/main.ts | 3 +- packages/browser/src/client/moduleGraph.ts | 23 -- packages/utils/src/descriptors.ts | 10 +- .../vitest/src/integrations/browser/server.ts | 2 + packages/vitest/src/node/mock.ts | 261 ++---------------- pnpm-lock.yaml | 15 +- test/browser/src/actions.ts | 3 + test/browser/src/calculator.ts | 8 + test/browser/test/mocked.test.ts | 12 + 12 files changed, 89 insertions(+), 275 deletions(-) delete mode 100644 packages/browser/src/client/moduleGraph.ts create mode 100644 test/browser/src/actions.ts create mode 100644 test/browser/src/calculator.ts create mode 100644 test/browser/test/mocked.test.ts diff --git a/examples/vue/test/__snapshots__/basic.test.ts.snap b/examples/vue/test/__snapshots__/basic.test.ts.snap index 45642c5659b1..522e894f6726 100644 --- a/examples/vue/test/__snapshots__/basic.test.ts.snap +++ b/examples/vue/test/__snapshots__/basic.test.ts.snap @@ -1,4 +1,4 @@ -// Vitest Snapshot v1 +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`mount component 1`] = ` "
4 x 2 = 8
diff --git a/packages/browser/package.json b/packages/browser/package.json index 77b8fd38d8ae..55b2e0fc5266 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -50,6 +50,7 @@ "@vitest/runner": "workspace:*", "@vitest/ui": "workspace:*", "@vitest/ws-client": "workspace:*", + "rollup": "3.20.2", "vitest": "workspace:*" } } diff --git a/packages/browser/src/client/index.html b/packages/browser/src/client/index.html index 284367484299..0cdeb4cea5cb 100644 --- a/packages/browser/src/client/index.html +++ b/packages/browser/src/client/index.html @@ -24,6 +24,30 @@ + diff --git a/packages/browser/src/client/main.ts b/packages/browser/src/client/main.ts index 91fff595fd49..f1cee975db8c 100644 --- a/packages/browser/src/client/main.ts +++ b/packages/browser/src/client/main.ts @@ -73,7 +73,8 @@ ws.addEventListener('open', async () => { globalThis.__vitest_worker__ = { config, browserHashMap, - moduleCache: new Map(), + // @ts-expect-error untyped global for internal use + moduleCache: globalThis.__vitest_module_cache__, rpc: client.rpc, safeRpc, durations: { diff --git a/packages/browser/src/client/moduleGraph.ts b/packages/browser/src/client/moduleGraph.ts deleted file mode 100644 index 302fc6974a7d..000000000000 --- a/packages/browser/src/client/moduleGraph.ts +++ /dev/null @@ -1,23 +0,0 @@ -type ModuleObject = Readonly> -type HijackedModuleObject = Record - -const modules = new WeakMap() - -const moduleCache = new Map() - -// this method receives a module object or "import" promise that it resolves and keeps track of -// and returns a hijacked module object that can be used to mock module exports -export function __vitest_wrap_module__(module: ModuleObject | Promise): HijackedModuleObject | Promise { - if (module instanceof Promise) { - moduleCache.set(module, { promise: module, evaluted: false }) - return module - .then(m => __vitest_wrap_module__(m)) - .finally(() => moduleCache.delete(module)) - } - const cached = modules.get(module) - if (cached) - return cached - const hijacked = Object.assign({}, module) - modules.set(module, hijacked) - return hijacked as HijackedModuleObject -} diff --git a/packages/utils/src/descriptors.ts b/packages/utils/src/descriptors.ts index 2d961b60d07b..d20f5ea7093c 100644 --- a/packages/utils/src/descriptors.ts +++ b/packages/utils/src/descriptors.ts @@ -1,6 +1,10 @@ -import concordance from 'concordance' +import * as concordance from 'concordance' import { getColors } from './colors' +const concordanceModule = 'default' in concordance + ? concordance.default + : concordance as any + interface DisplayOptions { theme?: any maxDepth?: number @@ -89,6 +93,6 @@ export function getConcordanceTheme() { } } -export function diffDescriptors(actual: unknown, expected: unknown, options: DisplayOptions) { - return concordance.diff(expected, actual, options) +export function diffDescriptors(actual: unknown, expected: unknown, options: DisplayOptions): string { + return concordanceModule.diff(expected, actual, options) } diff --git a/packages/vitest/src/integrations/browser/server.ts b/packages/vitest/src/integrations/browser/server.ts index 00f0f8bf86a8..31fd84b86c2d 100644 --- a/packages/vitest/src/integrations/browser/server.ts +++ b/packages/vitest/src/integrations/browser/server.ts @@ -7,6 +7,7 @@ import { ensurePackageInstalled } from '../../node/pkg' import { resolveApiServerConfig } from '../../node/config' import { CoverageTransform } from '../../node/plugins/coverageTransform' import type { WorkspaceProject } from '../../node/workspace' +import { ESMMockerPlugin } from '../../node/plugins/esm-mocker' export async function createBrowserServer(project: WorkspaceProject, options: UserConfig) { const root = project.config.root @@ -33,6 +34,7 @@ export async function createBrowserServer(project: WorkspaceProject, options: Us plugins: [ (await import('@vitest/browser')).default('/'), CoverageTransform(project.ctx), + ESMMockerPlugin(project), { enforce: 'post', name: 'vitest:browser:config', diff --git a/packages/vitest/src/node/mock.ts b/packages/vitest/src/node/mock.ts index 400a278c82be..b0c6ce7379e7 100644 --- a/packages/vitest/src/node/mock.ts +++ b/packages/vitest/src/node/mock.ts @@ -2,11 +2,8 @@ import MagicString from 'magic-string' import { Parser } from 'acorn' import { findNodeAround, simple as walk } from 'acorn-walk' import type { CallExpression, Expression, Identifier, ImportDeclaration, ImportExpression, VariableDeclaration } from 'estree' -import type { DecodedSourceMap, RawSourceMap } from '@ampproject/remapping' -import type { SourceMap } from 'rollup' -import type { TransformResult, ViteDevServer } from 'vite' -import remapping from '@ampproject/remapping' -import { getCallLastIndex, toArray } from '../utils' +import type { ViteDevServer } from 'vite' +import { toArray } from '../utils' import type { WorkspaceProject } from './workspace' import type { Vitest } from './core' @@ -27,8 +24,6 @@ function getAcornParser(server: ViteDevServer) { return parser } -const hoistRegexp = /^[ \t]*\b(?:__vite_ssr_import_\d+__\.)?((?:vitest|vi)\s*.\s*(mock|unmock)\(["`'\s]+(.*[@\w_-]+)["`'\s]+)[),]{1};?/gm - const API_NOT_FOUND_ERROR = `There are some problems in resolving the mocks API. You may encounter this issue when importing the mocks API from another module other than 'vitest'. @@ -39,227 +34,7 @@ To fix this issue you can either: const API_NOT_FOUND_CHECK = 'if (typeof globalThis.vi === "undefined" && typeof globalThis.vitest === "undefined") ' + `{ throw new Error(${JSON.stringify(API_NOT_FOUND_ERROR)}) }\n` -export function hoistModuleMocks(mod: TransformResult, vitestPath: string): TransformResult { - if (!mod.code) - return mod - const m = hoistCodeMocks(mod.code) - - if (m) { - const vitestRegexp = new RegExp(`const __vite_ssr_import_\\d+__ = await __vite_ssr_import__\\("(?:\/@fs\/?)?(?:${vitestPath}|vitest)"\\);`, 'gm') - // hoist vitest imports in case it was used inside vi.mock factory #425 - const vitestImports = mod.code.matchAll(vitestRegexp) - let found = false - - for (const match of vitestImports) { - const indexStart = match.index! - const indexEnd = match[0].length + indexStart - m.remove(indexStart, indexEnd) - m.prepend(`${match[0]}\n`) - found = true - } - - // if no vitest import found, check if the mock API is reachable after the hoisting - if (!found) { - m.prepend('if (typeof globalThis.vi === "undefined" && typeof globalThis.vitest === "undefined") ' - + `{ throw new Error(${JSON.stringify(API_NOT_FOUND_ERROR)}) }\n`) - } - - return { - ...mod, - code: m.toString(), - map: mod.map - ? combineSourcemaps( - mod.map.file, - [ - { - ...m.generateMap({ hires: true }), - sourcesContent: mod.map.sourcesContent, - } as RawSourceMap, - mod.map as RawSourceMap, - ], - ) as SourceMap - : null, - } - } - - return mod -} - -function hoistCodeMocks(code: string) { - let m: MagicString | undefined - const mocks = code.matchAll(hoistRegexp) - - for (const mockResult of mocks) { - const lastIndex = getMockLastIndex(code.slice(mockResult.index!)) - - if (lastIndex === null) - continue - - const startIndex = mockResult.index! - - const { insideComment, insideString } = getIndexStatus(code, startIndex) - - if (insideComment || insideString) - continue - - const endIndex = startIndex + lastIndex - - m ??= new MagicString(code) - - m.prepend(`${m.slice(startIndex, endIndex)}\n`) - m.remove(startIndex, endIndex) - } - - return m -} - -function escapeToLinuxLikePath(path: string) { - if (/^[A-Z]:/.test(path)) - return path.replace(/^([A-Z]):\//, '/windows/$1/') - - if (/^\/[^/]/.test(path)) - return `/linux${path}` - - return path -} - -function unescapeToLinuxLikePath(path: string) { - if (path.startsWith('/linux/')) - return path.slice('/linux'.length) - - if (path.startsWith('/windows/')) - return path.replace(/^\/windows\/([A-Z])\//, '$1:/') - - return path -} - -// based on https://github.com/vitejs/vite/blob/6b40f03574cd71a17cbe564bc63adebb156ff06e/packages/vite/src/node/utils.ts#L727 -const nullSourceMap: RawSourceMap = { - names: [], - sources: [], - mappings: '', - version: 3, -} -export function combineSourcemaps( - filename: string, - sourcemapList: Array, - excludeContent = true, -): RawSourceMap { - if ( - sourcemapList.length === 0 - || sourcemapList.every(m => m.sources.length === 0) - ) - return { ...nullSourceMap } - - // hack for parse broken with normalized absolute paths on windows (C:/path/to/something). - // escape them to linux like paths - // also avoid mutation here to prevent breaking plugin's using cache to generate sourcemaps like vue (see #7442) - sourcemapList = sourcemapList.map((sourcemap) => { - const newSourcemaps = { ...sourcemap } - newSourcemaps.sources = sourcemap.sources.map(source => - source ? escapeToLinuxLikePath(source) : null, - ) - if (sourcemap.sourceRoot) - newSourcemaps.sourceRoot = escapeToLinuxLikePath(sourcemap.sourceRoot) - - return newSourcemaps - }) - const escapedFilename = escapeToLinuxLikePath(filename) - - // We don't declare type here so we can convert/fake/map as RawSourceMap - let map // : SourceMap - let mapIndex = 1 - const useArrayInterface - = sourcemapList.slice(0, -1).find(m => m.sources.length !== 1) === undefined - if (useArrayInterface) { - map = remapping(sourcemapList, () => null, excludeContent) - } - else { - map = remapping( - sourcemapList[0], - (sourcefile) => { - if (sourcefile === escapedFilename && sourcemapList[mapIndex]) - return sourcemapList[mapIndex++] - - else - return null - }, - excludeContent, - ) - } - if (!map.file) - delete map.file - - // unescape the previous hack - map.sources = map.sources.map(source => - source ? unescapeToLinuxLikePath(source) : source, - ) - map.file = filename - - return map as RawSourceMap -} - -function getMockLastIndex(code: string): number | null { - const index = getCallLastIndex(code) - if (index === null) - return null - return code[index + 1] === ';' ? index + 2 : index + 1 -} - -function getIndexStatus(code: string, from: number) { - let index = 0 - let commentStarted = false - let commentEnded = true - let multilineCommentStarted = false - let multilineCommentEnded = true - let inString: string | null = null - let beforeChar: string | null = null - - while (index <= from) { - const char = code[index] - const sub = code[index] + code[index + 1] - - if (!inString) { - if (sub === '/*') { - multilineCommentStarted = true - multilineCommentEnded = false - } - if (sub === '*/' && multilineCommentStarted) { - multilineCommentStarted = false - multilineCommentEnded = true - } - if (sub === '//') { - commentStarted = true - commentEnded = false - } - if ((char === '\n' || sub === '\r\n') && commentStarted) { - commentStarted = false - commentEnded = true - } - } - - if (!multilineCommentStarted && !commentStarted) { - const isCharString = char === '"' || char === '\'' || char === '`' - - if (isCharString && beforeChar !== '\\') { - if (inString === char) - inString = null - else if (!inString) - inString = char - } - } - - beforeChar = char - index++ - } - - return { - insideComment: !multilineCommentEnded || !commentEnded, - insideString: inString !== null, - } -} - -const regexpHoistable = /^[ \t]*\b(vi|vitest)\s*\.\s*(mock|unmock|hoist)/m +const regexpHoistable = /^[ \t]*\b(vi|vitest)\s*\.\s*(mock|unmock|hoist)\(/m const hashbangRE = /^#!.*\n/ function isIdentifier(node: any): node is Positioned { @@ -287,6 +62,9 @@ function transformImportSpecifiers(node: ImportDeclaration) { return null }).filter(Boolean).join(', ') + if (!dynamicImports.length) + return '' + return `{ ${dynamicImports} }` } @@ -294,7 +72,7 @@ export function transformMockableFile(project: WorkspaceProject | Vitest, id: st const hasMocks = regexpHoistable.test(source) const hijackEsm = project.config.slowHijackESM ?? false - // we don't need to constrol __vitest_module__ in Node.js, + // we don't need to control __vitest_module__ in Node.js, // because we control the module resolution directly, // but we stil need to hoist mocks everywhere if (!hijackEsm && !hasMocks) @@ -329,16 +107,25 @@ export function transformMockableFile(project: WorkspaceProject | Vitest, id: st // in browser environment it will wrap the module value with "vitest_wrap_module" function // that returns a proxy to the module so that named exports can be mocked const transformImportDeclaration = (node: ImportDeclaration) => { + const specifiers = transformImportSpecifiers(node) // if we don't hijack ESM and process this file, then we definetly have mocks, // so we need to transform imports into dynamic ones, so "vi.mock" can be executed before - if (!hijackEsm) - return `const ${transformImportSpecifiers(node)} = await import('${node.source.value}')\n` + if (!hijackEsm) { + return specifiers + ? `const ${specifiers} = await import('${node.source.value}')\n` + : `await import('${node.source.value}')\n` + } const moduleName = `__vitest_module_${idx++}__` - const destructured = `const ${transformImportSpecifiers(node)} = __vitest_wrap_module__(${moduleName})` - if (hasMocks) - return `const ${moduleName} = await import('${node.source.value}')\n${destructured}` - return `import * as ${moduleName} from '${node.source.value}'\n${destructured}` + const destructured = `const ${specifiers} = __vitest_wrap_module__(${moduleName})\n` + if (hasMocks) { + return specifiers + ? `const ${moduleName} = await import('${node.source.value}')\n${destructured}` + : `await __vitest_wrap_module__(import('${node.source.value}'))\n` + } + return specifiers + ? `import * as ${moduleName} from '${node.source.value}'\n${destructured}` + : `import '${node.source.value}'\n` } walk(ast, { @@ -409,12 +196,12 @@ export function transformMockableFile(project: WorkspaceProject | Vitest, id: st }) if (hasMocks) - hoistedCalls += 'await __vitest_mocker__.prepare()\n' + hoistedCalls += '\nawait __vitest_mocker__.prepare()\n' magicString.appendLeft( hoistIndex, hoistedVitestImports - + (hoistedVitestImports ? '' : API_NOT_FOUND_CHECK) + + ((!hoistedVitestImports && hoistedCalls) ? API_NOT_FOUND_CHECK : '') + hoistedCalls, ) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9ddd8a6c3815..ffdd5e2495f4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -321,7 +321,7 @@ importers: version: 18.16.0 '@types/react': specifier: latest - version: 18.0.38 + version: 18.2.0 '@vitejs/plugin-react': specifier: latest version: 4.0.0(vite@4.2.1) @@ -867,6 +867,9 @@ importers: '@vitest/ws-client': specifier: workspace:* version: link:../ws-client + rollup: + specifier: 3.20.2 + version: 3.20.2 vitest: specifier: workspace:* version: link:../vitest @@ -8469,14 +8472,6 @@ packages: csstype: 3.1.0 dev: true - /@types/react@18.0.38: - resolution: {integrity: sha512-ExsidLLSzYj4cvaQjGnQCk4HFfVT9+EZ9XZsQ8Hsrcn8QNgXtpZ3m9vSIC2MWtx7jHictK6wYhQgGh6ic58oOw==} - dependencies: - '@types/prop-types': 15.7.5 - '@types/scheduler': 0.16.2 - csstype: 3.1.0 - dev: true - /@types/react@18.2.0: resolution: {integrity: sha512-0FLj93y5USLHdnhIhABk83rm8XEGA7kH3cr+YUlvxoUGp1xNt/DINUMvqPxLyOQMzLmZe8i4RTHbvb8MC7NmrA==} dependencies: @@ -11818,7 +11813,7 @@ packages: dev: true /concat-map@0.0.1: - resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} /concat-stream@1.6.2: resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} diff --git a/test/browser/src/actions.ts b/test/browser/src/actions.ts new file mode 100644 index 000000000000..55ae85133596 --- /dev/null +++ b/test/browser/src/actions.ts @@ -0,0 +1,3 @@ +export function plus(a: number, b: number) { + return a + b +} diff --git a/test/browser/src/calculator.ts b/test/browser/src/calculator.ts new file mode 100644 index 000000000000..9d9d67cfe878 --- /dev/null +++ b/test/browser/src/calculator.ts @@ -0,0 +1,8 @@ +import { plus } from './actions' + +export function calculator(operation: 'plus', a: number, b: number) { + if (operation === 'plus') + return plus(a, b) + + throw new Error('unknown operation') +} diff --git a/test/browser/test/mocked.test.ts b/test/browser/test/mocked.test.ts new file mode 100644 index 000000000000..8a68916ff6d5 --- /dev/null +++ b/test/browser/test/mocked.test.ts @@ -0,0 +1,12 @@ +import { expect, test, vi } from 'vitest' +import * as actions from '../src/actions' +import { calculator } from '../src/calculator' + +test.skip('spyOn works on ESM', () => { + vi.spyOn(actions, 'plus').mockReturnValue(30) + expect(calculator('plus', 1, 2)).toBe(30) + expect(actions.plus).toHaveBeenCalledTimes(1) + vi.mocked(actions.plus).mockRestore() + expect(calculator('plus', 1, 2)).toBe(3) + expect(actions.plus).toHaveBeenCalledTimes(2) +}) From 50acc800e748fb849ee18de772eae6eed011c383 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 25 Apr 2023 20:26:18 +0200 Subject: [PATCH 05/22] chore: cleanup --- packages/vitest/src/node/mock.ts | 8 ++++---- test/browser/specs/runner.test.mjs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/vitest/src/node/mock.ts b/packages/vitest/src/node/mock.ts index b0c6ce7379e7..b4940a1ea0f7 100644 --- a/packages/vitest/src/node/mock.ts +++ b/packages/vitest/src/node/mock.ts @@ -34,7 +34,7 @@ To fix this issue you can either: const API_NOT_FOUND_CHECK = 'if (typeof globalThis.vi === "undefined" && typeof globalThis.vitest === "undefined") ' + `{ throw new Error(${JSON.stringify(API_NOT_FOUND_ERROR)}) }\n` -const regexpHoistable = /^[ \t]*\b(vi|vitest)\s*\.\s*(mock|unmock|hoist)\(/m +const regexpHoistable = /^[ \t]*\b(vi|vitest)\s*\.\s*(mock|unmock|hoisted)\(/m const hashbangRE = /^#!.*\n/ function isIdentifier(node: any): node is Positioned { @@ -181,12 +181,12 @@ export function transformMockableFile(project: WorkspaceProject | Vitest, id: st && isIdentifier(init.callee.property) && init.callee.property.name === 'hoisted' ) { - // hoist const variable = vi.hoisted(() => {}) + // hoist "const variable = vi.hoisted(() => {})" hoistedCalls += `${source.slice(declarationNode.start, declarationNode.end)}\n` magicString.remove(declarationNode.start, declarationNode.end) } else { - // hoist vi.hoisted(() => {}) + // hoist "vi.hoisted(() => {})" hoistedCalls += `${source.slice(node.start, node.end)}\n` magicString.remove(node.start, node.end) } @@ -206,7 +206,7 @@ export function transformMockableFile(project: WorkspaceProject | Vitest, id: st ) const code = magicString.toString() - const map = needMap ? magicString.generateMap({ hires: true }) : null + const map = needMap ? magicString.generateMap({ hires: true, source: id }) : null return { code, diff --git a/test/browser/specs/runner.test.mjs b/test/browser/specs/runner.test.mjs index b6a0d460a02d..976cf0293f00 100644 --- a/test/browser/specs/runner.test.mjs +++ b/test/browser/specs/runner.test.mjs @@ -17,7 +17,7 @@ await test('tests are actually running', async () => { const browserResult = await readFile('./browser.json', 'utf-8') const browserResultJson = JSON.parse(browserResult) - assert.ok(browserResultJson.testResults.length === 6, 'Not all the tests have been run') + assert.ok(browserResultJson.testResults.length === 7, 'Not all the tests have been run') for (const result of browserResultJson.testResults) assert.ok(result.status === 'passed', `${result.name} has failed`) From a34b216f790545bf460d01680110595a328084dd Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 25 Apr 2023 21:07:25 +0200 Subject: [PATCH 06/22] test: add test for hoisted --- examples/mocks/test/hoisted.test.ts | 26 ++++++++++++++++++++++++++ examples/mocks/tsconfig.json | 3 +++ packages/vitest/src/node/mock.ts | 22 ++++++++++++++-------- packages/vitest/src/node/pool.ts | 2 +- 4 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 examples/mocks/test/hoisted.test.ts diff --git a/examples/mocks/test/hoisted.test.ts b/examples/mocks/test/hoisted.test.ts new file mode 100644 index 000000000000..1866ea4a9c5e --- /dev/null +++ b/examples/mocks/test/hoisted.test.ts @@ -0,0 +1,26 @@ +import { expect, test, vi } from 'vitest' +import { asyncSquare as importedAsyncSquare, square as importedSquare } from '../src/example' + +const mocks = vi.hoisted(() => { + return { + square: vi.fn(), + } +}) + +const { asyncSquare } = await vi.hoisted(async () => { + return { + asyncSquare: vi.fn(), + } +}) + +vi.mock('../src/example.ts', () => { + return { + square: mocks.square, + asyncSquare, + } +}) + +test('hoisted works', () => { + expect(importedSquare).toBe(mocks.square) + expect(importedAsyncSquare).toBe(asyncSquare) +}) diff --git a/examples/mocks/tsconfig.json b/examples/mocks/tsconfig.json index aa0a8c03107c..784f2ef7fbfc 100644 --- a/examples/mocks/tsconfig.json +++ b/examples/mocks/tsconfig.json @@ -1,5 +1,8 @@ { "compilerOptions": { + "module": "esnext", + "target": "esnext", + "moduleResolution": "nodenext", "types": ["vitest/globals"] } } diff --git a/packages/vitest/src/node/mock.ts b/packages/vitest/src/node/mock.ts index b4940a1ea0f7..f47c4a695c2e 100644 --- a/packages/vitest/src/node/mock.ts +++ b/packages/vitest/src/node/mock.ts @@ -172,15 +172,21 @@ export function transformMockableFile(project: WorkspaceProject | Vitest, id: st if (methodName === 'hoisted') { const declarationNode = findNodeAround(ast, node.start, 'VariableDeclaration')?.node as Positioned | undefined const init = declarationNode?.declarations[0]?.init - if ( - init + const isViHoisted = (node: CallExpression) => { + return node.callee.type === 'MemberExpression' + && isIdentifier(node.callee.object) + && (node.callee.object.name === 'vi' || node.callee.object.name === 'vitest') + && isIdentifier(node.callee.property) + && node.callee.property.name === 'hoisted' + } + const canMoveDeclaration = (init && init.type === 'CallExpression' - && init.callee.type === 'MemberExpression' - && isIdentifier(init.callee.object) - && (node.callee.object.name === 'vi' || node.callee.object.name === 'vitest') - && isIdentifier(init.callee.property) - && init.callee.property.name === 'hoisted' - ) { + && isViHoisted(init)) + || (init + && init.type === 'AwaitExpression' + && init.argument.type === 'CallExpression' + && isViHoisted(init.argument)) + if (canMoveDeclaration) { // hoist "const variable = vi.hoisted(() => {})" hoistedCalls += `${source.slice(declarationNode.start, declarationNode.end)}\n` magicString.remove(declarationNode.start, declarationNode.end) diff --git a/packages/vitest/src/node/pool.ts b/packages/vitest/src/node/pool.ts index e4912c552f57..dcfa13a14bd6 100644 --- a/packages/vitest/src/node/pool.ts +++ b/packages/vitest/src/node/pool.ts @@ -43,7 +43,7 @@ export function createPool(ctx: Vitest): ProcessPool { function getPoolName([project, file]: WorkspaceSpec) { for (const [glob, pool] of project.config.poolMatchGlobs || []) { if (pool === 'browser') - throw new Error('Since Vitest 0.31.0 "browser" pool is not supported in "poolMatchGlobs". You can create a workspace to run some of your tests in browser in paralle. Read more: https://vitest.dev/guide/workspace') + throw new Error('Since Vitest 0.31.0 "browser" pool is not supported in "poolMatchGlobs". You can create a workspace to run some of your tests in browser in parallel. Read more: https://vitest.dev/guide/workspace') if (mm.isMatch(file, glob, { cwd: project.config.root })) return pool as VitestPool } From 97d450ed91c230769017a632266293f2568bf3f8 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 25 Apr 2023 21:24:30 +0200 Subject: [PATCH 07/22] docs: vi.hoisted --- docs/api/expect.md | 14 ++++----- docs/api/vi.md | 71 ++++++++++++++++++++++++++++++++++++++++++-- docs/config/index.md | 2 +- 3 files changed, 77 insertions(+), 10 deletions(-) diff --git a/docs/api/expect.md b/docs/api/expect.md index a5068955ac61..68527034bed0 100644 --- a/docs/api/expect.md +++ b/docs/api/expect.md @@ -138,7 +138,7 @@ type Awaitable = T | PromiseLike ```ts import { Stocks } from './stocks.js' - + const stocks = new Stocks() stocks.sync('Bill') if (stocks.getInfo('Bill')) @@ -150,7 +150,7 @@ type Awaitable = T | PromiseLike ```ts import { expect, test } from 'vitest' import { Stocks } from './stocks.js' - + const stocks = new Stocks() test('if we know Bill stock, sell apples to him', () => { @@ -171,7 +171,7 @@ type Awaitable = T | PromiseLike ```ts import { Stocks } from './stocks.js' - + const stocks = new Stocks() stocks.sync('Bill') if (!stocks.stockFailed('Bill')) @@ -183,7 +183,7 @@ type Awaitable = T | PromiseLike ```ts import { expect, test } from 'vitest' import { Stocks } from './stocks.js' - + const stocks = new Stocks() test('if Bill stock hasn\'t failed, sell apples to him', () => { @@ -242,7 +242,7 @@ type Awaitable = T | PromiseLike ```ts import { expect, test } from 'vitest' - + const actual = 'stock' test('stock is type of string', () => { @@ -259,7 +259,7 @@ type Awaitable = T | PromiseLike ```ts import { expect, test } from 'vitest' import { Stocks } from './stocks.js' - + const stocks = new Stocks() test('stocks are instance of Stocks', () => { @@ -695,7 +695,7 @@ If the value in the error message is too truncated, you can increase [chaiConfig ## toMatchFileSnapshot - **Type:** `(filepath: string, message?: string) => Promise` -- **Version:** Vitest 0.30.0 +- **Version:** Since Vitest 0.30.0 Compare or update the snapshot with the content of a file explicitly specified (instead of the `.snap` file). diff --git a/docs/api/vi.md b/docs/api/vi.md index afa7976291a2..856439928716 100644 --- a/docs/api/vi.md +++ b/docs/api/vi.md @@ -114,11 +114,55 @@ import { vi } from 'vitest' When using `vi.useFakeTimers`, `Date.now` calls are mocked. If you need to get real time in milliseconds, you can call this function. +## vi.hoisted + +- **Type**: `(factory: () => T) => T` +- **Version**: Since Vitest 0.31.0 + + All static `import` statements in ES modules are hoisted to top of the file, so any code that is define before the imports will actually be executed after imports are evaluated. + + Hovewer it can be useful to invoke some side effect like mocking dates before importing a module. + + To bypass this limitation, you can rewrite static imports into dynamic ones like this: + + ```diff + callFunctionWithSideEffect() + - import { value } from './some/module.ts' + + const { value } = await import('./some/module.ts') + ``` + + When running `vitest`, you can do this automatically by using `vi.hoisted` method. + + ```diff + - callFunctionWithSideEffect() + import { value } from './some/module.ts' + + vi.hoisted(() => callFunctionWithSideEffect()) + ``` + + This method returns the value that was returned from the factory. You can use that value in your `vi.mock` factories if you need an easy access to locally defined variables: + + ```ts + import { expect, vi } from 'vitest' + import { originalMethod } from './path/to/module.js' + + const { mockedMethod } = vi.hoisted(() => { + return { mockedMethod: vi.fn() } + }) + + vi.mocked('./path/to/module.js', () => { + return { originalMethod: mockedMethod } + }) + + mockedMethod.mockReturnValue(100) + expect(originalMethod()).toBe(100) + ``` + + ## vi.mock - **Type**: `(path: string, factory?: () => unknown) => void` - Substitutes all imported modules from provided `path` with another module. You can use configured Vite aliases inside a path. The call to `vi.mock` is hoisted, so it doesn't matter where you call it. It will always be executed before all imports. + Substitutes all imported modules from provided `path` with another module. You can use configured Vite aliases inside a path. The call to `vi.mock` is hoisted, so it doesn't matter where you call it. It will always be executed before all imports. If you need to reference some variables outside of its scope, you can defined them inside [`vi.hoisted`](/vi/#vi-hoisted) and reference inside `vi.mock`. ::: warning `vi.mock` works only for modules that were imported with the `import` keyword. It doesn't work with `require`. @@ -151,6 +195,29 @@ import { vi } from 'vitest' This also means that you cannot use any variables inside the factory that are defined outside the factory. If you need to use variables inside the factory, try [`vi.doMock`](#vi-domock). It works the same way but isn't hoisted. Beware that it only mocks subsequent imports. + + You can also reference variables defined by `vi.hoisted` method if it was declared before `vi.mock`: + + ```ts + import { namedExport } from './path/to/module.js' + + const mocks = vi.hoisted(() => { + return { + namedExport: vi.fn(), + } + }) + + vi.mock('./path/to/module.js', () => { + return { + namedExport: mocks.namedExport, + } + }) + + vi.mocked(namedExport).mockReturnValue(100) + + expect(namedExport()).toBe(100) + expect(namedExport).toBe(mocks.namedExport) + ``` ::: ::: warning @@ -199,7 +266,7 @@ import { vi } from 'vitest' ``` ::: warning - Beware that if you don't call `vi.mock`, modules **are not** mocked automatically. + Beware that if you don't call `vi.mock`, modules **are not** mocked automatically. To replicate Jest's automocking behaviour, you can call `vi.mock` for each required module inside [`setupFiles`](/config/#setupFiles). ::: If there is no `__mocks__` folder or a factory provided, Vitest will import the original module and auto-mock all its exports. For the rules applied, see [algorithm](/guide/mocking#automocking-algorithm). diff --git a/docs/config/index.md b/docs/config/index.md index 27454edbfa01..0ffdcd3707ed 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -1349,7 +1349,7 @@ The number of milliseconds after which a test is considered slow and reported as - **Type:** `{ includeStack?, showDiff?, truncateThreshold? }` - **Default:** `{ includeStack: false, showDiff: true, truncateThreshold: 40 }` -- **Version:** Vitest 0.30.0 +- **Version:** Since Vitest 0.30.0 Equivalent to [Chai config](https://github.com/chaijs/chai/blob/4.x.x/lib/chai/config.js). From 69895fb9293af200c4d2010d579bd3713c51df31 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 25 Apr 2023 21:32:13 +0200 Subject: [PATCH 08/22] chore: fix docs --- docs/api/vi.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api/vi.md b/docs/api/vi.md index 856439928716..29ba59e3f578 100644 --- a/docs/api/vi.md +++ b/docs/api/vi.md @@ -162,7 +162,7 @@ import { vi } from 'vitest' - **Type**: `(path: string, factory?: () => unknown) => void` - Substitutes all imported modules from provided `path` with another module. You can use configured Vite aliases inside a path. The call to `vi.mock` is hoisted, so it doesn't matter where you call it. It will always be executed before all imports. If you need to reference some variables outside of its scope, you can defined them inside [`vi.hoisted`](/vi/#vi-hoisted) and reference inside `vi.mock`. + Substitutes all imported modules from provided `path` with another module. You can use configured Vite aliases inside a path. The call to `vi.mock` is hoisted, so it doesn't matter where you call it. It will always be executed before all imports. If you need to reference some variables outside of its scope, you can defined them inside [`vi.hoisted`](/api/vi#vi-hoisted) and reference inside `vi.mock`. ::: warning `vi.mock` works only for modules that were imported with the `import` keyword. It doesn't work with `require`. @@ -266,7 +266,7 @@ import { vi } from 'vitest' ``` ::: warning - Beware that if you don't call `vi.mock`, modules **are not** mocked automatically. To replicate Jest's automocking behaviour, you can call `vi.mock` for each required module inside [`setupFiles`](/config/#setupFiles). + Beware that if you don't call `vi.mock`, modules **are not** mocked automatically. To replicate Jest's automocking behaviour, you can call `vi.mock` for each required module inside [`setupFiles`](/config/#setupfiles). ::: If there is no `__mocks__` folder or a factory provided, Vitest will import the original module and auto-mock all its exports. For the rules applied, see [algorithm](/guide/mocking#automocking-algorithm). From 9edd01b268334618f2cf93df096f70ca43de275e Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 26 Apr 2023 13:22:24 +0200 Subject: [PATCH 09/22] refactor: rewrite every import like a psychopath --- packages/browser/src/client/index.html | 47 +- packages/browser/src/client/main.ts | 2 +- packages/browser/src/client/utils.ts | 3 +- packages/vitest/package.json | 2 + .../vitest/src/integrations/browser/server.ts | 4 +- packages/vitest/src/node/esmInjector.ts | 404 ++++++++++++++++++ packages/vitest/src/node/esmWalker.ts | 304 +++++++++++++ packages/vitest/src/node/mock.ts | 221 ---------- .../{esm-mocker.ts => esmTransform.ts} | 8 +- packages/vitest/src/node/plugins/index.ts | 4 +- packages/vitest/src/node/plugins/workspace.ts | 4 +- pnpm-lock.yaml | 164 +++---- test/browser/test/mocked.test.ts | 4 +- 13 files changed, 852 insertions(+), 319 deletions(-) create mode 100644 packages/vitest/src/node/esmInjector.ts create mode 100644 packages/vitest/src/node/esmWalker.ts delete mode 100644 packages/vitest/src/node/mock.ts rename packages/vitest/src/node/plugins/{esm-mocker.ts => esmTransform.ts} (50%) diff --git a/packages/browser/src/client/index.html b/packages/browser/src/client/index.html index 0cdeb4cea5cb..8d319d9444e8 100644 --- a/packages/browser/src/client/index.html +++ b/packages/browser/src/client/index.html @@ -25,28 +25,53 @@ diff --git a/packages/browser/src/client/main.ts b/packages/browser/src/client/main.ts index f1cee975db8c..1a0f434ab187 100644 --- a/packages/browser/src/client/main.ts +++ b/packages/browser/src/client/main.ts @@ -74,7 +74,7 @@ ws.addEventListener('open', async () => { config, browserHashMap, // @ts-expect-error untyped global for internal use - moduleCache: globalThis.__vitest_module_cache__, + moduleCache: globalThis.__vi_module_cache__, rpc: client.rpc, safeRpc, durations: { diff --git a/packages/browser/src/client/utils.ts b/packages/browser/src/client/utils.ts index 91dba6260a96..9c751c2be0ea 100644 --- a/packages/browser/src/client/utils.ts +++ b/packages/browser/src/client/utils.ts @@ -1,4 +1,5 @@ export function importId(id: string) { const name = `/@id/${id}` - return import(name) + // @ts-expect-error mocking vitest apis + return __vi_wrap_module__(import(name)) } diff --git a/packages/vitest/package.json b/packages/vitest/package.json index 3ba792543692..0eafc11462b2 100644 --- a/packages/vitest/package.json +++ b/packages/vitest/package.json @@ -148,6 +148,7 @@ "chai": "^4.3.7", "concordance": "^5.0.4", "debug": "^4.3.4", + "estree-walker": "^3.0.3", "local-pkg": "^0.4.3", "magic-string": "^0.30.0", "pathe": "^1.1.0", @@ -190,6 +191,7 @@ "micromatch": "^4.0.5", "mlly": "^1.2.0", "p-limit": "^4.0.0", + "periscopic": "^3.1.0", "pkg-types": "^1.0.2", "playwright": "^1.32.2", "pretty-format": "^27.5.1", diff --git a/packages/vitest/src/integrations/browser/server.ts b/packages/vitest/src/integrations/browser/server.ts index 31fd84b86c2d..0950f062a0bf 100644 --- a/packages/vitest/src/integrations/browser/server.ts +++ b/packages/vitest/src/integrations/browser/server.ts @@ -7,7 +7,7 @@ import { ensurePackageInstalled } from '../../node/pkg' import { resolveApiServerConfig } from '../../node/config' import { CoverageTransform } from '../../node/plugins/coverageTransform' import type { WorkspaceProject } from '../../node/workspace' -import { ESMMockerPlugin } from '../../node/plugins/esm-mocker' +import { ESMTransformPlugin } from '../../node/plugins/esmTransform' export async function createBrowserServer(project: WorkspaceProject, options: UserConfig) { const root = project.config.root @@ -34,7 +34,7 @@ export async function createBrowserServer(project: WorkspaceProject, options: Us plugins: [ (await import('@vitest/browser')).default('/'), CoverageTransform(project.ctx), - ESMMockerPlugin(project), + ESMTransformPlugin(project), { enforce: 'post', name: 'vitest:browser:config', diff --git a/packages/vitest/src/node/esmInjector.ts b/packages/vitest/src/node/esmInjector.ts new file mode 100644 index 000000000000..46f0a2a39024 --- /dev/null +++ b/packages/vitest/src/node/esmInjector.ts @@ -0,0 +1,404 @@ +import { Parser } from 'acorn' +import MagicString from 'magic-string' +import { extract_names as extractNames } from 'periscopic' +import type { CallExpression, Expression, Identifier, ImportDeclaration, VariableDeclaration } from 'estree' +import { findNodeAround } from 'acorn-walk' +import type { ViteDevServer } from 'vite' +import { toArray } from '../utils' +import type { Node, Positioned } from './esmWalker' +import { esmWalker, isInDestructuringAssignment, isNodeInPattern, isStaticProperty } from './esmWalker' +import type { WorkspaceProject } from './workspace' +import type { Vitest } from './core' + +const parsers = new WeakMap() + +function getAcornParser(server: ViteDevServer) { + let parser = parsers.get(server)! + if (!parser) { + const acornPlugins = server.pluginContainer.options.acornInjectPlugins || [] + parser = Parser.extend(...toArray(acornPlugins) as any) + parsers.set(server, parser) + } + return parser +} + +function isIdentifier(node: any): node is Positioned { + return node.type === 'Identifier' +} + +function transformImportSpecifiers(node: ImportDeclaration, mode: 'object' | 'named' = 'object') { + const specifiers = node.specifiers + + if (specifiers.length === 1 && specifiers[0].type === 'ImportNamespaceSpecifier') + return specifiers[0].local.name + + const dynamicImports = node.specifiers.map((specifier) => { + if (specifier.type === 'ImportDefaultSpecifier') + return `default ${mode === 'object' ? ':' : 'as'} ${specifier.local.name}` + + if (specifier.type === 'ImportSpecifier') { + const local = specifier.local.name + const imported = specifier.imported.name + if (local === imported) + return local + return `${imported} ${mode === 'object' ? ':' : 'as'} ${local}` + } + + return null + }).filter(Boolean).join(', ') + + if (!dynamicImports.length) + return '' + + return `{ ${dynamicImports} }` +} + +const viInjectedKey = '__vi_inject__' +// const viImportMetaKey = '__vi_import_meta__' // to allow overwrite +const viExportAllHelper = '__vi_export_all__' + +const regexpHoistable = /^[ \t]*\b(vi|vitest)\s*\.\s*(mock|unmock|hoisted)\(/m + +const skipHijack = [ + '/@vite/client', + '/@vite/env', + /vite\/dist\/client/, +] + +// this is basically copypaste from Vite SSR +export function injectVitestModule(project: WorkspaceProject | Vitest, code: string, id: string) { + if (skipHijack.some(skip => id.match(skip))) + return + + const hasMocks = regexpHoistable.test(code) + const hijackEsm = project.config.slowHijackESM ?? false + + if (!hasMocks && !hijackEsm) + return + + const s = new MagicString(code) + const parser = getAcornParser(project.server) + + let ast: any + try { + ast = parser.parse(code, { + sourceType: 'module', + ecmaVersion: 'latest', + locations: true, + ranges: true, + }) + } + catch (err) { + console.error(err) + return + } + + let uid = 0 + const idToImportMap = new Map() + const declaredConst = new Set() + + const hoistIndex = 0 + + let hasInjected = false + let hoistedCode = '' + let hoistedVitestImports = '' + + // this will tranfrom import statements into dynamic ones, if there are imports + // it will keep the import as is, if we don't need to mock anything + // in browser environment it will wrap the module value with "vitest_wrap_module" function + // that returns a proxy to the module so that named exports can be mocked + const transformImportDeclaration = (node: ImportDeclaration) => { + // if we don't hijack ESM and process this file, then we definetly have mocks, + // so we need to transform imports into dynamic ones, so "vi.mock" can be executed before + if (!hijackEsm) { + const specifiers = transformImportSpecifiers(node) + const code = specifiers + ? `const ${specifiers} = await import('${node.source.value}')\n` + : `await import('${node.source.value}')\n` + return { code } + } + + const importId = `__vi_esm_${uid++}__` + const hasSpecifiers = node.specifiers.length > 0 + if (hasMocks) { + const code = hasSpecifiers + ? `const { ${viInjectedKey}: ${importId} } = await import('${node.source.value}')\n` + : `await import('${node.source.value}')\n` + return { + code, + id: importId, + } + } + const code = hasSpecifiers + ? `import { ${viInjectedKey} as ${importId} } from '${node.source.value}'\n` + : `import '${node.source.value}'\n` + return { + code, + id: importId, + } + } + + function defineImport(node: ImportDeclaration) { + if (node.source.value === 'vitest') { + const importId = `__vi_esm_${uid++}__` + const code = hijackEsm + ? `import { ${viInjectedKey} as ${importId} } from 'vitest'\nconst ${transformImportSpecifiers(node)} = ${importId};\n` + : `import ${transformImportSpecifiers(node, 'named')} from 'vitest'\n` + hoistedVitestImports += code + return + } + const { code, id } = transformImportDeclaration(node) + s.appendLeft(hoistIndex, code) + return id + } + + function defineImportAll(source: string) { + const importId = `__vi_esm_${uid++}__` + s.appendLeft(hoistIndex, `const { ${viInjectedKey}: ${importId} } = await import(${JSON.stringify(source)});\n`) + return importId + } + + function defineExport(position: number, name: string, local = name) { + hasInjected = true + s.appendLeft( + position, + `\nObject.defineProperty(${viInjectedKey}, "${name}", ` + + `{ enumerable: true, configurable: true, get(){ return ${local} }});`, + ) + } + + // 1. check all import statements and record id -> importName map + for (const node of ast.body as Node[]) { + // import foo from 'foo' --> foo -> __import_foo__.default + // import { baz } from 'foo' --> baz -> __import_foo__.baz + // import * as ok from 'foo' --> ok -> __import_foo__ + if (node.type === 'ImportDeclaration') { + const importId = defineImport(node) + s.remove(node.start, node.end) + if (!hijackEsm || !importId) + continue + for (const spec of node.specifiers) { + if (spec.type === 'ImportSpecifier') { + idToImportMap.set( + spec.local.name, + `${importId}.${spec.imported.name}`, + ) + } + else if (spec.type === 'ImportDefaultSpecifier') { + idToImportMap.set(spec.local.name, `${importId}.default`) + } + else { + // namespace specifier + idToImportMap.set(spec.local.name, importId) + } + } + } + } + + // 2. check all export statements and define exports + for (const node of ast.body as Node[]) { + if (!hijackEsm) + break + + // named exports + if (node.type === 'ExportNamedDeclaration') { + if (node.declaration) { + if ( + node.declaration.type === 'FunctionDeclaration' + || node.declaration.type === 'ClassDeclaration' + ) { + // export function foo() {} + defineExport(node.end, node.declaration.id!.name) + } + else { + // export const foo = 1, bar = 2 + for (const declaration of node.declaration.declarations) { + const names = extractNames(declaration.id as any) + for (const name of names) + defineExport(node.end, name) + } + } + s.remove(node.start, (node.declaration as Node).start) + } + else { + s.remove(node.start, node.end) + if (node.source) { + // export { foo, bar } from './foo' + const importId = defineImportAll(node.source.value as string) + // hoist re-exports near the defined import so they are immediately exported + for (const spec of node.specifiers) { + defineExport( + hoistIndex, + spec.exported.name, + `${importId}.${spec.local.name}`, + ) + } + } + else { + // export { foo, bar } + for (const spec of node.specifiers) { + const local = spec.local.name + const binding = idToImportMap.get(local) + defineExport(node.end, spec.exported.name, binding || local) + } + } + } + } + + // default export + if (node.type === 'ExportDefaultDeclaration') { + const expressionTypes = ['FunctionExpression', 'ClassExpression'] + if ( + 'id' in node.declaration + && node.declaration.id + && !expressionTypes.includes(node.declaration.type) + ) { + // named hoistable/class exports + // export default function foo() {} + // export default class A {} + hasInjected = true + const { name } = node.declaration.id + s.remove(node.start, node.start + 15 /* 'export default '.length */) + s.append( + `\nObject.defineProperty(${viInjectedKey}, "default", ` + + `{ enumerable: true, configurable: true, value: ${name} });`, + ) + } + else { + // anonymous default exports + hasInjected = true + s.update( + node.start, + node.start + 14 /* 'export default'.length */, + `${viInjectedKey}.default =`, + ) + if (id.startsWith(project.server.config.cacheDir)) { + // keep export default for optimized dependencies + s.append(`\nexport default { ${viInjectedKey}: ${viInjectedKey}.default };\n`) + } + } + } + + // export * from './foo' + if (node.type === 'ExportAllDeclaration') { + s.remove(node.start, node.end) + const importId = defineImportAll(node.source.value as string) + // hoist re-exports near the defined import so they are immediately exported + if (node.exported) { + defineExport(hoistIndex, node.exported.name, `${importId}`) + } + else { + hasInjected = true + s.appendLeft(hoistIndex, `${viExportAllHelper}(${viInjectedKey}, ${importId});\n`) + } + } + } + + if (hijackEsm) { + // 3. convert references to import bindings & import.meta references + esmWalker(ast, { + onCallExpression(node) { + if ( + node.callee.type === 'MemberExpression' + && isIdentifier(node.callee.object) + && (node.callee.object.name === 'vi' || node.callee.object.name === 'vitest') + && isIdentifier(node.callee.property) + ) { + const methodName = node.callee.property.name + if (methodName === 'mock' || methodName === 'unmock') { + hoistedCode += `${code.slice(node.start, node.end)}\n` + s.remove(node.start, node.end) + } + if (methodName === 'hoisted') { + const declarationNode = findNodeAround(ast, node.start, 'VariableDeclaration')?.node as Positioned | undefined + const init = declarationNode?.declarations[0]?.init + const isViHoisted = (node: CallExpression) => { + return node.callee.type === 'MemberExpression' + && isIdentifier(node.callee.object) + && (node.callee.object.name === 'vi' || node.callee.object.name === 'vitest') + && isIdentifier(node.callee.property) + && node.callee.property.name === 'hoisted' + } + const canMoveDeclaration = (init + && init.type === 'CallExpression' + && isViHoisted(init)) + || (init + && init.type === 'AwaitExpression' + && init.argument.type === 'CallExpression' + && isViHoisted(init.argument)) + if (canMoveDeclaration) { + // hoist "const variable = vi.hoisted(() => {})" + hoistedCode += `${code.slice(declarationNode.start, declarationNode.end)}\n` + s.remove(declarationNode.start, declarationNode.end) + } + else { + // hoist "vi.hoisted(() => {})" + hoistedCode += `${code.slice(node.start, node.end)}\n` + s.remove(node.start, node.end) + } + } + } + }, + onIdentifier(id, parent, parentStack) { + const grandparent = parentStack[1] + const binding = idToImportMap.get(id.name) + if (!binding) + return + + if (isStaticProperty(parent) && parent.shorthand) { + // let binding used in a property shorthand + // { foo } -> { foo: __import_x__.foo } + // skip for destructuring patterns + if ( + !isNodeInPattern(parent) + || isInDestructuringAssignment(parent, parentStack) + ) + s.appendLeft(id.end, `: ${binding}`) + } + else if ( + (parent.type === 'PropertyDefinition' + && grandparent?.type === 'ClassBody') + || (parent.type === 'ClassDeclaration' && id === parent.superClass) + ) { + if (!declaredConst.has(id.name)) { + declaredConst.add(id.name) + // locate the top-most node containing the class declaration + const topNode = parentStack[parentStack.length - 2] + s.prependRight(topNode.start, `const ${id.name} = ${binding};\n`) + } + } + else { + s.update(id.start, id.end, binding) + } + }, + // TODO: make env updatable + onImportMeta() { + // s.update(node.start, node.end, viImportMetaKey) + }, + onDynamicImport(node) { + const replace = '__vi_wrap_module__(import(' + s.overwrite(node.start, (node.source as Positioned).start, replace) + s.overwrite(node.end - 1, node.end, '))') + }, + }) + } + + if (hoistedCode || hoistedVitestImports) { + s.appendLeft( + 0, + hoistedVitestImports + + hoistedCode, + ) + } + + if (hasInjected) { + s.prepend(`const ${viInjectedKey} = { [Symbol.toStringTag]: "Module" };\n`) + s.append(`\nexport { ${viInjectedKey} }`) + } + + return { + ast, + code: s.toString(), + map: s.generateMap({ hires: true, source: id }), + } +} diff --git a/packages/vitest/src/node/esmWalker.ts b/packages/vitest/src/node/esmWalker.ts new file mode 100644 index 000000000000..6fc890359503 --- /dev/null +++ b/packages/vitest/src/node/esmWalker.ts @@ -0,0 +1,304 @@ +import type { + CallExpression, + Function as FunctionNode, + Identifier, + ImportExpression, + Pattern, + Property, + VariableDeclaration, + Node as _Node, +} from 'estree' +import { walk as eswalk } from 'estree-walker' + +export type Positioned = T & { + start: number + end: number +} + +export type Node = Positioned<_Node> + +interface Visitors { + onIdentifier: ( + node: Positioned, + parent: Node, + parentStack: Node[], + ) => void + onCallExpression: (node: Positioned) => void + onImportMeta: (node: Node) => void + onDynamicImport: (node: Positioned) => void +} + +const isNodeInPatternWeakSet = new WeakSet<_Node>() +export function setIsNodeInPattern(node: Property) { + return isNodeInPatternWeakSet.add(node) +} +export function isNodeInPattern(node: _Node): node is Property { + return isNodeInPatternWeakSet.has(node) +} + +/** + * Same logic from \@vue/compiler-core & \@vue/compiler-sfc + * Except this is using acorn AST + */ +export function esmWalker( + root: Node, + { onIdentifier, onImportMeta, onDynamicImport, onCallExpression }: Visitors, +) { + const parentStack: Node[] = [] + const varKindStack: VariableDeclaration['kind'][] = [] + const scopeMap = new WeakMap<_Node, Set>() + const identifiers: [id: any, stack: Node[]][] = [] + + const setScope = (node: _Node, name: string) => { + let scopeIds = scopeMap.get(node) + if (scopeIds && scopeIds.has(name)) + return + + if (!scopeIds) { + scopeIds = new Set() + scopeMap.set(node, scopeIds) + } + scopeIds.add(name) + } + + function isInScope(name: string, parents: Node[]) { + return parents.some(node => node && scopeMap.get(node)?.has(name)) + } + function handlePattern(p: Pattern, parentScope: _Node) { + if (p.type === 'Identifier') { + setScope(parentScope, p.name) + } + else if (p.type === 'RestElement') { + handlePattern(p.argument, parentScope) + } + else if (p.type === 'ObjectPattern') { + p.properties.forEach((property) => { + if (property.type === 'RestElement') + setScope(parentScope, (property.argument as Identifier).name) + + else + handlePattern(property.value, parentScope) + }) + } + else if (p.type === 'ArrayPattern') { + p.elements.forEach((element) => { + if (element) + handlePattern(element, parentScope) + }) + } + else if (p.type === 'AssignmentPattern') { + handlePattern(p.left, parentScope) + } + else { + setScope(parentScope, (p as any).name) + } + } + + (eswalk as any)(root, { + enter(node: Node, parent: Node | null) { + if (node.type === 'ImportDeclaration') + return this.skip() + + // track parent stack, skip for "else-if"/"else" branches as acorn nests + // the ast within "if" nodes instead of flattening them + if ( + parent + && !(parent.type === 'IfStatement' && node === parent.alternate) + ) + parentStack.unshift(parent) + + // track variable declaration kind stack used by VariableDeclarator + if (node.type === 'VariableDeclaration') + varKindStack.unshift(node.kind) + + if (node.type === 'MetaProperty' && node.meta.name === 'import') + onImportMeta(node) + + else if (node.type === 'ImportExpression') + onDynamicImport(node) + + else if (node.type === 'CallExpression') + onCallExpression(node) + + if (node.type === 'Identifier') { + if ( + !isInScope(node.name, parentStack) + && isRefIdentifier(node, parent!, parentStack) + ) { + // record the identifier, for DFS -> BFS + identifiers.push([node, parentStack.slice(0)]) + } + } + else if (isFunctionNode(node)) { + // If it is a function declaration, it could be shadowing an import + // Add its name to the scope so it won't get replaced + if (node.type === 'FunctionDeclaration') { + const parentScope = findParentScope(parentStack) + if (parentScope) + setScope(parentScope, node.id!.name) + } + // walk function expressions and add its arguments to known identifiers + // so that we don't prefix them + node.params.forEach((p) => { + if (p.type === 'ObjectPattern' || p.type === 'ArrayPattern') { + handlePattern(p, node) + return + } + (eswalk as any)(p.type === 'AssignmentPattern' ? p.left : p, { + enter(child: Node, parent: Node) { + // skip params default value of destructure + if ( + parent?.type === 'AssignmentPattern' + && parent?.right === child + ) + return this.skip() + + if (child.type !== 'Identifier') + return + // do not record as scope variable if is a destructuring keyword + if (isStaticPropertyKey(child, parent)) + return + // do not record if this is a default value + // assignment of a destructuring variable + if ( + (parent?.type === 'TemplateLiteral' + && parent?.expressions.includes(child)) + || (parent?.type === 'CallExpression' && parent?.callee === child) + ) + return + + setScope(node, child.name) + }, + }) + }) + } + else if (node.type === 'Property' && parent!.type === 'ObjectPattern') { + // mark property in destructuring pattern + setIsNodeInPattern(node) + } + else if (node.type === 'VariableDeclarator') { + const parentFunction = findParentScope( + parentStack, + varKindStack[0] === 'var', + ) + if (parentFunction) + handlePattern(node.id, parentFunction) + } + else if (node.type === 'CatchClause' && node.param) { + handlePattern(node.param, node) + } + }, + + leave(node: Node, parent: Node | null) { + // untrack parent stack from above + if ( + parent + && !(parent.type === 'IfStatement' && node === parent.alternate) + ) + parentStack.shift() + + if (node.type === 'VariableDeclaration') + varKindStack.shift() + }, + }) + + // emit the identifier events in BFS so the hoisted declarations + // can be captured correctly + identifiers.forEach(([node, stack]) => { + if (!isInScope(node.name, stack)) + onIdentifier(node, stack[0], stack) + }) +} + +function isRefIdentifier(id: Identifier, parent: _Node, parentStack: _Node[]) { + // declaration id + if ( + parent.type === 'CatchClause' + || ((parent.type === 'VariableDeclarator' + || parent.type === 'ClassDeclaration') + && parent.id === id) + ) + return false + + if (isFunctionNode(parent)) { + // function declaration/expression id + if ((parent as any).id === id) + return false + + // params list + if (parent.params.includes(id)) + return false + } + + // class method name + if (parent.type === 'MethodDefinition' && !parent.computed) + return false + + // property key + if (isStaticPropertyKey(id, parent)) + return false + + // object destructuring pattern + if (isNodeInPattern(parent) && parent.value === id) + return false + + // non-assignment array destructuring pattern + if ( + parent.type === 'ArrayPattern' + && !isInDestructuringAssignment(parent, parentStack) + ) + return false + + // member expression property + if ( + parent.type === 'MemberExpression' + && parent.property === id + && !parent.computed + ) + return false + + if (parent.type === 'ExportSpecifier') + return false + + // is a special keyword but parsed as identifier + if (id.name === 'arguments') + return false + + return true +} + +export function isStaticProperty(node: _Node): node is Property { + return node && node.type === 'Property' && !node.computed +} + +export function isStaticPropertyKey(node: _Node, parent: _Node) { + return isStaticProperty(parent) && parent.key === node +} + +const functionNodeTypeRE = /Function(?:Expression|Declaration)$|Method$/ +export function isFunctionNode(node: _Node): node is FunctionNode { + return functionNodeTypeRE.test(node.type) +} + +function findParentScope( + parentStack: _Node[], + isVar = false, +): _Node | undefined { + const predicate = isVar + ? isFunctionNode + : (node: _Node) => node.type === 'BlockStatement' + return parentStack.find(predicate) +} + +export function isInDestructuringAssignment( + parent: _Node, + parentStack: _Node[], +): boolean { + if ( + parent + && (parent.type === 'Property' || parent.type === 'ArrayPattern') + ) + return parentStack.some(i => i.type === 'AssignmentExpression') + + return false +} diff --git a/packages/vitest/src/node/mock.ts b/packages/vitest/src/node/mock.ts deleted file mode 100644 index f47c4a695c2e..000000000000 --- a/packages/vitest/src/node/mock.ts +++ /dev/null @@ -1,221 +0,0 @@ -import MagicString from 'magic-string' -import { Parser } from 'acorn' -import { findNodeAround, simple as walk } from 'acorn-walk' -import type { CallExpression, Expression, Identifier, ImportDeclaration, ImportExpression, VariableDeclaration } from 'estree' -import type { ViteDevServer } from 'vite' -import { toArray } from '../utils' -import type { WorkspaceProject } from './workspace' -import type { Vitest } from './core' - -type Positioned = T & { - start: number - end: number -} - -const parsers = new WeakMap() - -function getAcornParser(server: ViteDevServer) { - const acornPlugins = server.pluginContainer.options.acornInjectPlugins || [] - let parser = parsers.get(server)! - if (!parser) { - parser = Parser.extend(...toArray(acornPlugins) as any) - parsers.set(server, parser) - } - return parser -} - -const API_NOT_FOUND_ERROR = `There are some problems in resolving the mocks API. -You may encounter this issue when importing the mocks API from another module other than 'vitest'. - -To fix this issue you can either: -- import the mocks API directly from 'vitest' -- enable the 'globals' options` - -const API_NOT_FOUND_CHECK = 'if (typeof globalThis.vi === "undefined" && typeof globalThis.vitest === "undefined") ' -+ `{ throw new Error(${JSON.stringify(API_NOT_FOUND_ERROR)}) }\n` - -const regexpHoistable = /^[ \t]*\b(vi|vitest)\s*\.\s*(mock|unmock|hoisted)\(/m -const hashbangRE = /^#!.*\n/ - -function isIdentifier(node: any): node is Positioned { - return node.type === 'Identifier' -} - -function transformImportSpecifiers(node: ImportDeclaration) { - const specifiers = node.specifiers - - if (specifiers.length === 1 && specifiers[0].type === 'ImportNamespaceSpecifier') - return specifiers[0].local.name - - const dynamicImports = node.specifiers.map((specifier) => { - if (specifier.type === 'ImportDefaultSpecifier') - return `default: ${specifier.local.name}` - - if (specifier.type === 'ImportSpecifier') { - const local = specifier.local.name - const imported = specifier.imported.name - if (local === imported) - return local - return `${imported}: ${local}` - } - - return null - }).filter(Boolean).join(', ') - - if (!dynamicImports.length) - return '' - - return `{ ${dynamicImports} }` -} - -export function transformMockableFile(project: WorkspaceProject | Vitest, id: string, source: string, needMap = false) { - const hasMocks = regexpHoistable.test(source) - const hijackEsm = project.config.slowHijackESM ?? false - - // we don't need to control __vitest_module__ in Node.js, - // because we control the module resolution directly, - // but we stil need to hoist mocks everywhere - if (!hijackEsm && !hasMocks) - return - - const parser = getAcornParser(project.server) - const hoistIndex = source.match(hashbangRE)?.[0].length ?? 0 - let ast: any - try { - ast = parser.parse(source, { - sourceType: 'module', - ecmaVersion: 'latest', - locations: true, - allowHashBang: true, - }) - } - catch (err) { - console.error(`[vitest] Not able to parse source code of ${id}.`) - console.error(err) - return - } - - const magicString = new MagicString(source) - - let hoistedCalls = '' - // hoist Vitest imports at the very top of the file - let hoistedVitestImports = '' - let idx = 0 - - // this will tranfrom import statements into dynamic ones, if there are imports - // it will keep the import as is, if we don't need to mock anything - // in browser environment it will wrap the module value with "vitest_wrap_module" function - // that returns a proxy to the module so that named exports can be mocked - const transformImportDeclaration = (node: ImportDeclaration) => { - const specifiers = transformImportSpecifiers(node) - // if we don't hijack ESM and process this file, then we definetly have mocks, - // so we need to transform imports into dynamic ones, so "vi.mock" can be executed before - if (!hijackEsm) { - return specifiers - ? `const ${specifiers} = await import('${node.source.value}')\n` - : `await import('${node.source.value}')\n` - } - - const moduleName = `__vitest_module_${idx++}__` - const destructured = `const ${specifiers} = __vitest_wrap_module__(${moduleName})\n` - if (hasMocks) { - return specifiers - ? `const ${moduleName} = await import('${node.source.value}')\n${destructured}` - : `await __vitest_wrap_module__(import('${node.source.value}'))\n` - } - return specifiers - ? `import * as ${moduleName} from '${node.source.value}'\n${destructured}` - : `import '${node.source.value}'\n` - } - - walk(ast, { - ImportExpression(_node) { - if (!hijackEsm) - return - const node = _node as any as Positioned - const replace = '__vitest_wrap_module__(import(' - magicString.overwrite(node.start, (node.source as Positioned).start, replace) - magicString.overwrite(node.end - 1, node.end, '))') - }, - - ImportDeclaration(_node) { - const node = _node as any as Positioned - - const start = node.start - const end = node.end - - if (node.source.value === 'vitest') { - hoistedVitestImports += transformImportDeclaration(node) - magicString.remove(start, end) - return - } - - const dynamicImport = transformImportDeclaration(node) - - magicString.overwrite(start, end, dynamicImport) - }, - - CallExpression(_node) { - const node = _node as any as Positioned - - if ( - node.callee.type === 'MemberExpression' - && isIdentifier(node.callee.object) - && (node.callee.object.name === 'vi' || node.callee.object.name === 'vitest') - && isIdentifier(node.callee.property) - ) { - const methodName = node.callee.property.name - if (methodName === 'mock' || methodName === 'unmock') { - hoistedCalls += `${source.slice(node.start, node.end)}\n` - magicString.remove(node.start, node.end) - } - if (methodName === 'hoisted') { - const declarationNode = findNodeAround(ast, node.start, 'VariableDeclaration')?.node as Positioned | undefined - const init = declarationNode?.declarations[0]?.init - const isViHoisted = (node: CallExpression) => { - return node.callee.type === 'MemberExpression' - && isIdentifier(node.callee.object) - && (node.callee.object.name === 'vi' || node.callee.object.name === 'vitest') - && isIdentifier(node.callee.property) - && node.callee.property.name === 'hoisted' - } - const canMoveDeclaration = (init - && init.type === 'CallExpression' - && isViHoisted(init)) - || (init - && init.type === 'AwaitExpression' - && init.argument.type === 'CallExpression' - && isViHoisted(init.argument)) - if (canMoveDeclaration) { - // hoist "const variable = vi.hoisted(() => {})" - hoistedCalls += `${source.slice(declarationNode.start, declarationNode.end)}\n` - magicString.remove(declarationNode.start, declarationNode.end) - } - else { - // hoist "vi.hoisted(() => {})" - hoistedCalls += `${source.slice(node.start, node.end)}\n` - magicString.remove(node.start, node.end) - } - } - } - }, - }) - - if (hasMocks) - hoistedCalls += '\nawait __vitest_mocker__.prepare()\n' - - magicString.appendLeft( - hoistIndex, - hoistedVitestImports - + ((!hoistedVitestImports && hoistedCalls) ? API_NOT_FOUND_CHECK : '') - + hoistedCalls, - ) - - const code = magicString.toString() - const map = needMap ? magicString.generateMap({ hires: true, source: id }) : null - - return { - code, - map, - } -} diff --git a/packages/vitest/src/node/plugins/esm-mocker.ts b/packages/vitest/src/node/plugins/esmTransform.ts similarity index 50% rename from packages/vitest/src/node/plugins/esm-mocker.ts rename to packages/vitest/src/node/plugins/esmTransform.ts index 651142c781f3..50418d310811 100644 --- a/packages/vitest/src/node/plugins/esm-mocker.ts +++ b/packages/vitest/src/node/plugins/esmTransform.ts @@ -1,14 +1,14 @@ import type { Plugin } from 'vite' -import { transformMockableFile } from '../mock' +import { injectVitestModule } from '../esmInjector' import type { Vitest } from '../core' import type { WorkspaceProject } from '../workspace' -export function ESMMockerPlugin(ctx: WorkspaceProject | Vitest): Plugin { +export function ESMTransformPlugin(ctx: WorkspaceProject | Vitest): Plugin { return { name: 'vitest:mocker-plugin', enforce: 'post', - transform(code, id) { - return transformMockableFile(ctx, id, code, true) + transform(source, id) { + return injectVitestModule(ctx, source, id) }, } } diff --git a/packages/vitest/src/node/plugins/index.ts b/packages/vitest/src/node/plugins/index.ts index 64a5f6c444f9..2d8b41ce3244 100644 --- a/packages/vitest/src/node/plugins/index.ts +++ b/packages/vitest/src/node/plugins/index.ts @@ -11,7 +11,7 @@ import { EnvReplacerPlugin } from './envReplacer' import { GlobalSetupPlugin } from './globalSetup' import { CSSEnablerPlugin } from './cssEnabler' import { CoverageTransform } from './coverageTransform' -import { ESMMockerPlugin } from './esm-mocker' +import { ESMTransformPlugin } from './esmTransform' export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest('test')): Promise { const userConfig = deepMerge({}, options) as UserConfig @@ -243,7 +243,7 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest('t options.ui ? await UIPlugin() : null, - ESMMockerPlugin(ctx), + ESMTransformPlugin(ctx), ] .filter(notNullish) } diff --git a/packages/vitest/src/node/plugins/workspace.ts b/packages/vitest/src/node/plugins/workspace.ts index bdf7f7daf6f3..29f5b1b32afd 100644 --- a/packages/vitest/src/node/plugins/workspace.ts +++ b/packages/vitest/src/node/plugins/workspace.ts @@ -9,7 +9,7 @@ import { CoverageTransform } from './coverageTransform' import { CSSEnablerPlugin } from './cssEnabler' import { EnvReplacerPlugin } from './envReplacer' import { GlobalSetupPlugin } from './globalSetup' -import { ESMMockerPlugin } from './esm-mocker' +import { ESMTransformPlugin } from './esmTransform' interface WorkspaceOptions extends UserWorkspaceConfig { root?: string @@ -139,6 +139,6 @@ export function WorkspaceVitestPlugin(project: WorkspaceProject, options: Worksp ...CSSEnablerPlugin(project), CoverageTransform(project.ctx), GlobalSetupPlugin(project, project.ctx.logger), - ESMMockerPlugin(project), + ESMTransformPlugin(project), ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ffdd5e2495f4..f63c2e8e7e86 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -146,7 +146,7 @@ importers: version: 0.0.5(vite-plugin-pwa@0.14.7) '@vitejs/plugin-vue': specifier: latest - version: 4.2.0(vite@4.2.1)(vue@3.2.47) + version: 4.2.1(vite@4.2.1)(vue@3.2.47) esno: specifier: ^0.16.3 version: 0.16.3 @@ -318,7 +318,7 @@ importers: version: 13.3.0(react-dom@18.0.0)(react@18.0.0) '@types/node': specifier: latest - version: 18.16.0 + version: 18.16.1 '@types/react': specifier: latest version: 18.2.0 @@ -678,7 +678,7 @@ importers: devDependencies: '@vitejs/plugin-vue': specifier: latest - version: 4.2.0(vite@4.2.1)(vue@3.2.47) + version: 4.2.1(vite@4.2.1)(vue@3.2.47) '@vue/test-utils': specifier: latest version: 2.3.2(vue@3.2.47) @@ -755,7 +755,7 @@ importers: devDependencies: '@vitejs/plugin-vue': specifier: latest - version: 4.2.0(vite@4.2.1)(vue@3.2.47) + version: 4.2.1(vite@4.2.1)(vue@3.2.47) '@vue/test-utils': specifier: ^2.0.2 version: 2.0.2(vue@3.2.47) @@ -783,7 +783,7 @@ importers: devDependencies: '@vitejs/plugin-vue': specifier: latest - version: 4.2.0(vite@4.2.1)(vue@3.2.47) + version: 4.2.1(vite@4.2.1)(vue@3.2.47) '@vue/test-utils': specifier: ^2.0.0 version: 2.0.0(vue@3.2.47) @@ -801,7 +801,7 @@ importers: devDependencies: '@vitejs/plugin-vue': specifier: latest - version: 4.2.0(vite@4.2.1)(vue@3.2.47) + version: 4.2.1(vite@4.2.1)(vue@3.2.47) '@vitejs/plugin-vue-jsx': specifier: latest version: 3.0.1(vite@4.2.1)(vue@3.2.47) @@ -1203,6 +1203,9 @@ importers: debug: specifier: ^4.3.4 version: 4.3.4(supports-color@8.1.1) + estree-walker: + specifier: ^3.0.3 + version: 3.0.3 local-pkg: specifier: ^0.4.3 version: 0.4.3 @@ -1324,6 +1327,9 @@ importers: p-limit: specifier: ^4.0.0 version: 4.0.0 + periscopic: + specifier: ^3.1.0 + version: 3.1.0 pkg-types: specifier: ^1.0.2 version: 1.0.2 @@ -1511,7 +1517,7 @@ importers: version: 2.0.4 '@vitejs/plugin-vue': specifier: latest - version: 4.2.0(vite@4.2.1)(vue@3.2.47) + version: 4.2.1(vite@4.2.1)(vue@3.2.47) '@vitest/browser': specifier: workspace:* version: link:../../packages/browser @@ -2112,10 +2118,6 @@ packages: dependencies: '@babel/highlight': 7.18.6 - /@babel/compat-data@7.20.10: - resolution: {integrity: sha512-sEnuDPpOJR/fcafHMjpcpGN5M2jbUGUHwmuWKM/YdPzeEDJg8bgmbcWQFUfE32MQjti1koACvoPVsDe8Uq+idg==} - engines: {node: '>=6.9.0'} - /@babel/compat-data@7.21.4: resolution: {integrity: sha512-/DYyDpeCfaVinT40FPGdkkb+lYSKvsVuMjDAG7jPOWWiM1ibOaB9CXJAlc4d1QpP/U2q2P9jbrSlClKSErd55g==} engines: {node: '>=6.9.0'} @@ -2249,9 +2251,9 @@ packages: peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/compat-data': 7.20.10 + '@babel/compat-data': 7.21.4 '@babel/core': 7.18.13 - '@babel/helper-validator-option': 7.18.6 + '@babel/helper-validator-option': 7.21.0 browserslist: 4.21.3 semver: 6.3.0 @@ -2585,10 +2587,6 @@ packages: resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==} engines: {node: '>=6.9.0'} - /@babel/helper-validator-option@7.18.6: - resolution: {integrity: sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==} - engines: {node: '>=6.9.0'} - /@babel/helper-validator-option@7.21.0: resolution: {integrity: sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==} engines: {node: '>=6.9.0'} @@ -4657,7 +4655,7 @@ packages: dependencies: '@babel/core': 7.20.5 '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-validator-option': 7.18.6 + '@babel/helper-validator-option': 7.21.0 '@babel/plugin-transform-typescript': 7.21.3(@babel/core@7.20.5) transitivePeerDependencies: - supports-color @@ -4671,7 +4669,7 @@ packages: dependencies: '@babel/core': 7.21.4 '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-validator-option': 7.18.6 + '@babel/helper-validator-option': 7.21.0 '@babel/plugin-transform-typescript': 7.21.3(@babel/core@7.21.4) transitivePeerDependencies: - supports-color @@ -5488,7 +5486,7 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: '@jest/types': 27.5.1 - '@types/node': 18.16.0 + '@types/node': 18.16.1 chalk: 4.1.2 jest-message-util: 27.5.1 jest-util: 27.5.1 @@ -5509,7 +5507,7 @@ packages: '@jest/test-result': 27.5.1 '@jest/transform': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 18.16.0 + '@types/node': 18.16.1 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.8.1 @@ -5546,7 +5544,7 @@ packages: dependencies: '@jest/fake-timers': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 18.16.0 + '@types/node': 18.16.1 jest-mock: 27.5.1 dev: true @@ -5563,7 +5561,7 @@ packages: dependencies: '@jest/types': 27.5.1 '@sinonjs/fake-timers': 8.1.0 - '@types/node': 18.16.0 + '@types/node': 18.16.1 jest-message-util: 27.5.1 jest-mock: 27.5.1 jest-util: 27.5.1 @@ -5592,7 +5590,7 @@ packages: '@jest/test-result': 27.5.1 '@jest/transform': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 18.16.0 + '@types/node': 18.16.1 chalk: 4.1.2 collect-v8-coverage: 1.0.1 exit: 0.1.2 @@ -5706,7 +5704,7 @@ packages: dependencies: '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 18.16.0 + '@types/node': 18.16.1 '@types/yargs': 15.0.14 chalk: 4.1.2 dev: true @@ -5717,7 +5715,7 @@ packages: dependencies: '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 18.16.0 + '@types/node': 18.16.1 '@types/yargs': 16.0.5 chalk: 4.1.2 dev: true @@ -5729,7 +5727,7 @@ packages: '@jest/schemas': 29.0.0 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 18.16.0 + '@types/node': 18.16.1 '@types/yargs': 17.0.12 chalk: 4.1.2 dev: true @@ -6242,7 +6240,7 @@ packages: engines: {node: '>=14'} hasBin: true dependencies: - '@types/node': 18.16.0 + '@types/node': 18.16.1 playwright-core: 1.28.0 dev: true @@ -8151,7 +8149,7 @@ packages: /@types/cheerio@0.22.31: resolution: {integrity: sha512-Kt7Cdjjdi2XWSfrZ53v4Of0wG3ZcmaegFXjMmz9tfNrZSkzzo36G0AL1YqSdcIA78Etjt6E609pt5h1xnQkPUw==} dependencies: - '@types/node': 18.16.0 + '@types/node': 18.16.1 dev: true /@types/codemirror@5.60.7: @@ -8213,7 +8211,6 @@ packages: /@types/estree@1.0.1: resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} - dev: true /@types/fs-extra@11.0.1: resolution: {integrity: sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA==} @@ -8225,27 +8222,27 @@ packages: /@types/fs-extra@9.0.13: resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} dependencies: - '@types/node': 18.16.0 + '@types/node': 18.16.1 dev: true /@types/glob@7.2.0: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} dependencies: '@types/minimatch': 5.1.1 - '@types/node': 18.16.0 + '@types/node': 18.16.1 dev: true /@types/glob@8.0.0: resolution: {integrity: sha512-l6NQsDDyQUVeoTynNpC9uRvCUint/gSUXQA2euwmTuWGvPY5LSDUu6tkCtJB2SvGQlJQzLaKqcGZP4//7EDveA==} dependencies: '@types/minimatch': 5.1.1 - '@types/node': 18.16.0 + '@types/node': 18.16.1 dev: true /@types/graceful-fs@4.1.5: resolution: {integrity: sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==} dependencies: - '@types/node': 18.16.0 + '@types/node': 18.16.1 dev: true /@types/hast@2.3.4: @@ -8326,7 +8323,7 @@ packages: /@types/jsonfile@6.1.1: resolution: {integrity: sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png==} dependencies: - '@types/node': 18.16.0 + '@types/node': 18.16.1 dev: true /@types/lodash@4.14.192: @@ -8360,7 +8357,7 @@ packages: /@types/node-fetch@2.6.2: resolution: {integrity: sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==} dependencies: - '@types/node': 18.16.0 + '@types/node': 18.16.1 form-data: 3.0.1 dev: true @@ -8379,6 +8376,10 @@ packages: resolution: {integrity: sha512-BsAaKhB+7X+H4GnSjGhJG9Qi8Tw+inU9nJDwmD5CgOmBLEI6ArdhikpLX7DjbjDRDTbqZzU2LSQNZg8WGPiSZQ==} dev: true + /@types/node@18.16.1: + resolution: {integrity: sha512-DZxSZWXxFfOlx7k7Rv4LAyiMroaxa3Ly/7OOzZO8cBNho0YzAi4qlbrx8W27JGqG57IgR/6J7r+nOJWw6kcvZA==} + dev: true + /@types/node@18.7.13: resolution: {integrity: sha512-46yIhxSe5xEaJZXWdIBP7GU4HDTG8/eo0qd9atdiL+lFpA03y8KS+lkTN834TWJj5767GbWv4n/P6efyTFt1Dw==} dev: false @@ -8482,7 +8483,7 @@ packages: /@types/resolve@1.17.1: resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} dependencies: - '@types/node': 18.16.0 + '@types/node': 18.16.1 dev: true /@types/resolve@1.20.2: @@ -8499,7 +8500,7 @@ packages: /@types/set-cookie-parser@2.4.2: resolution: {integrity: sha512-fBZgytwhYAUkj/jC/FAV4RQ5EerRup1YQsXQCh8rZfiHkc4UahC192oH0smGwsXol3cL3A5oETuAHeQHmhXM4w==} dependencies: - '@types/node': 18.16.0 + '@types/node': 18.16.1 dev: true /@types/sinonjs__fake-timers@8.1.1: @@ -8573,7 +8574,7 @@ packages: /@types/webpack-sources@3.2.0: resolution: {integrity: sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==} dependencies: - '@types/node': 18.16.0 + '@types/node': 18.16.1 '@types/source-list-map': 0.1.2 source-map: 0.7.4 dev: true @@ -8581,7 +8582,7 @@ packages: /@types/webpack@4.41.32: resolution: {integrity: sha512-cb+0ioil/7oz5//7tZUSwbrSAN/NWHrQylz5cW8G0dWTcF/g+/dSdMlKVZspBYuMAN1+WnwHrkxiRrLcwd0Heg==} dependencies: - '@types/node': 18.16.0 + '@types/node': 18.16.1 '@types/tapable': 1.0.8 '@types/uglify-js': 3.17.0 '@types/webpack-sources': 3.2.0 @@ -8625,7 +8626,7 @@ packages: resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} requiresBuild: true dependencies: - '@types/node': 18.16.0 + '@types/node': 18.16.1 dev: true optional: true @@ -9026,7 +9027,7 @@ packages: '@babel/plugin-transform-react-jsx-self': 7.21.0(@babel/core@7.21.4) '@babel/plugin-transform-react-jsx-source': 7.19.6(@babel/core@7.21.4) react-refresh: 0.14.0 - vite: 4.2.1(@types/node@18.16.0) + vite: 4.2.1(@types/node@18.16.1) transitivePeerDependencies: - supports-color dev: true @@ -9069,8 +9070,8 @@ packages: vue: 3.2.47 dev: true - /@vitejs/plugin-vue@4.2.0(vite@4.2.1)(vue@3.2.47): - resolution: {integrity: sha512-hYaXFvEKEwyTmwHq2ft7GGeLBvyYLwTM3E5R1jpvzxg9gO4m5PQcTVvj1wEPKoPL8PAt+KAlxo3gyJWnmwzaWQ==} + /@vitejs/plugin-vue@4.2.1(vite@4.2.1)(vue@3.2.47): + resolution: {integrity: sha512-ZTZjzo7bmxTRTkb8GSTwkPOYDIP7pwuyV+RV53c9PYUouwcbkIZIvWvNWlX2b1dYZqtOv7D6iUAnJLVNGcLrSw==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: vite: ^4.0.0 @@ -9507,21 +9508,21 @@ packages: resolution: {integrity: sha512-vyJzqHJ5yOmfVyk5WWo6pRsJ2xhgWl3DVIBdDNR0wKrtFcm/g1jnB+pNf6Eb7NhCDh3oGul25bmhAwWDoxcFYA==} engines: {node: ^16.13 || >=18} dependencies: - '@types/node': 18.16.0 + '@types/node': 18.16.1 dev: true /@wdio/types@8.6.8: resolution: {integrity: sha512-hwlkQ6E8DNIFL/l8vHve3Zpl1t6Hqle7vtatEkAlrmbnExc7qI6Yw6SI5T/KiBSAi0ez1OypbGhdrbXFfywxng==} engines: {node: ^16.13 || >=18} dependencies: - '@types/node': 18.16.0 + '@types/node': 18.16.1 dev: true /@wdio/types@8.8.0: resolution: {integrity: sha512-Ai6yIlwWB32FUfvQKCqSa6nSyHIhSF5BOU9OfE7I2XYkLAJTxu8B6NORHQ+rgoppHSWc4D2V9r21y3etF8AGnQ==} engines: {node: ^16.13 || >=18} dependencies: - '@types/node': 18.16.0 + '@types/node': 18.16.1 dev: true /@wdio/utils@8.6.8: @@ -11496,7 +11497,7 @@ packages: engines: {node: '>=12.13.0'} hasBin: true dependencies: - '@types/node': 18.16.0 + '@types/node': 18.16.1 escape-string-regexp: 4.0.0 is-wsl: 2.2.0 lighthouse-logger: 1.3.0 @@ -12736,7 +12737,7 @@ packages: resolution: {integrity: sha512-Xv7NA5nUPU2ma/VMcAYRIMLX4+YrsEOXMG6ZGPVuU5tC9zylb5L7fkBCqjqYJ/kkVWibbIn3l63hMpnZ63AS5w==} engines: {node: ^16.13 || >=18} dependencies: - '@types/node': 18.16.0 + '@types/node': 18.16.1 '@wdio/config': 8.6.8 '@wdio/logger': 8.6.6 '@wdio/protocols': 8.6.6 @@ -12762,7 +12763,7 @@ packages: resolution: {integrity: sha512-FfvMEald7LtXIA12oo6wStlxSlAFy3NMAkVAHmu23g8jYhuhl2ASQQzVUFlBHKhVqLvbwSF0VuPZzaPRoz3uDQ==} engines: {node: ^16.13 || >=18} dependencies: - '@types/node': 18.16.0 + '@types/node': 18.16.1 '@wdio/config': 8.8.0 '@wdio/logger': 8.6.6 '@wdio/protocols': 8.6.6 @@ -14236,6 +14237,11 @@ packages: /estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + /estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + dependencies: + '@types/estree': 1.0.1 + /esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -16391,6 +16397,12 @@ packages: '@types/estree': 1.0.1 dev: true + /is-reference@3.0.1: + resolution: {integrity: sha512-baJJdQLiYaJdvFbJqXrcGv3WU3QCzBlUcI5QhbesIm6/xPsvmO+2CDoi/GMOFBQEQm+PXkwOPrp9KK5ozZsp2w==} + dependencies: + '@types/estree': 1.0.1 + dev: true + /is-regex@1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} @@ -16648,7 +16660,7 @@ packages: '@jest/environment': 27.5.1 '@jest/test-result': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 18.16.0 + '@types/node': 18.16.1 chalk: 4.1.2 co: 4.6.0 dedent: 0.7.0 @@ -16784,7 +16796,7 @@ packages: '@jest/environment': 27.5.1 '@jest/fake-timers': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 18.16.0 + '@types/node': 18.16.1 jest-mock: 27.5.1 jest-util: 27.5.1 jsdom: 16.7.0 @@ -16802,7 +16814,7 @@ packages: '@jest/environment': 27.5.1 '@jest/fake-timers': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 18.16.0 + '@types/node': 18.16.1 jest-mock: 27.5.1 jest-util: 27.5.1 dev: true @@ -16823,7 +16835,7 @@ packages: dependencies: '@jest/types': 26.6.2 '@types/graceful-fs': 4.1.5 - '@types/node': 18.16.0 + '@types/node': 18.16.1 anymatch: 3.1.2 fb-watchman: 2.0.1 graceful-fs: 4.2.10 @@ -16846,7 +16858,7 @@ packages: dependencies: '@jest/types': 27.5.1 '@types/graceful-fs': 4.1.5 - '@types/node': 18.16.0 + '@types/node': 18.16.1 anymatch: 3.1.2 fb-watchman: 2.0.1 graceful-fs: 4.2.10 @@ -16886,7 +16898,7 @@ packages: '@jest/source-map': 27.5.1 '@jest/test-result': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 18.16.0 + '@types/node': 18.16.1 chalk: 4.1.2 co: 4.6.0 expect: 27.5.1 @@ -16966,7 +16978,7 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: '@jest/types': 27.5.1 - '@types/node': 18.16.0 + '@types/node': 18.16.1 dev: true /jest-pnp-resolver@1.2.3(jest-resolve@27.5.1): @@ -17027,7 +17039,7 @@ packages: '@jest/test-result': 27.5.1 '@jest/transform': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 18.16.0 + '@types/node': 18.16.1 chalk: 4.1.2 emittery: 0.8.1 graceful-fs: 4.2.10 @@ -17084,7 +17096,7 @@ packages: resolution: {integrity: sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==} engines: {node: '>= 10.14.2'} dependencies: - '@types/node': 18.16.0 + '@types/node': 18.16.1 graceful-fs: 4.2.10 dev: true @@ -17092,7 +17104,7 @@ packages: resolution: {integrity: sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: - '@types/node': 18.16.0 + '@types/node': 18.16.1 graceful-fs: 4.2.10 dev: true @@ -17131,7 +17143,7 @@ packages: engines: {node: '>= 10.14.2'} dependencies: '@jest/types': 26.6.2 - '@types/node': 18.16.0 + '@types/node': 18.16.1 chalk: 4.1.2 graceful-fs: 4.2.10 is-ci: 2.0.0 @@ -17143,7 +17155,7 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: '@jest/types': 27.5.1 - '@types/node': 18.16.0 + '@types/node': 18.16.1 chalk: 4.1.2 ci-info: 3.7.0 graceful-fs: 4.2.10 @@ -17155,7 +17167,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.0.1 - '@types/node': 18.16.0 + '@types/node': 18.16.1 chalk: 4.1.2 ci-info: 3.7.0 graceful-fs: 4.2.10 @@ -17180,7 +17192,7 @@ packages: dependencies: '@jest/test-result': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 18.16.0 + '@types/node': 18.16.1 ansi-escapes: 4.3.2 chalk: 4.1.2 jest-util: 27.5.1 @@ -17191,7 +17203,7 @@ packages: resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 18.16.0 + '@types/node': 18.16.1 merge-stream: 2.0.0 supports-color: 7.2.0 dev: true @@ -17200,7 +17212,7 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 18.16.0 + '@types/node': 18.16.1 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true @@ -19629,6 +19641,14 @@ packages: resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} dev: true + /periscopic@3.1.0: + resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} + dependencies: + '@types/estree': 1.0.1 + estree-walker: 3.0.3 + is-reference: 3.0.1 + dev: true + /picocolors@0.2.1: resolution: {integrity: sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==} dev: true @@ -23768,7 +23788,7 @@ packages: optionalDependencies: fsevents: 2.3.2 - /vite@4.2.1(@types/node@18.16.0): + /vite@4.2.1(@types/node@18.16.1): resolution: {integrity: sha512-7MKhqdy0ISo4wnvwtqZkjke6XN4taqQ2TBaTccLIpOKv7Vp2h4Y+NpmWCnGDeSvvn45KxvWgGyb0MkHvY1vgbg==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true @@ -23793,7 +23813,7 @@ packages: terser: optional: true dependencies: - '@types/node': 18.16.0 + '@types/node': 18.16.1 esbuild: 0.17.15 postcss: 8.4.21 resolve: 1.22.1 @@ -23853,7 +23873,7 @@ packages: dependencies: '@docsearch/css': 3.3.3 '@docsearch/js': 3.3.3(@algolia/client-search@4.14.2) - '@vitejs/plugin-vue': 4.2.0(vite@4.2.1)(vue@3.2.47) + '@vitejs/plugin-vue': 4.2.1(vite@4.2.1)(vue@3.2.47) '@vue/devtools-api': 6.5.0 '@vueuse/core': 9.13.0(vue@3.2.47) body-scroll-lock: 4.0.0-beta.0 @@ -24094,7 +24114,7 @@ packages: resolution: {integrity: sha512-v+43Z4miGKa1JaFAIxgK5AedBgHV/7MI+jd3fJao24R/KWYNgC9GD4BUD6LxC7AChuyRdA1/cN3NjwrPTg0ujg==} engines: {node: ^16.13 || >=18} dependencies: - '@types/node': 18.16.0 + '@types/node': 18.16.1 '@types/ws': 8.5.4 '@wdio/config': 8.6.8 '@wdio/logger': 8.6.6 @@ -24114,7 +24134,7 @@ packages: resolution: {integrity: sha512-LqO06orjZlODkQm5npEkuXtBEdVc+tKZAzX468Wra71U9naUZN7YrMjboHvbtsUuiRLWt0RzByO5VCWRS0o/Zg==} engines: {node: ^16.13 || >=18} dependencies: - '@types/node': 18.16.0 + '@types/node': 18.16.1 '@types/ws': 8.5.4 '@wdio/config': 8.8.0 '@wdio/logger': 8.6.6 @@ -24171,7 +24191,7 @@ packages: resolution: {integrity: sha512-QMce84O2CX/T3GUowO0/4V16RFE5METrQ3fjeWx0oLq/6rvZJe3X97Tdk5Xnlpcma6Ot+zhIsU8zWsMgi07wCA==} engines: {node: ^16.13 || >=18} dependencies: - '@types/node': 18.16.0 + '@types/node': 18.16.1 '@wdio/config': 8.8.0 '@wdio/logger': 8.6.6 '@wdio/protocols': 8.6.6 diff --git a/test/browser/test/mocked.test.ts b/test/browser/test/mocked.test.ts index 8a68916ff6d5..c85a3694abf8 100644 --- a/test/browser/test/mocked.test.ts +++ b/test/browser/test/mocked.test.ts @@ -2,11 +2,9 @@ import { expect, test, vi } from 'vitest' import * as actions from '../src/actions' import { calculator } from '../src/calculator' -test.skip('spyOn works on ESM', () => { +test('spyOn works on ESM', () => { vi.spyOn(actions, 'plus').mockReturnValue(30) expect(calculator('plus', 1, 2)).toBe(30) - expect(actions.plus).toHaveBeenCalledTimes(1) vi.mocked(actions.plus).mockRestore() expect(calculator('plus', 1, 2)).toBe(3) - expect(actions.plus).toHaveBeenCalledTimes(2) }) From 92c72e7ba92d780884641896685b4907d36a097d Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 26 Apr 2023 14:07:03 +0200 Subject: [PATCH 10/22] fix: actually hoist vi.mock --- packages/vitest/src/node/esmInjector.ts | 94 +++++++++++++------------ 1 file changed, 50 insertions(+), 44 deletions(-) diff --git a/packages/vitest/src/node/esmInjector.ts b/packages/vitest/src/node/esmInjector.ts index 46f0a2a39024..cb457e5893ac 100644 --- a/packages/vitest/src/node/esmInjector.ts +++ b/packages/vitest/src/node/esmInjector.ts @@ -2,7 +2,7 @@ import { Parser } from 'acorn' import MagicString from 'magic-string' import { extract_names as extractNames } from 'periscopic' import type { CallExpression, Expression, Identifier, ImportDeclaration, VariableDeclaration } from 'estree' -import { findNodeAround } from 'acorn-walk' +import { findNodeAround, simple as simpleWalk } from 'acorn-walk' import type { ViteDevServer } from 'vite' import { toArray } from '../utils' import type { Node, Positioned } from './esmWalker' @@ -294,51 +294,53 @@ export function injectVitestModule(project: WorkspaceProject | Vitest, code: str } } - if (hijackEsm) { - // 3. convert references to import bindings & import.meta references - esmWalker(ast, { - onCallExpression(node) { - if ( - node.callee.type === 'MemberExpression' + function CallExpression(node: Positioned) { + if ( + node.callee.type === 'MemberExpression' + && isIdentifier(node.callee.object) + && (node.callee.object.name === 'vi' || node.callee.object.name === 'vitest') + && isIdentifier(node.callee.property) + ) { + const methodName = node.callee.property.name + if (methodName === 'mock' || methodName === 'unmock') { + hoistedCode += `${code.slice(node.start, node.end)}\n` + s.remove(node.start, node.end) + } + if (methodName === 'hoisted') { + const declarationNode = findNodeAround(ast, node.start, 'VariableDeclaration')?.node as Positioned | undefined + const init = declarationNode?.declarations[0]?.init + const isViHoisted = (node: CallExpression) => { + return node.callee.type === 'MemberExpression' && isIdentifier(node.callee.object) && (node.callee.object.name === 'vi' || node.callee.object.name === 'vitest') && isIdentifier(node.callee.property) - ) { - const methodName = node.callee.property.name - if (methodName === 'mock' || methodName === 'unmock') { - hoistedCode += `${code.slice(node.start, node.end)}\n` - s.remove(node.start, node.end) - } - if (methodName === 'hoisted') { - const declarationNode = findNodeAround(ast, node.start, 'VariableDeclaration')?.node as Positioned | undefined - const init = declarationNode?.declarations[0]?.init - const isViHoisted = (node: CallExpression) => { - return node.callee.type === 'MemberExpression' - && isIdentifier(node.callee.object) - && (node.callee.object.name === 'vi' || node.callee.object.name === 'vitest') - && isIdentifier(node.callee.property) - && node.callee.property.name === 'hoisted' - } - const canMoveDeclaration = (init - && init.type === 'CallExpression' - && isViHoisted(init)) - || (init - && init.type === 'AwaitExpression' - && init.argument.type === 'CallExpression' - && isViHoisted(init.argument)) - if (canMoveDeclaration) { - // hoist "const variable = vi.hoisted(() => {})" - hoistedCode += `${code.slice(declarationNode.start, declarationNode.end)}\n` - s.remove(declarationNode.start, declarationNode.end) - } - else { - // hoist "vi.hoisted(() => {})" - hoistedCode += `${code.slice(node.start, node.end)}\n` - s.remove(node.start, node.end) - } - } + && node.callee.property.name === 'hoisted' } - }, + const canMoveDeclaration = (init + && init.type === 'CallExpression' + && isViHoisted(init)) + || (init + && init.type === 'AwaitExpression' + && init.argument.type === 'CallExpression' + && isViHoisted(init.argument)) + if (canMoveDeclaration) { + // hoist "const variable = vi.hoisted(() => {})" + hoistedCode += `${code.slice(declarationNode.start, declarationNode.end)}\n` + s.remove(declarationNode.start, declarationNode.end) + } + else { + // hoist "vi.hoisted(() => {})" + hoistedCode += `${code.slice(node.start, node.end)}\n` + s.remove(node.start, node.end) + } + } + } + } + + if (hijackEsm) { + // 3. convert references to import bindings & import.meta references + esmWalker(ast, { + onCallExpression: CallExpression, onIdentifier(id, parent, parentStack) { const grandparent = parentStack[1] const binding = idToImportMap.get(id.name) @@ -382,10 +384,14 @@ export function injectVitestModule(project: WorkspaceProject | Vitest, code: str }, }) } + else { + simpleWalk(ast, { + CallExpression: CallExpression as any, + }) + } if (hoistedCode || hoistedVitestImports) { - s.appendLeft( - 0, + s.prepend( hoistedVitestImports + hoistedCode, ) From 06f4805d6e660035005a1b3c54c4b2ea41850f9d Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 26 Apr 2023 14:22:33 +0200 Subject: [PATCH 11/22] chore: return "vi" access check --- packages/vitest/src/node/esmInjector.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/vitest/src/node/esmInjector.ts b/packages/vitest/src/node/esmInjector.ts index cb457e5893ac..fbafaaa8fc55 100644 --- a/packages/vitest/src/node/esmInjector.ts +++ b/packages/vitest/src/node/esmInjector.ts @@ -10,6 +10,15 @@ import { esmWalker, isInDestructuringAssignment, isNodeInPattern, isStaticProper import type { WorkspaceProject } from './workspace' import type { Vitest } from './core' +const API_NOT_FOUND_ERROR = `There are some problems in resolving the mocks API. +You may encounter this issue when importing the mocks API from another module other than 'vitest'. +To fix this issue you can either: +- import the mocks API directly from 'vitest' +- enable the 'globals' options` + +const API_NOT_FOUND_CHECK = '\nif (typeof vi === "undefined" && typeof vitest === "undefined") ' ++ `{ throw new Error(${JSON.stringify(API_NOT_FOUND_ERROR)}) }\n` + const parsers = new WeakMap() function getAcornParser(server: ViteDevServer) { @@ -393,6 +402,7 @@ export function injectVitestModule(project: WorkspaceProject | Vitest, code: str if (hoistedCode || hoistedVitestImports) { s.prepend( hoistedVitestImports + + ((!hoistedVitestImports && hoistedCode) ? API_NOT_FOUND_CHECK : '') + hoistedCode, ) } From 8b2d63363352782ec4ecea5a5006cb48cb3f1f4e Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 26 Apr 2023 14:50:09 +0200 Subject: [PATCH 12/22] chore: check on globalThis --- packages/vitest/src/node/esmInjector.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/vitest/src/node/esmInjector.ts b/packages/vitest/src/node/esmInjector.ts index fbafaaa8fc55..6cb16430ed85 100644 --- a/packages/vitest/src/node/esmInjector.ts +++ b/packages/vitest/src/node/esmInjector.ts @@ -16,7 +16,7 @@ To fix this issue you can either: - import the mocks API directly from 'vitest' - enable the 'globals' options` -const API_NOT_FOUND_CHECK = '\nif (typeof vi === "undefined" && typeof vitest === "undefined") ' +const API_NOT_FOUND_CHECK = '\nif (typeof globalThis.vi === "undefined" && typeof globalThis.vitest === "undefined") ' + `{ throw new Error(${JSON.stringify(API_NOT_FOUND_ERROR)}) }\n` const parsers = new WeakMap() @@ -412,6 +412,8 @@ export function injectVitestModule(project: WorkspaceProject | Vitest, code: str s.append(`\nexport { ${viInjectedKey} }`) } + console.error(s.toString()) + return { ast, code: s.toString(), From ab20e9edafcd7f000ae85b50ade1fc33d2a1eca5 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 26 Apr 2023 15:12:22 +0200 Subject: [PATCH 13/22] chore: remove log --- packages/vitest/src/node/esmInjector.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/vitest/src/node/esmInjector.ts b/packages/vitest/src/node/esmInjector.ts index 6cb16430ed85..1ed0493e3bda 100644 --- a/packages/vitest/src/node/esmInjector.ts +++ b/packages/vitest/src/node/esmInjector.ts @@ -98,7 +98,7 @@ export function injectVitestModule(project: WorkspaceProject | Vitest, code: str }) } catch (err) { - console.error(err) + console.error(`Cannot parse ${id}:\n${(err as any).message}`) return } @@ -412,8 +412,6 @@ export function injectVitestModule(project: WorkspaceProject | Vitest, code: str s.append(`\nexport { ${viInjectedKey} }`) } - console.error(s.toString()) - return { ast, code: s.toString(), From 6354e64ab7b4e08ab812aaceca986397c93b1ad5 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 26 Apr 2023 15:33:03 +0200 Subject: [PATCH 14/22] chore: don't rewrite skipped imports --- packages/vitest/src/node/esmInjector.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/vitest/src/node/esmInjector.ts b/packages/vitest/src/node/esmInjector.ts index 1ed0493e3bda..603dbfdfb682 100644 --- a/packages/vitest/src/node/esmInjector.ts +++ b/packages/vitest/src/node/esmInjector.ts @@ -117,13 +117,15 @@ export function injectVitestModule(project: WorkspaceProject | Vitest, code: str // in browser environment it will wrap the module value with "vitest_wrap_module" function // that returns a proxy to the module so that named exports can be mocked const transformImportDeclaration = (node: ImportDeclaration) => { + const source = node.source.value as string + // if we don't hijack ESM and process this file, then we definetly have mocks, // so we need to transform imports into dynamic ones, so "vi.mock" can be executed before - if (!hijackEsm) { + if (!hijackEsm || skipHijack.some(skip => source.match(skip))) { const specifiers = transformImportSpecifiers(node) const code = specifiers - ? `const ${specifiers} = await import('${node.source.value}')\n` - : `await import('${node.source.value}')\n` + ? `const ${specifiers} = await import('${source}')\n` + : `await import('${source}')\n` return { code } } @@ -131,16 +133,16 @@ export function injectVitestModule(project: WorkspaceProject | Vitest, code: str const hasSpecifiers = node.specifiers.length > 0 if (hasMocks) { const code = hasSpecifiers - ? `const { ${viInjectedKey}: ${importId} } = await import('${node.source.value}')\n` - : `await import('${node.source.value}')\n` + ? `const { ${viInjectedKey}: ${importId} } = await import('${source}')\n` + : `await import('${source}')\n` return { code, id: importId, } } const code = hasSpecifiers - ? `import { ${viInjectedKey} as ${importId} } from '${node.source.value}'\n` - : `import '${node.source.value}'\n` + ? `import { ${viInjectedKey} as ${importId} } from '${source}'\n` + : `import '${source}'\n` return { code, id: importId, From b79db39d45ebc18deecd357c7b57e2f75f6c9ad4 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 26 Apr 2023 17:17:13 +0200 Subject: [PATCH 15/22] chore: use rollup parse function to parse files --- packages/vitest/src/node/esmInjector.ts | 61 +++++++++---------- .../vitest/src/node/plugins/esmTransform.ts | 2 +- 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/packages/vitest/src/node/esmInjector.ts b/packages/vitest/src/node/esmInjector.ts index 603dbfdfb682..ab129b2aadb3 100644 --- a/packages/vitest/src/node/esmInjector.ts +++ b/packages/vitest/src/node/esmInjector.ts @@ -1,10 +1,8 @@ -import { Parser } from 'acorn' import MagicString from 'magic-string' import { extract_names as extractNames } from 'periscopic' import type { CallExpression, Expression, Identifier, ImportDeclaration, VariableDeclaration } from 'estree' import { findNodeAround, simple as simpleWalk } from 'acorn-walk' -import type { ViteDevServer } from 'vite' -import { toArray } from '../utils' +import type { AcornNode } from 'rollup' import type { Node, Positioned } from './esmWalker' import { esmWalker, isInDestructuringAssignment, isNodeInPattern, isStaticProperty } from './esmWalker' import type { WorkspaceProject } from './workspace' @@ -19,18 +17,6 @@ To fix this issue you can either: const API_NOT_FOUND_CHECK = '\nif (typeof globalThis.vi === "undefined" && typeof globalThis.vitest === "undefined") ' + `{ throw new Error(${JSON.stringify(API_NOT_FOUND_ERROR)}) }\n` -const parsers = new WeakMap() - -function getAcornParser(server: ViteDevServer) { - let parser = parsers.get(server)! - if (!parser) { - const acornPlugins = server.pluginContainer.options.acornInjectPlugins || [] - parser = Parser.extend(...toArray(acornPlugins) as any) - parsers.set(server, parser) - } - return parser -} - function isIdentifier(node: any): node is Positioned { return node.type === 'Identifier' } @@ -75,7 +61,11 @@ const skipHijack = [ ] // this is basically copypaste from Vite SSR -export function injectVitestModule(project: WorkspaceProject | Vitest, code: string, id: string) { +// this method transforms all import and export statements into `__vi_injected__` variable +// to allow spying on them. this can be disabled by setting `slowHijackESM` to `false` +// to not parse the module twice, we reuse the ast to hoist vi.mock here +// and transform imports into dynamic ones if vi.mock is present +export function injectVitestModule(project: WorkspaceProject | Vitest, code: string, id: string, parse: (code: string, options: any) => AcornNode) { if (skipHijack.some(skip => id.match(skip))) return @@ -86,15 +76,13 @@ export function injectVitestModule(project: WorkspaceProject | Vitest, code: str return const s = new MagicString(code) - const parser = getAcornParser(project.server) let ast: any try { - ast = parser.parse(code, { + ast = parse(code, { sourceType: 'module', ecmaVersion: 'latest', locations: true, - ranges: true, }) } catch (err) { @@ -150,6 +138,8 @@ export function injectVitestModule(project: WorkspaceProject | Vitest, code: str } function defineImport(node: ImportDeclaration) { + // always hoist vitest import to top of the file, so + // "vi" helpers can access it if (node.source.value === 'vitest') { const importId = `__vi_esm_${uid++}__` const code = hijackEsm @@ -308,32 +298,36 @@ export function injectVitestModule(project: WorkspaceProject | Vitest, code: str function CallExpression(node: Positioned) { if ( node.callee.type === 'MemberExpression' - && isIdentifier(node.callee.object) - && (node.callee.object.name === 'vi' || node.callee.object.name === 'vitest') - && isIdentifier(node.callee.property) + && isIdentifier(node.callee.object) + && (node.callee.object.name === 'vi' || node.callee.object.name === 'vitest') + && isIdentifier(node.callee.property) ) { const methodName = node.callee.property.name + if (methodName === 'mock' || methodName === 'unmock') { hoistedCode += `${code.slice(node.start, node.end)}\n` s.remove(node.start, node.end) } + if (methodName === 'hoisted') { const declarationNode = findNodeAround(ast, node.start, 'VariableDeclaration')?.node as Positioned | undefined const init = declarationNode?.declarations[0]?.init const isViHoisted = (node: CallExpression) => { return node.callee.type === 'MemberExpression' - && isIdentifier(node.callee.object) - && (node.callee.object.name === 'vi' || node.callee.object.name === 'vitest') - && isIdentifier(node.callee.property) - && node.callee.property.name === 'hoisted' + && isIdentifier(node.callee.object) + && (node.callee.object.name === 'vi' || node.callee.object.name === 'vitest') + && isIdentifier(node.callee.property) + && node.callee.property.name === 'hoisted' } + const canMoveDeclaration = (init - && init.type === 'CallExpression' - && isViHoisted(init)) - || (init - && init.type === 'AwaitExpression' - && init.argument.type === 'CallExpression' - && isViHoisted(init.argument)) + && init.type === 'CallExpression' + && isViHoisted(init)) /* const v = vi.hoisted() */ + || (init + && init.type === 'AwaitExpression' + && init.argument.type === 'CallExpression' + && isViHoisted(init.argument)) /* const v = await vi.hoisted() */ + if (canMoveDeclaration) { // hoist "const variable = vi.hoisted(() => {})" hoistedCode += `${code.slice(declarationNode.start, declarationNode.end)}\n` @@ -348,6 +342,7 @@ export function injectVitestModule(project: WorkspaceProject | Vitest, code: str } } + // if we don't need to inject anything, skip the walking if (hijackEsm) { // 3. convert references to import bindings & import.meta references esmWalker(ast, { @@ -395,6 +390,7 @@ export function injectVitestModule(project: WorkspaceProject | Vitest, code: str }, }) } + // we still need to hoist "vi" helper else { simpleWalk(ast, { CallExpression: CallExpression as any, @@ -410,6 +406,7 @@ export function injectVitestModule(project: WorkspaceProject | Vitest, code: str } if (hasInjected) { + // make sure "__vi_injected__" is declared as soon as possible s.prepend(`const ${viInjectedKey} = { [Symbol.toStringTag]: "Module" };\n`) s.append(`\nexport { ${viInjectedKey} }`) } diff --git a/packages/vitest/src/node/plugins/esmTransform.ts b/packages/vitest/src/node/plugins/esmTransform.ts index 50418d310811..1f7e0920106f 100644 --- a/packages/vitest/src/node/plugins/esmTransform.ts +++ b/packages/vitest/src/node/plugins/esmTransform.ts @@ -8,7 +8,7 @@ export function ESMTransformPlugin(ctx: WorkspaceProject | Vitest): Plugin { name: 'vitest:mocker-plugin', enforce: 'post', transform(source, id) { - return injectVitestModule(ctx, source, id) + return injectVitestModule(ctx, source, id, (code, options) => this.parse(code, options)) }, } } From f2995b87a6b3121da0ef4d07b918d217de536c92 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 26 Apr 2023 17:32:17 +0200 Subject: [PATCH 16/22] chore: cleanup --- packages/vitest/src/node/esmInjector.ts | 13 ++++++++----- packages/vitest/src/node/plugins/esmTransform.ts | 5 ++++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/vitest/src/node/esmInjector.ts b/packages/vitest/src/node/esmInjector.ts index ab129b2aadb3..b9943f5e6ed2 100644 --- a/packages/vitest/src/node/esmInjector.ts +++ b/packages/vitest/src/node/esmInjector.ts @@ -5,8 +5,6 @@ import { findNodeAround, simple as simpleWalk } from 'acorn-walk' import type { AcornNode } from 'rollup' import type { Node, Positioned } from './esmWalker' import { esmWalker, isInDestructuringAssignment, isNodeInPattern, isStaticProperty } from './esmWalker' -import type { WorkspaceProject } from './workspace' -import type { Vitest } from './core' const API_NOT_FOUND_ERROR = `There are some problems in resolving the mocks API. You may encounter this issue when importing the mocks API from another module other than 'vitest'. @@ -60,17 +58,22 @@ const skipHijack = [ /vite\/dist\/client/, ] +interface Options { + hijackESM?: boolean + cacheDir: string +} + // this is basically copypaste from Vite SSR // this method transforms all import and export statements into `__vi_injected__` variable // to allow spying on them. this can be disabled by setting `slowHijackESM` to `false` // to not parse the module twice, we reuse the ast to hoist vi.mock here // and transform imports into dynamic ones if vi.mock is present -export function injectVitestModule(project: WorkspaceProject | Vitest, code: string, id: string, parse: (code: string, options: any) => AcornNode) { +export function injectVitestModule(code: string, id: string, parse: (code: string, options: any) => AcornNode, options: Options) { if (skipHijack.some(skip => id.match(skip))) return const hasMocks = regexpHoistable.test(code) - const hijackEsm = project.config.slowHijackESM ?? false + const hijackEsm = options.hijackESM ?? false if (!hasMocks && !hijackEsm) return @@ -273,7 +276,7 @@ export function injectVitestModule(project: WorkspaceProject | Vitest, code: str node.start + 14 /* 'export default'.length */, `${viInjectedKey}.default =`, ) - if (id.startsWith(project.server.config.cacheDir)) { + if (id.startsWith(options.cacheDir)) { // keep export default for optimized dependencies s.append(`\nexport default { ${viInjectedKey}: ${viInjectedKey}.default };\n`) } diff --git a/packages/vitest/src/node/plugins/esmTransform.ts b/packages/vitest/src/node/plugins/esmTransform.ts index 1f7e0920106f..8d72176910a6 100644 --- a/packages/vitest/src/node/plugins/esmTransform.ts +++ b/packages/vitest/src/node/plugins/esmTransform.ts @@ -8,7 +8,10 @@ export function ESMTransformPlugin(ctx: WorkspaceProject | Vitest): Plugin { name: 'vitest:mocker-plugin', enforce: 'post', transform(source, id) { - return injectVitestModule(ctx, source, id, (code, options) => this.parse(code, options)) + return injectVitestModule(source, id, (code, options) => this.parse(code, options), { + hijackESM: (ctx.config.browser.enabled && ctx.config.slowHijackESM) ?? false, + cacheDir: ctx.server.config.cacheDir, + }) }, } } From 8166d02f6c2e4e1b5c5b8adb7f7209f465cd36c1 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 26 Apr 2023 18:10:23 +0200 Subject: [PATCH 17/22] test: add more tests for injector --- packages/vitest/src/node/esmInjector.ts | 4 +- pnpm-lock.yaml | 17 +- test/browser/test/mocked.test.ts | 20 + test/core/package.json | 1 + test/core/test/injector-esm.test.ts | 899 ++++++++++++++++++++++++ test/core/test/injector-mock.test.ts | 110 +++ 6 files changed, 1039 insertions(+), 12 deletions(-) create mode 100644 test/core/test/injector-esm.test.ts create mode 100644 test/core/test/injector-mock.test.ts diff --git a/packages/vitest/src/node/esmInjector.ts b/packages/vitest/src/node/esmInjector.ts index b9943f5e6ed2..7826c70aeaab 100644 --- a/packages/vitest/src/node/esmInjector.ts +++ b/packages/vitest/src/node/esmInjector.ts @@ -124,8 +124,8 @@ export function injectVitestModule(code: string, id: string, parse: (code: strin const hasSpecifiers = node.specifiers.length > 0 if (hasMocks) { const code = hasSpecifiers - ? `const { ${viInjectedKey}: ${importId} } = await import('${source}')\n` - : `await import('${source}')\n` + ? `const { ${viInjectedKey}: ${importId} } = await __vi_wrap_module__(import('${source}'))\n` + : `await __vi_wrap_module__(import('${source}'))\n` return { code, id: importId, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f63c2e8e7e86..c547a1e9bf61 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1500,6 +1500,9 @@ importers: '@vitest/utils': specifier: workspace:* version: link:../../packages/utils + acorn: + specifier: ^8.8.2 + version: 8.8.2 tinyspy: specifier: ^1.0.2 version: 1.0.2 @@ -8307,7 +8310,7 @@ packages: /@types/jsdom@21.1.1: resolution: {integrity: sha512-cZFuoVLtzKP3gmq9eNosUL1R50U+USkbLtUQ1bYVgl/lKp0FZM7Cq4aIHAL8oIvQ17uSHi7jXPtfDOdjPwBE7A==} dependencies: - '@types/node': 18.16.0 + '@types/node': 18.16.1 '@types/tough-cookie': 4.0.2 parse5: 7.1.2 dev: true @@ -8410,7 +8413,7 @@ packages: /@types/prompts@2.4.4: resolution: {integrity: sha512-p5N9uoTH76lLvSAaYSZtBCdEXzpOOufsRjnhjVSrZGXikVGHX9+cc9ERtHRV4hvBKHyZb1bg4K+56Bd2TqUn4A==} dependencies: - '@types/node': 18.16.0 + '@types/node': 18.16.1 kleur: 3.0.3 dev: true @@ -9938,12 +9941,6 @@ packages: hasBin: true dev: true - /acorn@8.8.1: - resolution: {integrity: sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==} - engines: {node: '>=0.4.0'} - hasBin: true - dev: true - /acorn@8.8.2: resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} engines: {node: '>=0.4.0'} @@ -17391,7 +17388,7 @@ packages: optional: true dependencies: abab: 2.0.6 - acorn: 8.8.1 + acorn: 8.8.2 acorn-globals: 7.0.1 cssom: 0.5.0 cssstyle: 2.3.0 @@ -24154,7 +24151,7 @@ packages: resolution: {integrity: sha512-fyRdDc7vUBje5II0NdpjTTGh9BeTRtva+pDO52dmHiou1lF5Zths7/RlYpRb8xqYeWnCsAaSb7NTkskDvZxbow==} engines: {node: ^16.13 || >=18} dependencies: - '@types/node': 18.16.0 + '@types/node': 18.16.1 '@wdio/config': 8.6.8 '@wdio/logger': 8.6.6 '@wdio/protocols': 8.6.6 diff --git a/test/browser/test/mocked.test.ts b/test/browser/test/mocked.test.ts index c85a3694abf8..4441b13ae301 100644 --- a/test/browser/test/mocked.test.ts +++ b/test/browser/test/mocked.test.ts @@ -1,6 +1,7 @@ import { expect, test, vi } from 'vitest' import * as actions from '../src/actions' import { calculator } from '../src/calculator' +import * as calculatorModule from '../src/calculator' test('spyOn works on ESM', () => { vi.spyOn(actions, 'plus').mockReturnValue(30) @@ -8,3 +9,22 @@ test('spyOn works on ESM', () => { vi.mocked(actions.plus).mockRestore() expect(calculator('plus', 1, 2)).toBe(3) }) + +test('has module name', () => { + expect(String(actions)).toBe('[object Module]') + expect(actions[Symbol.toStringTag]).toBe('Module') +}) + +test('exports are correct', () => { + expect(Object.keys(actions)).toEqual(['plus']) + expect(Object.keys(calculatorModule)).toEqual(['calculator']) + expect(calculatorModule.calculator).toBe(calculator) +}) + +test('imports are still the same', async () => { + // @ts-expect-error typescript resolution + await expect(import('../src/calculator')).resolves.toBe(calculatorModule) + // @ts-expect-error typescript resolution + // eslint-disable-next-line @typescript-eslint/quotes + await expect(import(`../src/calculator`)).resolves.toBe(calculatorModule) +}) diff --git a/test/core/package.json b/test/core/package.json index 0186820d3b83..998f1603a476 100644 --- a/test/core/package.json +++ b/test/core/package.json @@ -9,6 +9,7 @@ "@vitest/expect": "workspace:*", "@vitest/runner": "workspace:*", "@vitest/utils": "workspace:*", + "acorn": "^8.8.2", "tinyspy": "^1.0.2", "url": "^0.11.0", "vitest": "workspace:*" diff --git a/test/core/test/injector-esm.test.ts b/test/core/test/injector-esm.test.ts new file mode 100644 index 000000000000..20d62f23a155 --- /dev/null +++ b/test/core/test/injector-esm.test.ts @@ -0,0 +1,899 @@ +import { Parser } from 'acorn' +import { injectVitestModule } from 'vitest/src/node/esmInjector' +import { expect, test } from 'vitest' +import { transformWithEsbuild } from 'vite' + +function parse(code: string, options: any) { + return Parser.parse(code, options) +} + +function injectSimpleCode(code: string) { + return injectVitestModule(code, '/test.js', parse, { + hijackESM: true, + cacheDir: '/tmp', + })?.code +} + +test('default import', async () => { + expect( + injectSimpleCode('import foo from \'vue\';console.log(foo.bar)'), + ).toMatchInlineSnapshot(` + "import { __vi_inject__ as __vi_esm_0__ } from 'vue' + console.log(__vi_esm_0__.default.bar)" + `) +}) + +test('named import', async () => { + expect( + injectSimpleCode( + 'import { ref } from \'vue\';function foo() { return ref(0) }', + ), + ).toMatchInlineSnapshot(` + "import { __vi_inject__ as __vi_esm_0__ } from 'vue' + function foo() { return __vi_esm_0__.ref(0) }" + `) +}) + +test('namespace import', async () => { + expect( + injectSimpleCode( + 'import * as vue from \'vue\';function foo() { return vue.ref(0) }', + ), + ).toMatchInlineSnapshot(` + "import { __vi_inject__ as __vi_esm_0__ } from 'vue' + function foo() { return __vi_esm_0__.ref(0) }" + `) +}) + +test('export function declaration', async () => { + expect(injectSimpleCode('export function foo() {}')) + .toMatchInlineSnapshot(` + "const __vi_inject__ = { [Symbol.toStringTag]: \\"Module\\" }; + function foo() {} + Object.defineProperty(__vi_inject__, \\"foo\\", { enumerable: true, configurable: true, get(){ return foo }}); + export { __vi_inject__ }" + `) +}) + +test('export class declaration', async () => { + expect(await injectSimpleCode('export class foo {}')) + .toMatchInlineSnapshot(` + "const __vi_inject__ = { [Symbol.toStringTag]: \\"Module\\" }; + class foo {} + Object.defineProperty(__vi_inject__, \\"foo\\", { enumerable: true, configurable: true, get(){ return foo }}); + export { __vi_inject__ }" + `) +}) + +test('export var declaration', async () => { + expect(await injectSimpleCode('export const a = 1, b = 2')) + .toMatchInlineSnapshot(` + "const __vi_inject__ = { [Symbol.toStringTag]: \\"Module\\" }; + const a = 1, b = 2 + Object.defineProperty(__vi_inject__, \\"a\\", { enumerable: true, configurable: true, get(){ return a }}); + Object.defineProperty(__vi_inject__, \\"b\\", { enumerable: true, configurable: true, get(){ return b }}); + export { __vi_inject__ }" + `) +}) + +test('export named', async () => { + expect( + injectSimpleCode('const a = 1, b = 2; export { a, b as c }'), + ).toMatchInlineSnapshot(` + "const __vi_inject__ = { [Symbol.toStringTag]: \\"Module\\" }; + const a = 1, b = 2; + Object.defineProperty(__vi_inject__, \\"a\\", { enumerable: true, configurable: true, get(){ return a }}); + Object.defineProperty(__vi_inject__, \\"c\\", { enumerable: true, configurable: true, get(){ return b }}); + export { __vi_inject__ }" + `) +}) + +test('export named from', async () => { + expect( + injectSimpleCode('export { ref, computed as c } from \'vue\''), + ).toMatchInlineSnapshot(` + "const __vi_inject__ = { [Symbol.toStringTag]: \\"Module\\" }; + const { __vi_inject__: __vi_esm_0__ } = await import(\\"vue\\"); + + Object.defineProperty(__vi_inject__, \\"ref\\", { enumerable: true, configurable: true, get(){ return __vi_esm_0__.ref }}); + Object.defineProperty(__vi_inject__, \\"c\\", { enumerable: true, configurable: true, get(){ return __vi_esm_0__.computed }}); + export { __vi_inject__ }" + `) +}) + +test('named exports of imported binding', async () => { + expect( + injectSimpleCode( + 'import {createApp} from \'vue\';export {createApp}', + ), + ).toMatchInlineSnapshot(` + "const __vi_inject__ = { [Symbol.toStringTag]: \\"Module\\" }; + import { __vi_inject__ as __vi_esm_0__ } from 'vue' + + Object.defineProperty(__vi_inject__, \\"createApp\\", { enumerable: true, configurable: true, get(){ return __vi_esm_0__.createApp }}); + export { __vi_inject__ }" + `) +}) + +test('export * from', async () => { + expect( + injectSimpleCode( + 'export * from \'vue\'\n' + 'export * from \'react\'', + ), + ).toMatchInlineSnapshot(` + "const __vi_inject__ = { [Symbol.toStringTag]: \\"Module\\" }; + const { __vi_inject__: __vi_esm_0__ } = await import(\\"vue\\"); + __vi_export_all__(__vi_inject__, __vi_esm_0__); + const { __vi_inject__: __vi_esm_1__ } = await import(\\"react\\"); + __vi_export_all__(__vi_inject__, __vi_esm_1__); + + + export { __vi_inject__ }" + `) +}) + +test('export * as from', async () => { + expect(injectSimpleCode('export * as foo from \'vue\'')) + .toMatchInlineSnapshot(` + "const __vi_inject__ = { [Symbol.toStringTag]: \\"Module\\" }; + const { __vi_inject__: __vi_esm_0__ } = await import(\\"vue\\"); + + Object.defineProperty(__vi_inject__, \\"foo\\", { enumerable: true, configurable: true, get(){ return __vi_esm_0__ }}); + export { __vi_inject__ }" + `) +}) + +test('export default', async () => { + expect( + injectSimpleCode('export default {}'), + ).toMatchInlineSnapshot(` + "const __vi_inject__ = { [Symbol.toStringTag]: \\"Module\\" }; + __vi_inject__.default = {} + export { __vi_inject__ }" + `) +}) + +test('export then import minified', async () => { + expect( + injectSimpleCode( + 'export * from \'vue\';import {createApp} from \'vue\';', + ), + ).toMatchInlineSnapshot(` + "const __vi_inject__ = { [Symbol.toStringTag]: \\"Module\\" }; + import { __vi_inject__ as __vi_esm_0__ } from 'vue' + const { __vi_inject__: __vi_esm_1__ } = await import(\\"vue\\"); + __vi_export_all__(__vi_inject__, __vi_esm_1__); + + export { __vi_inject__ }" + `) +}) + +test('hoist import to top', async () => { + expect( + injectSimpleCode( + 'path.resolve(\'server.js\');import path from \'node:path\';', + ), + ).toMatchInlineSnapshot(` + "import { __vi_inject__ as __vi_esm_0__ } from 'node:path' + __vi_esm_0__.default.resolve('server.js');" + `) +}) + +// test('import.meta', async () => { +// expect( +// injectSimpleCode('console.log(import.meta.url)'), +// ).toMatchInlineSnapshot('"console.log(__vite_ssr_import_meta__.url)"') +// }) + +test('dynamic import', async () => { + const result = injectSimpleCode( + 'export const i = () => import(\'./foo\')', + ) + expect(result).toMatchInlineSnapshot(` + "const __vi_inject__ = { [Symbol.toStringTag]: \\"Module\\" }; + const i = () => __vi_wrap_module__(import('./foo')) + export { __vi_inject__ }" + `) +}) + +test('do not rewrite method definition', async () => { + const result = injectSimpleCode( + 'import { fn } from \'vue\';class A { fn() { fn() } }', + ) + expect(result).toMatchInlineSnapshot(` + "import { __vi_inject__ as __vi_esm_0__ } from 'vue' + class A { fn() { __vi_esm_0__.fn() } }" + `) +}) + +test('do not rewrite when variable is in scope', async () => { + const result = injectSimpleCode( + 'import { fn } from \'vue\';function A(){ const fn = () => {}; return { fn }; }', + ) + expect(result).toMatchInlineSnapshot(` + "import { __vi_inject__ as __vi_esm_0__ } from 'vue' + function A(){ const fn = () => {}; return { fn }; }" + `) +}) + +// #5472 +test('do not rewrite when variable is in scope with object destructuring', async () => { + const result = injectSimpleCode( + 'import { fn } from \'vue\';function A(){ let {fn, test} = {fn: \'foo\', test: \'bar\'}; return { fn }; }', + ) + expect(result).toMatchInlineSnapshot(` + "import { __vi_inject__ as __vi_esm_0__ } from 'vue' + function A(){ let {fn, test} = {fn: 'foo', test: 'bar'}; return { fn }; }" + `) +}) + +// #5472 +test('do not rewrite when variable is in scope with array destructuring', async () => { + const result = injectSimpleCode( + 'import { fn } from \'vue\';function A(){ let [fn, test] = [\'foo\', \'bar\']; return { fn }; }', + ) + expect(result).toMatchInlineSnapshot(` + "import { __vi_inject__ as __vi_esm_0__ } from 'vue' + function A(){ let [fn, test] = ['foo', 'bar']; return { fn }; }" + `) +}) + +// #5727 +test('rewrite variable in string interpolation in function nested arguments', async () => { + const result = injectSimpleCode( + // eslint-disable-next-line no-template-curly-in-string + 'import { fn } from \'vue\';function A({foo = `test${fn}`} = {}){ return {}; }', + ) + expect(result).toMatchInlineSnapshot(` + "import { __vi_inject__ as __vi_esm_0__ } from 'vue' + function A({foo = \`test\${__vi_esm_0__.fn}\`} = {}){ return {}; }" + `) +}) + +// #6520 +test('rewrite variables in default value of destructuring params', async () => { + const result = injectSimpleCode( + 'import { fn } from \'vue\';function A({foo = fn}){ return {}; }', + ) + expect(result).toMatchInlineSnapshot(` + "import { __vi_inject__ as __vi_esm_0__ } from 'vue' + function A({foo = __vi_esm_0__.fn}){ return {}; }" + `) +}) + +test('do not rewrite when function declaration is in scope', async () => { + const result = injectSimpleCode( + 'import { fn } from \'vue\';function A(){ function fn() {}; return { fn }; }', + ) + expect(result).toMatchInlineSnapshot(` + "import { __vi_inject__ as __vi_esm_0__ } from 'vue' + function A(){ function fn() {}; return { fn }; }" + `) +}) + +test('do not rewrite catch clause', async () => { + const result = injectSimpleCode( + 'import {error} from \'./dependency\';try {} catch(error) {}', + ) + expect(result).toMatchInlineSnapshot(` + "import { __vi_inject__ as __vi_esm_0__ } from './dependency' + try {} catch(error) {}" + `) +}) + +// #2221 +test('should declare variable for imported super class', async () => { + expect( + injectSimpleCode( + 'import { Foo } from \'./dependency\';' + 'class A extends Foo {}', + ), + ).toMatchInlineSnapshot(` + "import { __vi_inject__ as __vi_esm_0__ } from './dependency' + const Foo = __vi_esm_0__.Foo; + class A extends Foo {}" + `) + + // exported classes: should prepend the declaration at root level, before the + // first class that uses the binding + expect( + injectSimpleCode( + 'import { Foo } from \'./dependency\';' + + 'export default class A extends Foo {}\n' + + 'export class B extends Foo {}', + ), + ).toMatchInlineSnapshot(` + "const __vi_inject__ = { [Symbol.toStringTag]: \\"Module\\" }; + import { __vi_inject__ as __vi_esm_0__ } from './dependency' + const Foo = __vi_esm_0__.Foo; + class A extends Foo {} + class B extends Foo {} + Object.defineProperty(__vi_inject__, \\"B\\", { enumerable: true, configurable: true, get(){ return B }}); + Object.defineProperty(__vi_inject__, \\"default\\", { enumerable: true, configurable: true, value: A }); + export { __vi_inject__ }" + `) +}) + +// #4049 +test('should handle default export variants', async () => { + // default anonymous functions + expect(injectSimpleCode('export default function() {}\n')) + .toMatchInlineSnapshot(` + "const __vi_inject__ = { [Symbol.toStringTag]: \\"Module\\" }; + __vi_inject__.default = function() {} + + export { __vi_inject__ }" + `) + // default anonymous class + expect(injectSimpleCode('export default class {}\n')) + .toMatchInlineSnapshot(` + "const __vi_inject__ = { [Symbol.toStringTag]: \\"Module\\" }; + __vi_inject__.default = class {} + + export { __vi_inject__ }" + `) + // default named functions + expect( + injectSimpleCode( + 'export default function foo() {}\n' + + 'foo.prototype = Object.prototype;', + ), + ).toMatchInlineSnapshot(` + "const __vi_inject__ = { [Symbol.toStringTag]: \\"Module\\" }; + function foo() {} + foo.prototype = Object.prototype; + Object.defineProperty(__vi_inject__, \\"default\\", { enumerable: true, configurable: true, value: foo }); + export { __vi_inject__ }" + `) + // default named classes + expect( + injectSimpleCode( + 'export default class A {}\n' + 'export class B extends A {}', + ), + ).toMatchInlineSnapshot(` + "const __vi_inject__ = { [Symbol.toStringTag]: \\"Module\\" }; + class A {} + class B extends A {} + Object.defineProperty(__vi_inject__, \\"B\\", { enumerable: true, configurable: true, get(){ return B }}); + Object.defineProperty(__vi_inject__, \\"default\\", { enumerable: true, configurable: true, value: A }); + export { __vi_inject__ }" + `) +}) + +test('overwrite bindings', async () => { + expect( + injectSimpleCode( + 'import { inject } from \'vue\';' + + 'const a = { inject }\n' + + 'const b = { test: inject }\n' + + 'function c() { const { test: inject } = { test: true }; console.log(inject) }\n' + + 'const d = inject\n' + + 'function f() { console.log(inject) }\n' + + 'function e() { const { inject } = { inject: true } }\n' + + 'function g() { const f = () => { const inject = true }; console.log(inject) }\n', + ), + ).toMatchInlineSnapshot(` + "import { __vi_inject__ as __vi_esm_0__ } from 'vue' + const a = { inject: __vi_esm_0__.inject } + const b = { test: __vi_esm_0__.inject } + function c() { const { test: inject } = { test: true }; console.log(inject) } + const d = __vi_esm_0__.inject + function f() { console.log(__vi_esm_0__.inject) } + function e() { const { inject } = { inject: true } } + function g() { const f = () => { const inject = true }; console.log(__vi_esm_0__.inject) } + " + `) +}) + +test('Empty array pattern', async () => { + expect( + injectSimpleCode('const [, LHS, RHS] = inMatch;'), + ).toMatchInlineSnapshot('"const [, LHS, RHS] = inMatch;"') +}) + +test('function argument destructure', async () => { + expect( + injectSimpleCode( + ` +import { foo, bar } from 'foo' +const a = ({ _ = foo() }) => {} +function b({ _ = bar() }) {} +function c({ _ = bar() + foo() }) {} +`, + ), + ).toMatchInlineSnapshot(` + "import { __vi_inject__ as __vi_esm_0__ } from 'foo' + + + const a = ({ _ = __vi_esm_0__.foo() }) => {} + function b({ _ = __vi_esm_0__.bar() }) {} + function c({ _ = __vi_esm_0__.bar() + __vi_esm_0__.foo() }) {} + " + `) +}) + +test('object destructure alias', async () => { + expect( + injectSimpleCode( + ` +import { n } from 'foo' +const a = () => { + const { type: n = 'bar' } = {} + console.log(n) +} +`, + ), + ).toMatchInlineSnapshot(` + "import { __vi_inject__ as __vi_esm_0__ } from 'foo' + + + const a = () => { + const { type: n = 'bar' } = {} + console.log(n) + } + " + `) + + // #9585 + expect( + injectSimpleCode( + ` +import { n, m } from 'foo' +const foo = {} + +{ + const { [n]: m } = foo +} +`, + ), + ).toMatchInlineSnapshot(` + "import { __vi_inject__ as __vi_esm_0__ } from 'foo' + + + const foo = {} + + { + const { [__vi_esm_0__.n]: m } = foo + } + " + `) +}) + +test('nested object destructure alias', async () => { + expect( + injectSimpleCode( + ` +import { remove, add, get, set, rest, objRest } from 'vue' + +function a() { + const { + o: { remove }, + a: { b: { c: [ add ] }}, + d: [{ get }, set, ...rest], + ...objRest + } = foo + + remove() + add() + get() + set() + rest() + objRest() +} + +remove() +add() +get() +set() +rest() +objRest() +`, + ), + ).toMatchInlineSnapshot(` + "import { __vi_inject__ as __vi_esm_0__ } from 'vue' + + + + function a() { + const { + o: { remove }, + a: { b: { c: [ add ] }}, + d: [{ get }, set, ...rest], + ...objRest + } = foo + + remove() + add() + get() + set() + rest() + objRest() + } + + __vi_esm_0__.remove() + __vi_esm_0__.add() + __vi_esm_0__.get() + __vi_esm_0__.set() + __vi_esm_0__.rest() + __vi_esm_0__.objRest() + " + `) +}) + +test('object props and methods', async () => { + expect( + injectSimpleCode( + ` +import foo from 'foo' + +const bar = 'bar' + +const obj = { + foo() {}, + [foo]() {}, + [bar]() {}, + foo: () => {}, + [foo]: () => {}, + [bar]: () => {}, + bar(foo) {} +} +`, + ), + ).toMatchInlineSnapshot(` + "import { __vi_inject__ as __vi_esm_0__ } from 'foo' + + + + const bar = 'bar' + + const obj = { + foo() {}, + [__vi_esm_0__.default]() {}, + [bar]() {}, + foo: () => {}, + [__vi_esm_0__.default]: () => {}, + [bar]: () => {}, + bar(foo) {} + } + " + `) +}) + +test('class props', async () => { + expect( + injectSimpleCode( + ` +import { remove, add } from 'vue' + +class A { + remove = 1 + add = null +} +`, + ), + ).toMatchInlineSnapshot(` + "import { __vi_inject__ as __vi_esm_0__ } from 'vue' + + + + const add = __vi_esm_0__.add; + const remove = __vi_esm_0__.remove; + class A { + remove = 1 + add = null + } + " + `) +}) + +test('class methods', async () => { + expect( + injectSimpleCode( + ` +import foo from 'foo' + +const bar = 'bar' + +class A { + foo() {} + [foo]() {} + [bar]() {} + #foo() {} + bar(foo) {} +} +`, + ), + ).toMatchInlineSnapshot(` + "import { __vi_inject__ as __vi_esm_0__ } from 'foo' + + + + const bar = 'bar' + + class A { + foo() {} + [__vi_esm_0__.default]() {} + [bar]() {} + #foo() {} + bar(foo) {} + } + " + `) +}) + +test('declare scope', async () => { + expect( + injectSimpleCode( + ` +import { aaa, bbb, ccc, ddd } from 'vue' + +function foobar() { + ddd() + + const aaa = () => { + bbb(ccc) + ddd() + } + const bbb = () => { + console.log('hi') + } + const ccc = 1 + function ddd() {} + + aaa() + bbb() + ccc() +} + +aaa() +bbb() +`, + ), + ).toMatchInlineSnapshot(` + "import { __vi_inject__ as __vi_esm_0__ } from 'vue' + + + + function foobar() { + ddd() + + const aaa = () => { + bbb(ccc) + ddd() + } + const bbb = () => { + console.log('hi') + } + const ccc = 1 + function ddd() {} + + aaa() + bbb() + ccc() + } + + __vi_esm_0__.aaa() + __vi_esm_0__.bbb() + " + `) +}) + +test('jsx', async () => { + const code = ` + import React from 'react' + import { Foo, Slot } from 'foo' + + function Bar({ Slot = }) { + return ( + <> + + + ) + } + ` + const id = '/foo.jsx' + const result = await transformWithEsbuild(code, id) + expect(injectSimpleCode(result.code)) + .toMatchInlineSnapshot(` + "import { __vi_inject__ as __vi_esm_0__ } from 'react' + import { __vi_inject__ as __vi_esm_1__ } from 'foo' + + + function Bar({ Slot: Slot2 = /* @__PURE__ */ __vi_esm_0__.default.createElement(__vi_esm_1__.Foo, null) }) { + return /* @__PURE__ */ __vi_esm_0__.default.createElement(__vi_esm_0__.default.Fragment, null, /* @__PURE__ */ __vi_esm_0__.default.createElement(Slot2, null)); + } + " + `) +}) + +test('continuous exports', async () => { + expect( + injectSimpleCode( + ` +export function fn1() { +}export function fn2() { +} + `, + ), + ).toMatchInlineSnapshot(` + "const __vi_inject__ = { [Symbol.toStringTag]: \\"Module\\" }; + + function fn1() { + } + Object.defineProperty(__vi_inject__, \\"fn1\\", { enumerable: true, configurable: true, get(){ return fn1 }});function fn2() { + } + Object.defineProperty(__vi_inject__, \\"fn2\\", { enumerable: true, configurable: true, get(){ return fn2 }}); + + export { __vi_inject__ }" + `) +}) + +// https://github.com/vitest-dev/vitest/issues/1141 +test('export default expression', async () => { + // esbuild transform result of following TS code + // export default function getRandom() { + // return Math.random() + // } + const code = ` +export default (function getRandom() { + return Math.random(); +}); +`.trim() + + expect(injectSimpleCode(code)).toMatchInlineSnapshot(` + "const __vi_inject__ = { [Symbol.toStringTag]: \\"Module\\" }; + __vi_inject__.default = (function getRandom() { + return Math.random(); + }); + export { __vi_inject__ }" + `) + + expect( + injectSimpleCode('export default (class A {});'), + ).toMatchInlineSnapshot(` + "const __vi_inject__ = { [Symbol.toStringTag]: \\"Module\\" }; + __vi_inject__.default = (class A {}); + export { __vi_inject__ }" + `) +}) + +// #8002 +// test('with hashbang', async () => { +// expect( +// injectSimpleCode( +// `#!/usr/bin/env node +// console.log("it can parse the hashbang")`, +// ), +// ).toMatchInlineSnapshot(` +// "#!/usr/bin/env node +// console.log(\\"it can parse the hashbang\\")" +// `) +// }) + +// test('import hoisted after hashbang', async () => { +// expect( +// await injectSimpleCode( +// `#!/usr/bin/env node +// import "foo"`, +// ), +// ).toMatchInlineSnapshot(` +// "#!/usr/bin/env node +// const __vite_ssr_import_0__ = await __vite_ssr_import__(\\"foo\\"); +// " +// `) +// }) + +// #10289 +test('track scope by class, function, condition blocks', async () => { + const code = ` +import { foo, bar } from 'foobar' +if (false) { + const foo = 'foo' + console.log(foo) +} else if (false) { + const [bar] = ['bar'] + console.log(bar) +} else { + console.log(foo) + console.log(bar) +} +export class Test { + constructor() { + if (false) { + const foo = 'foo' + console.log(foo) + } else if (false) { + const [bar] = ['bar'] + console.log(bar) + } else { + console.log(foo) + console.log(bar) + } + } +};`.trim() + + expect(injectSimpleCode(code)).toMatchInlineSnapshot(` + "const __vi_inject__ = { [Symbol.toStringTag]: \\"Module\\" }; + import { __vi_inject__ as __vi_esm_0__ } from 'foobar' + + if (false) { + const foo = 'foo' + console.log(foo) + } else if (false) { + const [bar] = ['bar'] + console.log(bar) + } else { + console.log(__vi_esm_0__.foo) + console.log(__vi_esm_0__.bar) + } + class Test { + constructor() { + if (false) { + const foo = 'foo' + console.log(foo) + } else if (false) { + const [bar] = ['bar'] + console.log(bar) + } else { + console.log(__vi_esm_0__.foo) + console.log(__vi_esm_0__.bar) + } + } + } + Object.defineProperty(__vi_inject__, \\"Test\\", { enumerable: true, configurable: true, get(){ return Test }});; + export { __vi_inject__ }" + `) +}) + +// #10386 +test('track var scope by function', async () => { + expect( + injectSimpleCode(` +import { foo, bar } from 'foobar' +function test() { + if (true) { + var foo = () => { var why = 'would' }, bar = 'someone' + } + return [foo, bar] +}`), + ).toMatchInlineSnapshot(` + "import { __vi_inject__ as __vi_esm_0__ } from 'foobar' + + + function test() { + if (true) { + var foo = () => { var why = 'would' }, bar = 'someone' + } + return [foo, bar] + }" + `) +}) + +// #11806 +test('track scope by blocks', async () => { + expect( + injectSimpleCode(` +import { foo, bar, baz } from 'foobar' +function test() { + [foo]; + { + let foo = 10; + let bar = 10; + } + try {} catch (baz){ baz }; + return bar; +}`), + ).toMatchInlineSnapshot(` + "import { __vi_inject__ as __vi_esm_0__ } from 'foobar' + + + function test() { + [__vi_esm_0__.foo]; + { + let foo = 10; + let bar = 10; + } + try {} catch (baz){ baz }; + return __vi_esm_0__.bar; + }" + `) +}) diff --git a/test/core/test/injector-mock.test.ts b/test/core/test/injector-mock.test.ts new file mode 100644 index 000000000000..fb4bcc519c70 --- /dev/null +++ b/test/core/test/injector-mock.test.ts @@ -0,0 +1,110 @@ +import { Parser } from 'acorn' +import { injectVitestModule } from 'vitest/src/node/esmInjector' +import { expect, test } from 'vitest' + +function parse(code: string, options: any) { + return Parser.parse(code, options) +} + +function injectSimpleCode(code: string) { + return injectVitestModule(code, '/test.js', parse, { + hijackESM: false, + cacheDir: '/tmp', + })?.code.trim() +} + +function injectHijackedCode(code: string) { + return injectVitestModule(code, '/test.js', parse, { + hijackESM: true, + cacheDir: '/tmp', + })?.code.trim() +} + +test('hoists mock, unmock, hoisted', () => { + expect(injectSimpleCode(` + vi.mock('path', () => {}) + vi.unmock('path') + vi.hoisted(() => {}) + `)).toMatchInlineSnapshot(` + "if (typeof globalThis.vi === \\"undefined\\" && typeof globalThis.vitest === \\"undefined\\") { throw new Error(\\"There are some problems in resolving the mocks API.\\\\nYou may encounter this issue when importing the mocks API from another module other than 'vitest'.\\\\nTo fix this issue you can either:\\\\n- import the mocks API directly from 'vitest'\\\\n- enable the 'globals' options\\") } + vi.mock('path', () => {}) + vi.unmock('path') + vi.hoisted(() => {})" + `) +}) + +test('always hoists import from vitest', () => { + expect(injectSimpleCode(` + import { vi } from 'vitest' + vi.mock('path', () => {}) + vi.unmock('path') + vi.hoisted(() => {}) + import { test } from 'vitest' + `)).toMatchInlineSnapshot(` + "import { vi } from 'vitest' + import { test } from 'vitest' + vi.mock('path', () => {}) + vi.unmock('path') + vi.hoisted(() => {})" + `) +}) + +test('always hoists mock, unmock, hoisted when modules are hijacked', () => { + expect(injectHijackedCode(` + import { vi } from 'vitest' + vi.mock('path', () => {}) + vi.unmock('path') + vi.hoisted(() => {}) + import { test } from 'vitest' + `)).toMatchInlineSnapshot(` + "import { __vi_inject__ as __vi_esm_0__ } from 'vitest' + const { vi } = __vi_esm_0__; + import { __vi_inject__ as __vi_esm_1__ } from 'vitest' + const { test } = __vi_esm_1__; + vi.mock('path', () => {}) + vi.unmock('path') + vi.hoisted(() => {})" + `) +}) + +test('always hoists all imports but they are under mocks', () => { + expect(injectSimpleCode(` + import { vi } from 'vitest' + import { someValue } from './path.js' + import { someValue2 } from './path2.js' + vi.mock('path', () => {}) + vi.unmock('path') + vi.hoisted(() => {}) + import { test } from 'vitest' + `)).toMatchInlineSnapshot(` + "import { vi } from 'vitest' + import { test } from 'vitest' + vi.mock('path', () => {}) + vi.unmock('path') + vi.hoisted(() => {}) + const { someValue } = await import('./path.js') + const { someValue2 } = await import('./path2.js')" + `) +}) + +test('always hoists all imports but they are under mocks when modules are hijacked', () => { + expect(injectHijackedCode(` + import { vi } from 'vitest' + import { someValue } from './path.js' + import { someValue2 } from './path2.js' + vi.mock('path', () => {}) + vi.unmock('path') + vi.hoisted(() => {}) + import { test } from 'vitest' + `)).toMatchInlineSnapshot(` + "import { __vi_inject__ as __vi_esm_0__ } from 'vitest' + const { vi } = __vi_esm_0__; + import { __vi_inject__ as __vi_esm_3__ } from 'vitest' + const { test } = __vi_esm_3__; + vi.mock('path', () => {}) + vi.unmock('path') + vi.hoisted(() => {}) + const { __vi_inject__: __vi_esm_1__ } = await __vi_wrap_module__(import('./path.js')) + const { __vi_inject__: __vi_esm_2__ } = await __vi_wrap_module__(import('./path2.js'))" + `) +}) From 4d674b9411fe33106fffed9d9d8322fea5732609 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 27 Apr 2023 13:55:56 +0200 Subject: [PATCH 18/22] chore: refactor mock hoisting and esm injector into two separate functions --- docs/config/index.md | 18 +- packages/browser/package.json | 5 +- .../src/node/esmInjector.ts | 240 ++++-------------- .../{vitest => browser}/src/node/esmWalker.ts | 17 +- packages/browser/src/node/index.ts | 14 +- packages/vitest/package.json | 2 - .../vitest/src/integrations/browser/server.ts | 6 +- packages/vitest/src/node/config.ts | 4 +- packages/vitest/src/node/hoistMocks.ts | 184 ++++++++++++++ .../vitest/src/node/plugins/esmTransform.ts | 17 -- packages/vitest/src/node/plugins/index.ts | 4 +- packages/vitest/src/node/plugins/mocks.ts | 12 + packages/vitest/src/node/plugins/workspace.ts | 4 +- packages/vitest/src/runtime/mocker.ts | 5 - packages/vitest/src/types/browser.ts | 9 + packages/vitest/src/types/config.ts | 9 - pnpm-lock.yaml | 25 +- test/core/test/injector-esm.test.ts | 31 ++- test/core/test/injector-mock.test.ts | 70 +---- 19 files changed, 358 insertions(+), 318 deletions(-) rename packages/{vitest => browser}/src/node/esmInjector.ts (52%) rename packages/{vitest => browser}/src/node/esmWalker.ts (95%) create mode 100644 packages/vitest/src/node/hoistMocks.ts delete mode 100644 packages/vitest/src/node/plugins/esmTransform.ts create mode 100644 packages/vitest/src/node/plugins/mocks.ts diff --git a/docs/config/index.md b/docs/config/index.md index 0ffdcd3707ed..c6099958ea49 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -954,7 +954,7 @@ Listen to port and serve API. When set to true, the default port is 51204 ### browser -- **Type:** `{ enabled?, name?, provider?, headless?, api? }` +- **Type:** `{ enabled?, name?, provider?, headless?, api?, slowHijackESM? }` - **Default:** `{ enabled: false, headless: process.env.CI, api: 63315 }` - **Version:** Since Vitest 0.29.4 - **CLI:** `--browser`, `--browser=`, `--browser.name=chrome --browser.headless` @@ -1026,6 +1026,22 @@ export interface BrowserProvider { This is an advanced API for library authors. If you just need to run tests in a browser, use the [browser](/config/#browser) option. ::: +### browser.slowHijackESM + + +#### slowHijackESM + +- **Type:** `boolean` +- **Default:** `true` +- **Version:** Since Vitest 0.31.0 + +When running tests in Node.js Vitest can use its own module resolution to easily mock modules with `vi.mock` syntax. However it's not so easy to replicate ES module resolution in browser, so we need to transform your source files before browser can consume it. + +This option has no effect on tests running inside Node.js. + +This options is enabled by default when running in the browser. If you don't rely on spying on ES modules with `vi.spyOn` and don't use `vi.mock`, you can disable this to get a slight boost to performance. + + ### clearMocks - **Type:** `boolean` diff --git a/packages/browser/package.json b/packages/browser/package.json index 55b2e0fc5266..7d97e046818f 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -39,17 +39,20 @@ "prepublishOnly": "pnpm build" }, "peerDependencies": { - "vitest": ">=0.29.4" + "vitest": ">=0.31.0" }, "dependencies": { "modern-node-polyfills": "^0.1.1", "sirv": "^2.0.2" }, "devDependencies": { + "@types/estree": "^1.0.1", "@types/ws": "^8.5.4", "@vitest/runner": "workspace:*", "@vitest/ui": "workspace:*", "@vitest/ws-client": "workspace:*", + "estree-walker": "^3.0.3", + "periscopic": "^3.1.0", "rollup": "3.20.2", "vitest": "workspace:*" } diff --git a/packages/vitest/src/node/esmInjector.ts b/packages/browser/src/node/esmInjector.ts similarity index 52% rename from packages/vitest/src/node/esmInjector.ts rename to packages/browser/src/node/esmInjector.ts index 7826c70aeaab..54e6d64c2954 100644 --- a/packages/vitest/src/node/esmInjector.ts +++ b/packages/browser/src/node/esmInjector.ts @@ -1,57 +1,14 @@ import MagicString from 'magic-string' import { extract_names as extractNames } from 'periscopic' -import type { CallExpression, Expression, Identifier, ImportDeclaration, VariableDeclaration } from 'estree' -import { findNodeAround, simple as simpleWalk } from 'acorn-walk' +import type { Expression, ImportDeclaration } from 'estree' import type { AcornNode } from 'rollup' import type { Node, Positioned } from './esmWalker' import { esmWalker, isInDestructuringAssignment, isNodeInPattern, isStaticProperty } from './esmWalker' -const API_NOT_FOUND_ERROR = `There are some problems in resolving the mocks API. -You may encounter this issue when importing the mocks API from another module other than 'vitest'. -To fix this issue you can either: -- import the mocks API directly from 'vitest' -- enable the 'globals' options` - -const API_NOT_FOUND_CHECK = '\nif (typeof globalThis.vi === "undefined" && typeof globalThis.vitest === "undefined") ' -+ `{ throw new Error(${JSON.stringify(API_NOT_FOUND_ERROR)}) }\n` - -function isIdentifier(node: any): node is Positioned { - return node.type === 'Identifier' -} - -function transformImportSpecifiers(node: ImportDeclaration, mode: 'object' | 'named' = 'object') { - const specifiers = node.specifiers - - if (specifiers.length === 1 && specifiers[0].type === 'ImportNamespaceSpecifier') - return specifiers[0].local.name - - const dynamicImports = node.specifiers.map((specifier) => { - if (specifier.type === 'ImportDefaultSpecifier') - return `default ${mode === 'object' ? ':' : 'as'} ${specifier.local.name}` - - if (specifier.type === 'ImportSpecifier') { - const local = specifier.local.name - const imported = specifier.imported.name - if (local === imported) - return local - return `${imported} ${mode === 'object' ? ':' : 'as'} ${local}` - } - - return null - }).filter(Boolean).join(', ') - - if (!dynamicImports.length) - return '' - - return `{ ${dynamicImports} }` -} - const viInjectedKey = '__vi_inject__' // const viImportMetaKey = '__vi_import_meta__' // to allow overwrite const viExportAllHelper = '__vi_export_all__' -const regexpHoistable = /^[ \t]*\b(vi|vitest)\s*\.\s*(mock|unmock|hoisted)\(/m - const skipHijack = [ '/@vite/client', '/@vite/env', @@ -72,10 +29,9 @@ export function injectVitestModule(code: string, id: string, parse: (code: strin if (skipHijack.some(skip => id.match(skip))) return - const hasMocks = regexpHoistable.test(code) const hijackEsm = options.hijackESM ?? false - if (!hasMocks && !hijackEsm) + if (!hijackEsm) return const s = new MagicString(code) @@ -100,8 +56,6 @@ export function injectVitestModule(code: string, id: string, parse: (code: strin const hoistIndex = 0 let hasInjected = false - let hoistedCode = '' - let hoistedVitestImports = '' // this will tranfrom import statements into dynamic ones, if there are imports // it will keep the import as is, if we don't need to mock anything @@ -110,27 +64,11 @@ export function injectVitestModule(code: string, id: string, parse: (code: strin const transformImportDeclaration = (node: ImportDeclaration) => { const source = node.source.value as string - // if we don't hijack ESM and process this file, then we definetly have mocks, - // so we need to transform imports into dynamic ones, so "vi.mock" can be executed before - if (!hijackEsm || skipHijack.some(skip => source.match(skip))) { - const specifiers = transformImportSpecifiers(node) - const code = specifiers - ? `const ${specifiers} = await import('${source}')\n` - : `await import('${source}')\n` - return { code } - } + if (skipHijack.some(skip => source.match(skip))) + return null const importId = `__vi_esm_${uid++}__` const hasSpecifiers = node.specifiers.length > 0 - if (hasMocks) { - const code = hasSpecifiers - ? `const { ${viInjectedKey}: ${importId} } = await __vi_wrap_module__(import('${source}'))\n` - : `await __vi_wrap_module__(import('${source}'))\n` - return { - code, - id: importId, - } - } const code = hasSpecifiers ? `import { ${viInjectedKey} as ${importId} } from '${source}'\n` : `import '${source}'\n` @@ -141,19 +79,11 @@ export function injectVitestModule(code: string, id: string, parse: (code: strin } function defineImport(node: ImportDeclaration) { - // always hoist vitest import to top of the file, so - // "vi" helpers can access it - if (node.source.value === 'vitest') { - const importId = `__vi_esm_${uid++}__` - const code = hijackEsm - ? `import { ${viInjectedKey} as ${importId} } from 'vitest'\nconst ${transformImportSpecifiers(node)} = ${importId};\n` - : `import ${transformImportSpecifiers(node, 'named')} from 'vitest'\n` - hoistedVitestImports += code - return - } - const { code, id } = transformImportDeclaration(node) - s.appendLeft(hoistIndex, code) - return id + const declaration = transformImportDeclaration(node) + if (!declaration) + return null + s.appendLeft(hoistIndex, declaration.code) + return declaration.id } function defineImportAll(source: string) { @@ -178,9 +108,9 @@ export function injectVitestModule(code: string, id: string, parse: (code: strin // import * as ok from 'foo' --> ok -> __import_foo__ if (node.type === 'ImportDeclaration') { const importId = defineImport(node) - s.remove(node.start, node.end) - if (!hijackEsm || !importId) + if (!importId) continue + s.remove(node.start, node.end) for (const spec of node.specifiers) { if (spec.type === 'ImportSpecifier') { idToImportMap.set( @@ -201,9 +131,6 @@ export function injectVitestModule(code: string, id: string, parse: (code: strin // 2. check all export statements and define exports for (const node of ast.body as Node[]) { - if (!hijackEsm) - break - // named exports if (node.type === 'ExportNamedDeclaration') { if (node.declaration) { @@ -298,115 +225,50 @@ export function injectVitestModule(code: string, id: string, parse: (code: strin } } - function CallExpression(node: Positioned) { - if ( - node.callee.type === 'MemberExpression' - && isIdentifier(node.callee.object) - && (node.callee.object.name === 'vi' || node.callee.object.name === 'vitest') - && isIdentifier(node.callee.property) - ) { - const methodName = node.callee.property.name - - if (methodName === 'mock' || methodName === 'unmock') { - hoistedCode += `${code.slice(node.start, node.end)}\n` - s.remove(node.start, node.end) - } - - if (methodName === 'hoisted') { - const declarationNode = findNodeAround(ast, node.start, 'VariableDeclaration')?.node as Positioned | undefined - const init = declarationNode?.declarations[0]?.init - const isViHoisted = (node: CallExpression) => { - return node.callee.type === 'MemberExpression' - && isIdentifier(node.callee.object) - && (node.callee.object.name === 'vi' || node.callee.object.name === 'vitest') - && isIdentifier(node.callee.property) - && node.callee.property.name === 'hoisted' - } - - const canMoveDeclaration = (init - && init.type === 'CallExpression' - && isViHoisted(init)) /* const v = vi.hoisted() */ - || (init - && init.type === 'AwaitExpression' - && init.argument.type === 'CallExpression' - && isViHoisted(init.argument)) /* const v = await vi.hoisted() */ - - if (canMoveDeclaration) { - // hoist "const variable = vi.hoisted(() => {})" - hoistedCode += `${code.slice(declarationNode.start, declarationNode.end)}\n` - s.remove(declarationNode.start, declarationNode.end) - } - else { - // hoist "vi.hoisted(() => {})" - hoistedCode += `${code.slice(node.start, node.end)}\n` - s.remove(node.start, node.end) - } - } - } - } - - // if we don't need to inject anything, skip the walking - if (hijackEsm) { - // 3. convert references to import bindings & import.meta references - esmWalker(ast, { - onCallExpression: CallExpression, - onIdentifier(id, parent, parentStack) { - const grandparent = parentStack[1] - const binding = idToImportMap.get(id.name) - if (!binding) - return - - if (isStaticProperty(parent) && parent.shorthand) { - // let binding used in a property shorthand - // { foo } -> { foo: __import_x__.foo } - // skip for destructuring patterns - if ( - !isNodeInPattern(parent) + // 3. convert references to import bindings & import.meta references + esmWalker(ast, { + onIdentifier(id, parent, parentStack) { + const grandparent = parentStack[1] + const binding = idToImportMap.get(id.name) + if (!binding) + return + + if (isStaticProperty(parent) && parent.shorthand) { + // let binding used in a property shorthand + // { foo } -> { foo: __import_x__.foo } + // skip for destructuring patterns + if ( + !isNodeInPattern(parent) || isInDestructuringAssignment(parent, parentStack) - ) - s.appendLeft(id.end, `: ${binding}`) - } - else if ( - (parent.type === 'PropertyDefinition' + ) + s.appendLeft(id.end, `: ${binding}`) + } + else if ( + (parent.type === 'PropertyDefinition' && grandparent?.type === 'ClassBody') || (parent.type === 'ClassDeclaration' && id === parent.superClass) - ) { - if (!declaredConst.has(id.name)) { - declaredConst.add(id.name) - // locate the top-most node containing the class declaration - const topNode = parentStack[parentStack.length - 2] - s.prependRight(topNode.start, `const ${id.name} = ${binding};\n`) - } - } - else { - s.update(id.start, id.end, binding) + ) { + if (!declaredConst.has(id.name)) { + declaredConst.add(id.name) + // locate the top-most node containing the class declaration + const topNode = parentStack[parentStack.length - 2] + s.prependRight(topNode.start, `const ${id.name} = ${binding};\n`) } - }, - // TODO: make env updatable - onImportMeta() { - // s.update(node.start, node.end, viImportMetaKey) - }, - onDynamicImport(node) { - const replace = '__vi_wrap_module__(import(' - s.overwrite(node.start, (node.source as Positioned).start, replace) - s.overwrite(node.end - 1, node.end, '))') - }, - }) - } - // we still need to hoist "vi" helper - else { - simpleWalk(ast, { - CallExpression: CallExpression as any, - }) - } - - if (hoistedCode || hoistedVitestImports) { - s.prepend( - hoistedVitestImports - + ((!hoistedVitestImports && hoistedCode) ? API_NOT_FOUND_CHECK : '') - + hoistedCode, - ) - } + } + else { + s.update(id.start, id.end, binding) + } + }, + // TODO: make env updatable + onImportMeta() { + // s.update(node.start, node.end, viImportMetaKey) + }, + onDynamicImport(node) { + const replace = '__vi_wrap_module__(import(' + s.overwrite(node.start, (node.source as Positioned).start, replace) + s.overwrite(node.end - 1, node.end, '))') + }, + }) if (hasInjected) { // make sure "__vi_injected__" is declared as soon as possible diff --git a/packages/vitest/src/node/esmWalker.ts b/packages/browser/src/node/esmWalker.ts similarity index 95% rename from packages/vitest/src/node/esmWalker.ts rename to packages/browser/src/node/esmWalker.ts index 6fc890359503..01e54ad1eea5 100644 --- a/packages/vitest/src/node/esmWalker.ts +++ b/packages/browser/src/node/esmWalker.ts @@ -1,5 +1,4 @@ import type { - CallExpression, Function as FunctionNode, Identifier, ImportExpression, @@ -23,7 +22,6 @@ interface Visitors { parent: Node, parentStack: Node[], ) => void - onCallExpression: (node: Positioned) => void onImportMeta: (node: Node) => void onDynamicImport: (node: Positioned) => void } @@ -42,7 +40,7 @@ export function isNodeInPattern(node: _Node): node is Property { */ export function esmWalker( root: Node, - { onIdentifier, onImportMeta, onDynamicImport, onCallExpression }: Visitors, + { onIdentifier, onImportMeta, onDynamicImport }: Visitors, ) { const parentStack: Node[] = [] const varKindStack: VariableDeclaration['kind'][] = [] @@ -117,9 +115,6 @@ export function esmWalker( else if (node.type === 'ImportExpression') onDynamicImport(node) - else if (node.type === 'CallExpression') - onCallExpression(node) - if (node.type === 'Identifier') { if ( !isInScope(node.name, parentStack) @@ -280,14 +275,16 @@ export function isFunctionNode(node: _Node): node is FunctionNode { return functionNodeTypeRE.test(node.type) } +const blockNodeTypeRE = /^BlockStatement$|^For(?:In|Of)?Statement$/ +function isBlock(node: _Node) { + return blockNodeTypeRE.test(node.type) +} + function findParentScope( parentStack: _Node[], isVar = false, ): _Node | undefined { - const predicate = isVar - ? isFunctionNode - : (node: _Node) => node.type === 'BlockStatement' - return parentStack.find(predicate) + return parentStack.find(isVar ? isFunctionNode : isBlock) } export function isInDestructuringAssignment( diff --git a/packages/browser/src/node/index.ts b/packages/browser/src/node/index.ts index d582f8908e0e..f820dcf93aa7 100644 --- a/packages/browser/src/node/index.ts +++ b/packages/browser/src/node/index.ts @@ -5,12 +5,14 @@ import { builtinModules } from 'node:module' import { polyfillPath } from 'modern-node-polyfills' import sirv from 'sirv' import type { Plugin } from 'vite' +import { injectVitestModule } from './esmInjector' const polyfills = [ 'util', ] -export default (base = '/'): Plugin[] => { +// don't expose type to not bundle it here +export default (project: any, base = '/'): Plugin[] => { const pkgRoot = resolve(fileURLToPath(import.meta.url), '../..') const distRoot = resolve(pkgRoot, 'dist') @@ -51,6 +53,16 @@ export default (base = '/'): Plugin[] => { return { id: await polyfillPath(id), moduleSideEffects: false } }, }, + { + name: 'vitest:browser:esm-injector', + enforce: 'post', + transform(source, id) { + return injectVitestModule(source, id, this.parse, { + hijackESM: project.config.browser.slowHijackESM ?? false, + cacheDir: project.server.config.cacheDir, + }) + }, + }, ] } diff --git a/packages/vitest/package.json b/packages/vitest/package.json index 0eafc11462b2..3ba792543692 100644 --- a/packages/vitest/package.json +++ b/packages/vitest/package.json @@ -148,7 +148,6 @@ "chai": "^4.3.7", "concordance": "^5.0.4", "debug": "^4.3.4", - "estree-walker": "^3.0.3", "local-pkg": "^0.4.3", "magic-string": "^0.30.0", "pathe": "^1.1.0", @@ -191,7 +190,6 @@ "micromatch": "^4.0.5", "mlly": "^1.2.0", "p-limit": "^4.0.0", - "periscopic": "^3.1.0", "pkg-types": "^1.0.2", "playwright": "^1.32.2", "pretty-format": "^27.5.1", diff --git a/packages/vitest/src/integrations/browser/server.ts b/packages/vitest/src/integrations/browser/server.ts index 0950f062a0bf..7dd2ac36b431 100644 --- a/packages/vitest/src/integrations/browser/server.ts +++ b/packages/vitest/src/integrations/browser/server.ts @@ -7,7 +7,7 @@ import { ensurePackageInstalled } from '../../node/pkg' import { resolveApiServerConfig } from '../../node/config' import { CoverageTransform } from '../../node/plugins/coverageTransform' import type { WorkspaceProject } from '../../node/workspace' -import { ESMTransformPlugin } from '../../node/plugins/esmTransform' +import { MocksPlugin } from '../../node/plugins/mocks' export async function createBrowserServer(project: WorkspaceProject, options: UserConfig) { const root = project.config.root @@ -32,9 +32,8 @@ export async function createBrowserServer(project: WorkspaceProject, options: Us }, }, plugins: [ - (await import('@vitest/browser')).default('/'), + (await import('@vitest/browser')).default(project, '/'), CoverageTransform(project.ctx), - ESMTransformPlugin(project), { enforce: 'post', name: 'vitest:browser:config', @@ -54,6 +53,7 @@ export async function createBrowserServer(project: WorkspaceProject, options: Us } }, }, + MocksPlugin(), ], }) diff --git a/packages/vitest/src/node/config.ts b/packages/vitest/src/node/config.ts index f3f99f874666..ae3cfe462329 100644 --- a/packages/vitest/src/node/config.ts +++ b/packages/vitest/src/node/config.ts @@ -272,14 +272,12 @@ export function resolveConfig( resolved.browser ??= {} as any resolved.browser.enabled ??= false resolved.browser.headless ??= isCI + resolved.browser.slowHijackESM ??= true resolved.browser.api = resolveApiServerConfig(resolved.browser) || { port: defaultBrowserPort, } - if (resolved.browser.enabled) - resolved.slowHijackESM ??= true - return resolved } diff --git a/packages/vitest/src/node/hoistMocks.ts b/packages/vitest/src/node/hoistMocks.ts new file mode 100644 index 000000000000..a3d9a2aed391 --- /dev/null +++ b/packages/vitest/src/node/hoistMocks.ts @@ -0,0 +1,184 @@ +import MagicString from 'magic-string' +import type { CallExpression, Identifier, ImportDeclaration, VariableDeclaration, Node as _Node } from 'estree' +import { findNodeAround, simple as simpleWalk } from 'acorn-walk' +import type { AcornNode } from 'rollup' + +export type Positioned = T & { + start: number + end: number +} + +export type Node = Positioned<_Node> + +const API_NOT_FOUND_ERROR = `There are some problems in resolving the mocks API. +You may encounter this issue when importing the mocks API from another module other than 'vitest'. +To fix this issue you can either: +- import the mocks API directly from 'vitest' +- enable the 'globals' options` + +const API_NOT_FOUND_CHECK = '\nif (typeof globalThis.vi === "undefined" && typeof globalThis.vitest === "undefined") ' ++ `{ throw new Error(${JSON.stringify(API_NOT_FOUND_ERROR)}) }\n` + +function isIdentifier(node: any): node is Positioned { + return node.type === 'Identifier' +} + +function transformImportSpecifiers(node: ImportDeclaration) { + const specifiers = node.specifiers + + if (specifiers.length === 1 && specifiers[0].type === 'ImportNamespaceSpecifier') + return specifiers[0].local.name + + const dynamicImports = node.specifiers.map((specifier) => { + if (specifier.type === 'ImportDefaultSpecifier') + return `default: ${specifier.local.name}` + + if (specifier.type === 'ImportSpecifier') { + const local = specifier.local.name + const imported = specifier.imported.name + if (local === imported) + return local + return `${imported}: ${local}` + } + + return null + }).filter(Boolean).join(', ') + + if (!dynamicImports.length) + return '' + + return `{ ${dynamicImports} }` +} + +const regexpHoistable = /^[ \t]*\b(vi|vitest)\s*\.\s*(mock|unmock|hoisted)\(/m +const hashbangRE = /^#!.*\n/ + +export function hoistMocks(code: string, id: string, parse: (code: string, options: any) => AcornNode) { + const hasMocks = regexpHoistable.test(code) + + if (!hasMocks) + return + + const s = new MagicString(code) + + let ast: any + try { + ast = parse(code, { + sourceType: 'module', + ecmaVersion: 'latest', + locations: true, + }) + } + catch (err) { + console.error(`Cannot parse ${id}:\n${(err as any).message}`) + return + } + + const hoistIndex = code.match(hashbangRE)?.[0].length ?? 0 + + let hoistedCode = '' + let hoistedVitestImports = '' + + // this will tranfrom import statements into dynamic ones, if there are imports + // it will keep the import as is, if we don't need to mock anything + // in browser environment it will wrap the module value with "vitest_wrap_module" function + // that returns a proxy to the module so that named exports can be mocked + const transformImportDeclaration = (node: ImportDeclaration) => { + const source = node.source.value as string + + // if we don't hijack ESM and process this file, then we definetly have mocks, + // so we need to transform imports into dynamic ones, so "vi.mock" can be executed before + const specifiers = transformImportSpecifiers(node) + const code = specifiers + ? `const ${specifiers} = await import('${source}')\n` + : `await import('${source}')\n` + return code + } + + function hoistImport(node: Positioned) { + // always hoist vitest import to top of the file, so + // "vi" helpers can access it + s.remove(node.start, node.end) + + if (node.source.value === 'vitest') { + const code = `const ${transformImportSpecifiers(node)} = await import('vitest')\n` + hoistedVitestImports += code + return + } + const code = transformImportDeclaration(node) + s.appendLeft(hoistIndex, code) + } + + // 1. check all import statements and record id -> importName map + for (const node of ast.body as Node[]) { + // import foo from 'foo' --> foo -> __import_foo__.default + // import { baz } from 'foo' --> baz -> __import_foo__.baz + // import * as ok from 'foo' --> ok -> __import_foo__ + if (node.type === 'ImportDeclaration') + hoistImport(node) + } + + simpleWalk(ast, { + CallExpression(_node) { + const node = _node as any as Positioned + if ( + node.callee.type === 'MemberExpression' + && isIdentifier(node.callee.object) + && (node.callee.object.name === 'vi' || node.callee.object.name === 'vitest') + && isIdentifier(node.callee.property) + ) { + const methodName = node.callee.property.name + + if (methodName === 'mock' || methodName === 'unmock') { + hoistedCode += `${code.slice(node.start, node.end)}\n` + s.remove(node.start, node.end) + } + + if (methodName === 'hoisted') { + const declarationNode = findNodeAround(ast, node.start, 'VariableDeclaration')?.node as Positioned | undefined + const init = declarationNode?.declarations[0]?.init + const isViHoisted = (node: CallExpression) => { + return node.callee.type === 'MemberExpression' + && isIdentifier(node.callee.object) + && (node.callee.object.name === 'vi' || node.callee.object.name === 'vitest') + && isIdentifier(node.callee.property) + && node.callee.property.name === 'hoisted' + } + + const canMoveDeclaration = (init + && init.type === 'CallExpression' + && isViHoisted(init)) /* const v = vi.hoisted() */ + || (init + && init.type === 'AwaitExpression' + && init.argument.type === 'CallExpression' + && isViHoisted(init.argument)) /* const v = await vi.hoisted() */ + + if (canMoveDeclaration) { + // hoist "const variable = vi.hoisted(() => {})" + hoistedCode += `${code.slice(declarationNode.start, declarationNode.end)}\n` + s.remove(declarationNode.start, declarationNode.end) + } + else { + // hoist "vi.hoisted(() => {})" + hoistedCode += `${code.slice(node.start, node.end)}\n` + s.remove(node.start, node.end) + } + } + } + }, + }) + + if (hoistedCode || hoistedVitestImports) { + s.prepend( + hoistedVitestImports + + ((!hoistedVitestImports && hoistedCode) ? API_NOT_FOUND_CHECK : '') + + hoistedCode, + ) + } + + return { + ast, + code: s.toString(), + map: s.generateMap({ hires: true, source: id }), + } +} diff --git a/packages/vitest/src/node/plugins/esmTransform.ts b/packages/vitest/src/node/plugins/esmTransform.ts deleted file mode 100644 index 8d72176910a6..000000000000 --- a/packages/vitest/src/node/plugins/esmTransform.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Plugin } from 'vite' -import { injectVitestModule } from '../esmInjector' -import type { Vitest } from '../core' -import type { WorkspaceProject } from '../workspace' - -export function ESMTransformPlugin(ctx: WorkspaceProject | Vitest): Plugin { - return { - name: 'vitest:mocker-plugin', - enforce: 'post', - transform(source, id) { - return injectVitestModule(source, id, (code, options) => this.parse(code, options), { - hijackESM: (ctx.config.browser.enabled && ctx.config.slowHijackESM) ?? false, - cacheDir: ctx.server.config.cacheDir, - }) - }, - } -} diff --git a/packages/vitest/src/node/plugins/index.ts b/packages/vitest/src/node/plugins/index.ts index 2d8b41ce3244..effb776c795e 100644 --- a/packages/vitest/src/node/plugins/index.ts +++ b/packages/vitest/src/node/plugins/index.ts @@ -11,7 +11,7 @@ import { EnvReplacerPlugin } from './envReplacer' import { GlobalSetupPlugin } from './globalSetup' import { CSSEnablerPlugin } from './cssEnabler' import { CoverageTransform } from './coverageTransform' -import { ESMTransformPlugin } from './esmTransform' +import { MocksPlugin } from './mocks' export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest('test')): Promise { const userConfig = deepMerge({}, options) as UserConfig @@ -243,7 +243,7 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest('t options.ui ? await UIPlugin() : null, - ESMTransformPlugin(ctx), + MocksPlugin(), ] .filter(notNullish) } diff --git a/packages/vitest/src/node/plugins/mocks.ts b/packages/vitest/src/node/plugins/mocks.ts new file mode 100644 index 000000000000..9628fe908606 --- /dev/null +++ b/packages/vitest/src/node/plugins/mocks.ts @@ -0,0 +1,12 @@ +import type { Plugin } from 'vite' +import { hoistMocks } from '../hoistMocks' + +export function MocksPlugin(): Plugin { + return { + name: 'vite:mocks', + enforce: 'post', + transform(code, id) { + return hoistMocks(code, id, this.parse) + }, + } +} diff --git a/packages/vitest/src/node/plugins/workspace.ts b/packages/vitest/src/node/plugins/workspace.ts index 29f5b1b32afd..bef905d8a90c 100644 --- a/packages/vitest/src/node/plugins/workspace.ts +++ b/packages/vitest/src/node/plugins/workspace.ts @@ -9,7 +9,7 @@ import { CoverageTransform } from './coverageTransform' import { CSSEnablerPlugin } from './cssEnabler' import { EnvReplacerPlugin } from './envReplacer' import { GlobalSetupPlugin } from './globalSetup' -import { ESMTransformPlugin } from './esmTransform' +import { MocksPlugin } from './mocks' interface WorkspaceOptions extends UserWorkspaceConfig { root?: string @@ -139,6 +139,6 @@ export function WorkspaceVitestPlugin(project: WorkspaceProject, options: Worksp ...CSSEnablerPlugin(project), CoverageTransform(project.ctx), GlobalSetupPlugin(project, project.ctx.logger), - ESMTransformPlugin(project), + MocksPlugin(), ] } diff --git a/packages/vitest/src/runtime/mocker.ts b/packages/vitest/src/runtime/mocker.ts index 0500a9026162..a3f617113b50 100644 --- a/packages/vitest/src/runtime/mocker.ts +++ b/packages/vitest/src/runtime/mocker.ts @@ -378,9 +378,4 @@ export class VitestMocker { public queueUnmock(id: string, importer: string) { VitestMocker.pendingIds.push({ type: 'unmock', id, importer }) } - - public async prepare() { - if (VitestMocker.pendingIds.length) - await this.resolveMocks() - } } diff --git a/packages/vitest/src/types/browser.ts b/packages/vitest/src/types/browser.ts index 2f1ede9b2ec2..9bfd32e464f6 100644 --- a/packages/vitest/src/types/browser.ts +++ b/packages/vitest/src/types/browser.ts @@ -51,6 +51,15 @@ export interface BrowserConfigOptions { * The default port is 63315. */ api?: ApiConfig | number + + /** + * Update ESM imports so they can be spied/stubbed with vi.spyOn. + * Enabled by default when running in browser. + * + * @default true + * @experimental + */ + slowHijackESM?: boolean } export interface ResolvedBrowserOptions extends BrowserConfigOptions { diff --git a/packages/vitest/src/types/config.ts b/packages/vitest/src/types/config.ts index 4493586825dd..54ec5e84e20f 100644 --- a/packages/vitest/src/types/config.ts +++ b/packages/vitest/src/types/config.ts @@ -424,15 +424,6 @@ export interface InlineConfig { */ uiBase?: string - /** - * Update ESM imports so they can be spied/stubbed with vi.spyOn. - * Enabled by default when running in browser. - * - * @default false - * @experimental - */ - slowHijackESM?: boolean - /** * Determine the transform method of modules */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c547a1e9bf61..4d0c985fbeac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -855,6 +855,9 @@ importers: specifier: ^2.0.2 version: 2.0.2 devDependencies: + '@types/estree': + specifier: ^1.0.1 + version: 1.0.1 '@types/ws': specifier: ^8.5.4 version: 8.5.4 @@ -867,6 +870,12 @@ importers: '@vitest/ws-client': specifier: workspace:* version: link:../ws-client + estree-walker: + specifier: ^3.0.3 + version: 3.0.3 + periscopic: + specifier: ^3.1.0 + version: 3.1.0 rollup: specifier: 3.20.2 version: 3.20.2 @@ -1203,9 +1212,6 @@ importers: debug: specifier: ^4.3.4 version: 4.3.4(supports-color@8.1.1) - estree-walker: - specifier: ^3.0.3 - version: 3.0.3 local-pkg: specifier: ^0.4.3 version: 0.4.3 @@ -1327,9 +1333,6 @@ importers: p-limit: specifier: ^4.0.0 version: 4.0.0 - periscopic: - specifier: ^3.1.0 - version: 3.1.0 pkg-types: specifier: ^1.0.2 version: 1.0.2 @@ -8214,12 +8217,13 @@ packages: /@types/estree@1.0.1: resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} + dev: true /@types/fs-extra@11.0.1: resolution: {integrity: sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA==} dependencies: '@types/jsonfile': 6.1.1 - '@types/node': 18.16.0 + '@types/node': 18.16.1 dev: true /@types/fs-extra@9.0.13: @@ -8375,10 +8379,6 @@ packages: /@types/node@18.15.11: resolution: {integrity: sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==} - /@types/node@18.16.0: - resolution: {integrity: sha512-BsAaKhB+7X+H4GnSjGhJG9Qi8Tw+inU9nJDwmD5CgOmBLEI6ArdhikpLX7DjbjDRDTbqZzU2LSQNZg8WGPiSZQ==} - dev: true - /@types/node@18.16.1: resolution: {integrity: sha512-DZxSZWXxFfOlx7k7Rv4LAyiMroaxa3Ly/7OOzZO8cBNho0YzAi4qlbrx8W27JGqG57IgR/6J7r+nOJWw6kcvZA==} dev: true @@ -8600,7 +8600,7 @@ packages: /@types/ws@8.5.4: resolution: {integrity: sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==} dependencies: - '@types/node': 18.16.0 + '@types/node': 18.16.1 dev: true /@types/yargs-parser@21.0.0: @@ -14238,6 +14238,7 @@ packages: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} dependencies: '@types/estree': 1.0.1 + dev: true /esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} diff --git a/test/core/test/injector-esm.test.ts b/test/core/test/injector-esm.test.ts index 20d62f23a155..7d1fc5330426 100644 --- a/test/core/test/injector-esm.test.ts +++ b/test/core/test/injector-esm.test.ts @@ -1,5 +1,5 @@ import { Parser } from 'acorn' -import { injectVitestModule } from 'vitest/src/node/esmInjector' +import { injectVitestModule } from '@vitest/browser/src/node/esmInjector' import { expect, test } from 'vitest' import { transformWithEsbuild } from 'vite' @@ -756,6 +756,35 @@ export default (function getRandom() { `) }) +test('track scope in for loops', async () => { + expect( + injectSimpleCode(` +import { test } from './test.js' +for (const test of tests) { + console.log(test) +} +for (let test = 0; test < 10; test++) { + console.log(test) +} +for (const test in tests) { + console.log(test) +}`), + ).toMatchInlineSnapshot(` + "import { __vi_inject__ as __vi_esm_0__ } from './test.js' + + + for (const test of tests) { + console.log(test) + } + for (let test = 0; test < 10; test++) { + console.log(test) + } + for (const test in tests) { + console.log(test) + }" + `) +}) + // #8002 // test('with hashbang', async () => { // expect( diff --git a/test/core/test/injector-mock.test.ts b/test/core/test/injector-mock.test.ts index fb4bcc519c70..2ee3fb32a4e0 100644 --- a/test/core/test/injector-mock.test.ts +++ b/test/core/test/injector-mock.test.ts @@ -1,27 +1,17 @@ import { Parser } from 'acorn' -import { injectVitestModule } from 'vitest/src/node/esmInjector' +import { hoistMocks } from 'vitest/src/node/hoistMocks' import { expect, test } from 'vitest' function parse(code: string, options: any) { return Parser.parse(code, options) } -function injectSimpleCode(code: string) { - return injectVitestModule(code, '/test.js', parse, { - hijackESM: false, - cacheDir: '/tmp', - })?.code.trim() -} - -function injectHijackedCode(code: string) { - return injectVitestModule(code, '/test.js', parse, { - hijackESM: true, - cacheDir: '/tmp', - })?.code.trim() +function hoistSimpleCode(code: string) { + return hoistMocks(code, '/test.js', parse)?.code.trim() } test('hoists mock, unmock, hoisted', () => { - expect(injectSimpleCode(` + expect(hoistSimpleCode(` vi.mock('path', () => {}) vi.unmock('path') vi.hoisted(() => {}) @@ -34,33 +24,15 @@ test('hoists mock, unmock, hoisted', () => { }) test('always hoists import from vitest', () => { - expect(injectSimpleCode(` - import { vi } from 'vitest' - vi.mock('path', () => {}) - vi.unmock('path') - vi.hoisted(() => {}) - import { test } from 'vitest' - `)).toMatchInlineSnapshot(` - "import { vi } from 'vitest' - import { test } from 'vitest' - vi.mock('path', () => {}) - vi.unmock('path') - vi.hoisted(() => {})" - `) -}) - -test('always hoists mock, unmock, hoisted when modules are hijacked', () => { - expect(injectHijackedCode(` + expect(hoistSimpleCode(` import { vi } from 'vitest' vi.mock('path', () => {}) vi.unmock('path') vi.hoisted(() => {}) import { test } from 'vitest' `)).toMatchInlineSnapshot(` - "import { __vi_inject__ as __vi_esm_0__ } from 'vitest' - const { vi } = __vi_esm_0__; - import { __vi_inject__ as __vi_esm_1__ } from 'vitest' - const { test } = __vi_esm_1__; + "const { vi } = await import('vitest') + const { test } = await import('vitest') vi.mock('path', () => {}) vi.unmock('path') vi.hoisted(() => {})" @@ -68,7 +40,7 @@ test('always hoists mock, unmock, hoisted when modules are hijacked', () => { }) test('always hoists all imports but they are under mocks', () => { - expect(injectSimpleCode(` + expect(hoistSimpleCode(` import { vi } from 'vitest' import { someValue } from './path.js' import { someValue2 } from './path2.js' @@ -77,8 +49,8 @@ test('always hoists all imports but they are under mocks', () => { vi.hoisted(() => {}) import { test } from 'vitest' `)).toMatchInlineSnapshot(` - "import { vi } from 'vitest' - import { test } from 'vitest' + "const { vi } = await import('vitest') + const { test } = await import('vitest') vi.mock('path', () => {}) vi.unmock('path') vi.hoisted(() => {}) @@ -86,25 +58,3 @@ test('always hoists all imports but they are under mocks', () => { const { someValue2 } = await import('./path2.js')" `) }) - -test('always hoists all imports but they are under mocks when modules are hijacked', () => { - expect(injectHijackedCode(` - import { vi } from 'vitest' - import { someValue } from './path.js' - import { someValue2 } from './path2.js' - vi.mock('path', () => {}) - vi.unmock('path') - vi.hoisted(() => {}) - import { test } from 'vitest' - `)).toMatchInlineSnapshot(` - "import { __vi_inject__ as __vi_esm_0__ } from 'vitest' - const { vi } = __vi_esm_0__; - import { __vi_inject__ as __vi_esm_3__ } from 'vitest' - const { test } = __vi_esm_3__; - vi.mock('path', () => {}) - vi.unmock('path') - vi.hoisted(() => {}) - const { __vi_inject__: __vi_esm_1__ } = await __vi_wrap_module__(import('./path.js')) - const { __vi_inject__: __vi_esm_2__ } = await __vi_wrap_module__(import('./path2.js'))" - `) -}) From 948530cb231abcb0cc3bd26034a603fba0f73821 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 27 Apr 2023 13:57:42 +0200 Subject: [PATCH 19/22] chore: update lockfile --- pnpm-lock.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4d0c985fbeac..b34742300b45 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1412,7 +1412,7 @@ importers: version: link:../../packages/vitest webdriverio: specifier: latest - version: 8.7.0(typescript@5.0.3) + version: 8.8.0(typescript@5.0.3) test/base: devDependencies: From b3932b7140888be2203620903c678cedef0c4560 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 27 Apr 2023 14:01:40 +0200 Subject: [PATCH 20/22] chore: fix slowHijack docs --- docs/config/index.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/config/index.md b/docs/config/index.md index c6099958ea49..0804ca138bc5 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -1026,10 +1026,7 @@ export interface BrowserProvider { This is an advanced API for library authors. If you just need to run tests in a browser, use the [browser](/config/#browser) option. ::: -### browser.slowHijackESM - - -#### slowHijackESM +#### browser.slowHijackESM - **Type:** `boolean` - **Default:** `true` From b295ccdaaf3c9d075db04bd1c0d405433bf4f738 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 27 Apr 2023 14:05:19 +0200 Subject: [PATCH 21/22] chore: cleanup --- packages/browser/src/node/esmInjector.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/browser/src/node/esmInjector.ts b/packages/browser/src/node/esmInjector.ts index 54e6d64c2954..c3d8ea1c9071 100644 --- a/packages/browser/src/node/esmInjector.ts +++ b/packages/browser/src/node/esmInjector.ts @@ -23,15 +23,10 @@ interface Options { // this is basically copypaste from Vite SSR // this method transforms all import and export statements into `__vi_injected__` variable // to allow spying on them. this can be disabled by setting `slowHijackESM` to `false` -// to not parse the module twice, we reuse the ast to hoist vi.mock here -// and transform imports into dynamic ones if vi.mock is present export function injectVitestModule(code: string, id: string, parse: (code: string, options: any) => AcornNode, options: Options) { - if (skipHijack.some(skip => id.match(skip))) - return - const hijackEsm = options.hijackESM ?? false - if (!hijackEsm) + if (!hijackEsm || skipHijack.some(skip => id.match(skip))) return const s = new MagicString(code) From 1da66d2d159e7d1ae935ba09762064eff211d5be Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 27 Apr 2023 14:52:27 +0200 Subject: [PATCH 22/22] chore: remove "hijackESM" option from the plugin --- packages/browser/src/node/esmInjector.ts | 5 +---- packages/browser/src/node/index.ts | 4 +++- test/core/test/injector-esm.test.ts | 1 - 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/browser/src/node/esmInjector.ts b/packages/browser/src/node/esmInjector.ts index c3d8ea1c9071..4aa977c089a7 100644 --- a/packages/browser/src/node/esmInjector.ts +++ b/packages/browser/src/node/esmInjector.ts @@ -16,7 +16,6 @@ const skipHijack = [ ] interface Options { - hijackESM?: boolean cacheDir: string } @@ -24,9 +23,7 @@ interface Options { // this method transforms all import and export statements into `__vi_injected__` variable // to allow spying on them. this can be disabled by setting `slowHijackESM` to `false` export function injectVitestModule(code: string, id: string, parse: (code: string, options: any) => AcornNode, options: Options) { - const hijackEsm = options.hijackESM ?? false - - if (!hijackEsm || skipHijack.some(skip => id.match(skip))) + if (skipHijack.some(skip => id.match(skip))) return const s = new MagicString(code) diff --git a/packages/browser/src/node/index.ts b/packages/browser/src/node/index.ts index f820dcf93aa7..541b1758d67e 100644 --- a/packages/browser/src/node/index.ts +++ b/packages/browser/src/node/index.ts @@ -57,8 +57,10 @@ export default (project: any, base = '/'): Plugin[] => { name: 'vitest:browser:esm-injector', enforce: 'post', transform(source, id) { + const hijackESM = project.config.browser.slowHijackESM ?? false + if (!hijackESM) + return return injectVitestModule(source, id, this.parse, { - hijackESM: project.config.browser.slowHijackESM ?? false, cacheDir: project.server.config.cacheDir, }) }, diff --git a/test/core/test/injector-esm.test.ts b/test/core/test/injector-esm.test.ts index 7d1fc5330426..e08ac7cac236 100644 --- a/test/core/test/injector-esm.test.ts +++ b/test/core/test/injector-esm.test.ts @@ -9,7 +9,6 @@ function parse(code: string, options: any) { function injectSimpleCode(code: string) { return injectVitestModule(code, '/test.js', parse, { - hijackESM: true, cacheDir: '/tmp', })?.code }