diff --git a/packages/mocker/package.json b/packages/mocker/package.json index bce580677..1bac213f4 100644 --- a/packages/mocker/package.json +++ b/packages/mocker/package.json @@ -32,6 +32,10 @@ "types": "./dist/redirect.d.ts", "default": "./dist/redirect.js" }, + "./automock": { + "types": "./dist/automock.d.ts", + "default": "./dist/automock.js" + }, "./register": { "types": "./dist/register.d.ts", "default": "./dist/register.js" diff --git a/packages/mocker/rollup.config.js b/packages/mocker/rollup.config.js index e4269d560..034c5c152 100644 --- a/packages/mocker/rollup.config.js +++ b/packages/mocker/rollup.config.js @@ -13,6 +13,7 @@ const entries = { 'index': 'src/index.ts', 'node': 'src/node/index.ts', 'redirect': 'src/node/redirect.ts', + 'automock': 'src/node/automock.ts', 'browser': 'src/browser/index.ts', 'register': 'src/browser/register.ts', 'auto-register': 'src/browser/auto-register.ts', diff --git a/packages/mocker/src/node/automock.ts b/packages/mocker/src/node/automock.ts new file mode 100644 index 000000000..8d0d79c95 --- /dev/null +++ b/packages/mocker/src/node/automock.ts @@ -0,0 +1,171 @@ +import type { Declaration, ExportDefaultDeclaration, ExportNamedDeclaration, Expression, Pattern, Positioned, Program } from './esmWalker' +import MagicString from 'magic-string' +import { + getArbitraryModuleIdentifier, +} from './esmWalker' + +export interface AutomockOptions { + /** + * @default "__vitest_mocker__" + */ + globalThisAccessor?: string +} + +// TODO: better source map replacement +export function automockModule( + code: string, + mockType: 'automock' | 'autospy', + parse: (code: string) => any, + options: AutomockOptions = {}, +): MagicString { + const globalThisAccessor = options.globalThisAccessor || '"__vitest_mocker__"' + const ast = parse(code) as Program + + const m = new MagicString(code) + + const allSpecifiers: { name: string; alias?: string }[] = [] + let importIndex = 0 + for (const _node of ast.body) { + if (_node.type === 'ExportAllDeclaration') { + throw new Error( + `automocking files with \`export *\` is not supported in browser mode because it cannot be statically analysed`, + ) + } + + if (_node.type === 'ExportNamedDeclaration') { + const node = _node as Positioned + const declaration = node.declaration // export const name + + function traversePattern(expression: Pattern) { + // export const test = '1' + if (expression.type === 'Identifier') { + allSpecifiers.push({ name: expression.name }) + } + // export const [test, ...rest] = [1, 2, 3] + else if (expression.type === 'ArrayPattern') { + expression.elements.forEach((element) => { + if (!element) { + return + } + traversePattern(element) + }) + } + else if (expression.type === 'ObjectPattern') { + expression.properties.forEach((property) => { + // export const { ...rest } = {} + if (property.type === 'RestElement') { + traversePattern(property) + } + // export const { test, test2: alias } = {} + else if (property.type === 'Property') { + traversePattern(property.value) + } + else { + property satisfies never + } + }) + } + else if (expression.type === 'RestElement') { + traversePattern(expression.argument) + } + // const [name[1], name[2]] = [] + // cannot be used in export + else if (expression.type === 'AssignmentPattern') { + throw new Error( + `AssignmentPattern is not supported. Please open a new bug report.`, + ) + } + // const test = thing.func() + // cannot be used in export + else if (expression.type === 'MemberExpression') { + throw new Error( + `MemberExpression is not supported. Please open a new bug report.`, + ) + } + else { + expression satisfies never + } + } + + if (declaration) { + if (declaration.type === 'FunctionDeclaration') { + allSpecifiers.push({ name: declaration.id.name }) + } + else if (declaration.type === 'VariableDeclaration') { + declaration.declarations.forEach((declaration) => { + traversePattern(declaration.id) + }) + } + else if (declaration.type === 'ClassDeclaration') { + allSpecifiers.push({ name: declaration.id.name }) + } + else { + declaration satisfies never + } + m.remove(node.start, (declaration as Positioned).start) + } + + const specifiers = node.specifiers || [] + const source = node.source + + if (!source && specifiers.length) { + specifiers.forEach((specifier) => { + allSpecifiers.push({ + alias: getArbitraryModuleIdentifier(specifier.exported), + name: getArbitraryModuleIdentifier(specifier.local), + }) + }) + m.remove(node.start, node.end) + } + else if (source && specifiers.length) { + const importNames: [string, string][] = [] + + specifiers.forEach((specifier) => { + const importedName = `__vitest_imported_${importIndex++}__` + importNames.push([getArbitraryModuleIdentifier(specifier.local), importedName]) + allSpecifiers.push({ + name: importedName, + alias: getArbitraryModuleIdentifier(specifier.exported), + }) + }) + + const importString = `import { ${importNames + .map(([name, alias]) => `${name} as ${alias}`) + .join(', ')} } from '${source.value}'` + + m.overwrite(node.start, node.end, importString) + } + } + if (_node.type === 'ExportDefaultDeclaration') { + const node = _node as Positioned + const declaration = node.declaration as Positioned + allSpecifiers.push({ name: '__vitest_default', alias: 'default' }) + m.overwrite(node.start, declaration.start, `const __vitest_default = `) + } + } + const moduleObject = ` +const __vitest_current_es_module__ = { + __esModule: true, + ${allSpecifiers.map(({ name }) => `["${name}"]: ${name},`).join('\n ')} +} +const __vitest_mocked_module__ = globalThis[${globalThisAccessor}].mockObject(__vitest_current_es_module__, "${mockType}") +` + const assigning = allSpecifiers + .map(({ name }, index) => { + return `const __vitest_mocked_${index}__ = __vitest_mocked_module__["${name}"]` + }) + .join('\n') + + const redeclarations = allSpecifiers + .map(({ name, alias }, index) => { + return ` __vitest_mocked_${index}__ as ${alias || name},` + }) + .join('\n') + const specifiersExports = ` +export { +${redeclarations} +} +` + m.append(moduleObject + assigning + specifiersExports) + return m +} diff --git a/packages/mocker/src/node/automockPlugin.ts b/packages/mocker/src/node/automockPlugin.ts index 608b28e88..8c746d5f0 100644 --- a/packages/mocker/src/node/automockPlugin.ts +++ b/packages/mocker/src/node/automockPlugin.ts @@ -1,21 +1,11 @@ import type { Plugin } from 'vite' -import type { Declaration, ExportDefaultDeclaration, ExportNamedDeclaration, Expression, Pattern, Positioned, Program } from './esmWalker' -import MagicString from 'magic-string' +import type { AutomockOptions } from './automock' import { cleanUrl } from '../utils' -import { +import { automockModule } from './automock' - getArbitraryModuleIdentifier, +export type { AutomockOptions as AutomockPluginOptions } from './automock' -} from './esmWalker' - -export interface AutomockPluginOptions { - /** - * @default "__vitest_mocker__" - */ - globalThisAccessor?: string -} - -export function automockPlugin(options: AutomockPluginOptions = {}): Plugin { +export function automockPlugin(options: AutomockOptions = {}): Plugin { return { name: 'vitest:automock', enforce: 'post', @@ -31,162 +21,3 @@ export function automockPlugin(options: AutomockPluginOptions = {}): Plugin { }, } } - -// TODO: better source map replacement -export function automockModule( - code: string, - mockType: 'automock' | 'autospy', - parse: (code: string) => any, - options: AutomockPluginOptions = {}, -): MagicString { - const globalThisAccessor = options.globalThisAccessor || '"__vitest_mocker__"' - const ast = parse(code) as Program - - const m = new MagicString(code) - - const allSpecifiers: { name: string; alias?: string }[] = [] - let importIndex = 0 - for (const _node of ast.body) { - if (_node.type === 'ExportAllDeclaration') { - throw new Error( - `automocking files with \`export *\` is not supported in browser mode because it cannot be statically analysed`, - ) - } - - if (_node.type === 'ExportNamedDeclaration') { - const node = _node as Positioned - const declaration = node.declaration // export const name - - function traversePattern(expression: Pattern) { - // export const test = '1' - if (expression.type === 'Identifier') { - allSpecifiers.push({ name: expression.name }) - } - // export const [test, ...rest] = [1, 2, 3] - else if (expression.type === 'ArrayPattern') { - expression.elements.forEach((element) => { - if (!element) { - return - } - traversePattern(element) - }) - } - else if (expression.type === 'ObjectPattern') { - expression.properties.forEach((property) => { - // export const { ...rest } = {} - if (property.type === 'RestElement') { - traversePattern(property) - } - // export const { test, test2: alias } = {} - else if (property.type === 'Property') { - traversePattern(property.value) - } - else { - property satisfies never - } - }) - } - else if (expression.type === 'RestElement') { - traversePattern(expression.argument) - } - // const [name[1], name[2]] = [] - // cannot be used in export - else if (expression.type === 'AssignmentPattern') { - throw new Error( - `AssignmentPattern is not supported. Please open a new bug report.`, - ) - } - // const test = thing.func() - // cannot be used in export - else if (expression.type === 'MemberExpression') { - throw new Error( - `MemberExpression is not supported. Please open a new bug report.`, - ) - } - else { - expression satisfies never - } - } - - if (declaration) { - if (declaration.type === 'FunctionDeclaration') { - allSpecifiers.push({ name: declaration.id.name }) - } - else if (declaration.type === 'VariableDeclaration') { - declaration.declarations.forEach((declaration) => { - traversePattern(declaration.id) - }) - } - else if (declaration.type === 'ClassDeclaration') { - allSpecifiers.push({ name: declaration.id.name }) - } - else { - declaration satisfies never - } - m.remove(node.start, (declaration as Positioned).start) - } - - const specifiers = node.specifiers || [] - const source = node.source - - if (!source && specifiers.length) { - specifiers.forEach((specifier) => { - allSpecifiers.push({ - alias: getArbitraryModuleIdentifier(specifier.exported), - name: getArbitraryModuleIdentifier(specifier.local), - }) - }) - m.remove(node.start, node.end) - } - else if (source && specifiers.length) { - const importNames: [string, string][] = [] - - specifiers.forEach((specifier) => { - const importedName = `__vitest_imported_${importIndex++}__` - importNames.push([getArbitraryModuleIdentifier(specifier.local), importedName]) - allSpecifiers.push({ - name: importedName, - alias: getArbitraryModuleIdentifier(specifier.exported), - }) - }) - - const importString = `import { ${importNames - .map(([name, alias]) => `${name} as ${alias}`) - .join(', ')} } from '${source.value}'` - - m.overwrite(node.start, node.end, importString) - } - } - if (_node.type === 'ExportDefaultDeclaration') { - const node = _node as Positioned - const declaration = node.declaration as Positioned - allSpecifiers.push({ name: '__vitest_default', alias: 'default' }) - m.overwrite(node.start, declaration.start, `const __vitest_default = `) - } - } - const moduleObject = ` -const __vitest_current_es_module__ = { - __esModule: true, - ${allSpecifiers.map(({ name }) => `["${name}"]: ${name},`).join('\n ')} -} -const __vitest_mocked_module__ = globalThis[${globalThisAccessor}].mockObject(__vitest_current_es_module__, "${mockType}") -` - const assigning = allSpecifiers - .map(({ name }, index) => { - return `const __vitest_mocked_${index}__ = __vitest_mocked_module__["${name}"]` - }) - .join('\n') - - const redeclarations = allSpecifiers - .map(({ name, alias }, index) => { - return ` __vitest_mocked_${index}__ as ${alias || name},` - }) - .join('\n') - const specifiersExports = ` -export { -${redeclarations} -} -` - m.append(moduleObject + assigning + specifiersExports) - return m -} diff --git a/packages/mocker/src/node/index.ts b/packages/mocker/src/node/index.ts index 88376fac0..8ad76b77b 100644 --- a/packages/mocker/src/node/index.ts +++ b/packages/mocker/src/node/index.ts @@ -1,6 +1,7 @@ export { createManualModuleSource } from '../utils' -export { automockModule, automockPlugin } from './automockPlugin' +export { automockModule } from './automock' export type { AutomockPluginOptions } from './automockPlugin' +export { automockPlugin } from './automockPlugin' export { dynamicImportPlugin } from './dynamicImportPlugin' export { hoistMocks, hoistMocksPlugin } from './hoistMocksPlugin' export type { HoistMocksPluginOptions, HoistMocksResult } from './hoistMocksPlugin' diff --git a/packages/mocker/src/node/interceptorPlugin.ts b/packages/mocker/src/node/interceptorPlugin.ts index 19641ca0e..0a58b03d5 100644 --- a/packages/mocker/src/node/interceptorPlugin.ts +++ b/packages/mocker/src/node/interceptorPlugin.ts @@ -4,7 +4,7 @@ import { readFile } from 'node:fs/promises' import { join } from 'node:path/posix' import { ManualMockedModule, MockerRegistry } from '../registry' import { cleanUrl, createManualModuleSource } from '../utils' -import { automockModule } from './automockPlugin' +import { automockModule } from './automock' export interface InterceptorPluginOptions { /**