Skip to content

Commit e9c9282

Browse files
authored
feat(mocker): add automocker entry (#8301)
1 parent ad16d46 commit e9c9282

File tree

6 files changed

+183
-175
lines changed

6 files changed

+183
-175
lines changed

packages/mocker/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
"types": "./dist/redirect.d.ts",
3333
"default": "./dist/redirect.js"
3434
},
35+
"./automock": {
36+
"types": "./dist/automock.d.ts",
37+
"default": "./dist/automock.js"
38+
},
3539
"./register": {
3640
"types": "./dist/register.d.ts",
3741
"default": "./dist/register.js"

packages/mocker/rollup.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const entries = {
1313
'index': 'src/index.ts',
1414
'node': 'src/node/index.ts',
1515
'redirect': 'src/node/redirect.ts',
16+
'automock': 'src/node/automock.ts',
1617
'browser': 'src/browser/index.ts',
1718
'register': 'src/browser/register.ts',
1819
'auto-register': 'src/browser/auto-register.ts',

packages/mocker/src/node/automock.ts

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import type { Declaration, ExportDefaultDeclaration, ExportNamedDeclaration, Expression, Pattern, Positioned, Program } from './esmWalker'
2+
import MagicString from 'magic-string'
3+
import {
4+
getArbitraryModuleIdentifier,
5+
} from './esmWalker'
6+
7+
export interface AutomockOptions {
8+
/**
9+
* @default "__vitest_mocker__"
10+
*/
11+
globalThisAccessor?: string
12+
}
13+
14+
// TODO: better source map replacement
15+
export function automockModule(
16+
code: string,
17+
mockType: 'automock' | 'autospy',
18+
parse: (code: string) => any,
19+
options: AutomockOptions = {},
20+
): MagicString {
21+
const globalThisAccessor = options.globalThisAccessor || '"__vitest_mocker__"'
22+
const ast = parse(code) as Program
23+
24+
const m = new MagicString(code)
25+
26+
const allSpecifiers: { name: string; alias?: string }[] = []
27+
let importIndex = 0
28+
for (const _node of ast.body) {
29+
if (_node.type === 'ExportAllDeclaration') {
30+
throw new Error(
31+
`automocking files with \`export *\` is not supported in browser mode because it cannot be statically analysed`,
32+
)
33+
}
34+
35+
if (_node.type === 'ExportNamedDeclaration') {
36+
const node = _node as Positioned<ExportNamedDeclaration>
37+
const declaration = node.declaration // export const name
38+
39+
function traversePattern(expression: Pattern) {
40+
// export const test = '1'
41+
if (expression.type === 'Identifier') {
42+
allSpecifiers.push({ name: expression.name })
43+
}
44+
// export const [test, ...rest] = [1, 2, 3]
45+
else if (expression.type === 'ArrayPattern') {
46+
expression.elements.forEach((element) => {
47+
if (!element) {
48+
return
49+
}
50+
traversePattern(element)
51+
})
52+
}
53+
else if (expression.type === 'ObjectPattern') {
54+
expression.properties.forEach((property) => {
55+
// export const { ...rest } = {}
56+
if (property.type === 'RestElement') {
57+
traversePattern(property)
58+
}
59+
// export const { test, test2: alias } = {}
60+
else if (property.type === 'Property') {
61+
traversePattern(property.value)
62+
}
63+
else {
64+
property satisfies never
65+
}
66+
})
67+
}
68+
else if (expression.type === 'RestElement') {
69+
traversePattern(expression.argument)
70+
}
71+
// const [name[1], name[2]] = []
72+
// cannot be used in export
73+
else if (expression.type === 'AssignmentPattern') {
74+
throw new Error(
75+
`AssignmentPattern is not supported. Please open a new bug report.`,
76+
)
77+
}
78+
// const test = thing.func()
79+
// cannot be used in export
80+
else if (expression.type === 'MemberExpression') {
81+
throw new Error(
82+
`MemberExpression is not supported. Please open a new bug report.`,
83+
)
84+
}
85+
else {
86+
expression satisfies never
87+
}
88+
}
89+
90+
if (declaration) {
91+
if (declaration.type === 'FunctionDeclaration') {
92+
allSpecifiers.push({ name: declaration.id.name })
93+
}
94+
else if (declaration.type === 'VariableDeclaration') {
95+
declaration.declarations.forEach((declaration) => {
96+
traversePattern(declaration.id)
97+
})
98+
}
99+
else if (declaration.type === 'ClassDeclaration') {
100+
allSpecifiers.push({ name: declaration.id.name })
101+
}
102+
else {
103+
declaration satisfies never
104+
}
105+
m.remove(node.start, (declaration as Positioned<Declaration>).start)
106+
}
107+
108+
const specifiers = node.specifiers || []
109+
const source = node.source
110+
111+
if (!source && specifiers.length) {
112+
specifiers.forEach((specifier) => {
113+
allSpecifiers.push({
114+
alias: getArbitraryModuleIdentifier(specifier.exported),
115+
name: getArbitraryModuleIdentifier(specifier.local),
116+
})
117+
})
118+
m.remove(node.start, node.end)
119+
}
120+
else if (source && specifiers.length) {
121+
const importNames: [string, string][] = []
122+
123+
specifiers.forEach((specifier) => {
124+
const importedName = `__vitest_imported_${importIndex++}__`
125+
importNames.push([getArbitraryModuleIdentifier(specifier.local), importedName])
126+
allSpecifiers.push({
127+
name: importedName,
128+
alias: getArbitraryModuleIdentifier(specifier.exported),
129+
})
130+
})
131+
132+
const importString = `import { ${importNames
133+
.map(([name, alias]) => `${name} as ${alias}`)
134+
.join(', ')} } from '${source.value}'`
135+
136+
m.overwrite(node.start, node.end, importString)
137+
}
138+
}
139+
if (_node.type === 'ExportDefaultDeclaration') {
140+
const node = _node as Positioned<ExportDefaultDeclaration>
141+
const declaration = node.declaration as Positioned<Expression>
142+
allSpecifiers.push({ name: '__vitest_default', alias: 'default' })
143+
m.overwrite(node.start, declaration.start, `const __vitest_default = `)
144+
}
145+
}
146+
const moduleObject = `
147+
const __vitest_current_es_module__ = {
148+
__esModule: true,
149+
${allSpecifiers.map(({ name }) => `["${name}"]: ${name},`).join('\n ')}
150+
}
151+
const __vitest_mocked_module__ = globalThis[${globalThisAccessor}].mockObject(__vitest_current_es_module__, "${mockType}")
152+
`
153+
const assigning = allSpecifiers
154+
.map(({ name }, index) => {
155+
return `const __vitest_mocked_${index}__ = __vitest_mocked_module__["${name}"]`
156+
})
157+
.join('\n')
158+
159+
const redeclarations = allSpecifiers
160+
.map(({ name, alias }, index) => {
161+
return ` __vitest_mocked_${index}__ as ${alias || name},`
162+
})
163+
.join('\n')
164+
const specifiersExports = `
165+
export {
166+
${redeclarations}
167+
}
168+
`
169+
m.append(moduleObject + assigning + specifiersExports)
170+
return m
171+
}
Lines changed: 4 additions & 173 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,11 @@
11
import type { Plugin } from 'vite'
2-
import type { Declaration, ExportDefaultDeclaration, ExportNamedDeclaration, Expression, Pattern, Positioned, Program } from './esmWalker'
3-
import MagicString from 'magic-string'
2+
import type { AutomockOptions } from './automock'
43
import { cleanUrl } from '../utils'
5-
import {
4+
import { automockModule } from './automock'
65

7-
getArbitraryModuleIdentifier,
6+
export type { AutomockOptions as AutomockPluginOptions } from './automock'
87

9-
} from './esmWalker'
10-
11-
export interface AutomockPluginOptions {
12-
/**
13-
* @default "__vitest_mocker__"
14-
*/
15-
globalThisAccessor?: string
16-
}
17-
18-
export function automockPlugin(options: AutomockPluginOptions = {}): Plugin {
8+
export function automockPlugin(options: AutomockOptions = {}): Plugin {
199
return {
2010
name: 'vitest:automock',
2111
enforce: 'post',
@@ -31,162 +21,3 @@ export function automockPlugin(options: AutomockPluginOptions = {}): Plugin {
3121
},
3222
}
3323
}
34-
35-
// TODO: better source map replacement
36-
export function automockModule(
37-
code: string,
38-
mockType: 'automock' | 'autospy',
39-
parse: (code: string) => any,
40-
options: AutomockPluginOptions = {},
41-
): MagicString {
42-
const globalThisAccessor = options.globalThisAccessor || '"__vitest_mocker__"'
43-
const ast = parse(code) as Program
44-
45-
const m = new MagicString(code)
46-
47-
const allSpecifiers: { name: string; alias?: string }[] = []
48-
let importIndex = 0
49-
for (const _node of ast.body) {
50-
if (_node.type === 'ExportAllDeclaration') {
51-
throw new Error(
52-
`automocking files with \`export *\` is not supported in browser mode because it cannot be statically analysed`,
53-
)
54-
}
55-
56-
if (_node.type === 'ExportNamedDeclaration') {
57-
const node = _node as Positioned<ExportNamedDeclaration>
58-
const declaration = node.declaration // export const name
59-
60-
function traversePattern(expression: Pattern) {
61-
// export const test = '1'
62-
if (expression.type === 'Identifier') {
63-
allSpecifiers.push({ name: expression.name })
64-
}
65-
// export const [test, ...rest] = [1, 2, 3]
66-
else if (expression.type === 'ArrayPattern') {
67-
expression.elements.forEach((element) => {
68-
if (!element) {
69-
return
70-
}
71-
traversePattern(element)
72-
})
73-
}
74-
else if (expression.type === 'ObjectPattern') {
75-
expression.properties.forEach((property) => {
76-
// export const { ...rest } = {}
77-
if (property.type === 'RestElement') {
78-
traversePattern(property)
79-
}
80-
// export const { test, test2: alias } = {}
81-
else if (property.type === 'Property') {
82-
traversePattern(property.value)
83-
}
84-
else {
85-
property satisfies never
86-
}
87-
})
88-
}
89-
else if (expression.type === 'RestElement') {
90-
traversePattern(expression.argument)
91-
}
92-
// const [name[1], name[2]] = []
93-
// cannot be used in export
94-
else if (expression.type === 'AssignmentPattern') {
95-
throw new Error(
96-
`AssignmentPattern is not supported. Please open a new bug report.`,
97-
)
98-
}
99-
// const test = thing.func()
100-
// cannot be used in export
101-
else if (expression.type === 'MemberExpression') {
102-
throw new Error(
103-
`MemberExpression is not supported. Please open a new bug report.`,
104-
)
105-
}
106-
else {
107-
expression satisfies never
108-
}
109-
}
110-
111-
if (declaration) {
112-
if (declaration.type === 'FunctionDeclaration') {
113-
allSpecifiers.push({ name: declaration.id.name })
114-
}
115-
else if (declaration.type === 'VariableDeclaration') {
116-
declaration.declarations.forEach((declaration) => {
117-
traversePattern(declaration.id)
118-
})
119-
}
120-
else if (declaration.type === 'ClassDeclaration') {
121-
allSpecifiers.push({ name: declaration.id.name })
122-
}
123-
else {
124-
declaration satisfies never
125-
}
126-
m.remove(node.start, (declaration as Positioned<Declaration>).start)
127-
}
128-
129-
const specifiers = node.specifiers || []
130-
const source = node.source
131-
132-
if (!source && specifiers.length) {
133-
specifiers.forEach((specifier) => {
134-
allSpecifiers.push({
135-
alias: getArbitraryModuleIdentifier(specifier.exported),
136-
name: getArbitraryModuleIdentifier(specifier.local),
137-
})
138-
})
139-
m.remove(node.start, node.end)
140-
}
141-
else if (source && specifiers.length) {
142-
const importNames: [string, string][] = []
143-
144-
specifiers.forEach((specifier) => {
145-
const importedName = `__vitest_imported_${importIndex++}__`
146-
importNames.push([getArbitraryModuleIdentifier(specifier.local), importedName])
147-
allSpecifiers.push({
148-
name: importedName,
149-
alias: getArbitraryModuleIdentifier(specifier.exported),
150-
})
151-
})
152-
153-
const importString = `import { ${importNames
154-
.map(([name, alias]) => `${name} as ${alias}`)
155-
.join(', ')} } from '${source.value}'`
156-
157-
m.overwrite(node.start, node.end, importString)
158-
}
159-
}
160-
if (_node.type === 'ExportDefaultDeclaration') {
161-
const node = _node as Positioned<ExportDefaultDeclaration>
162-
const declaration = node.declaration as Positioned<Expression>
163-
allSpecifiers.push({ name: '__vitest_default', alias: 'default' })
164-
m.overwrite(node.start, declaration.start, `const __vitest_default = `)
165-
}
166-
}
167-
const moduleObject = `
168-
const __vitest_current_es_module__ = {
169-
__esModule: true,
170-
${allSpecifiers.map(({ name }) => `["${name}"]: ${name},`).join('\n ')}
171-
}
172-
const __vitest_mocked_module__ = globalThis[${globalThisAccessor}].mockObject(__vitest_current_es_module__, "${mockType}")
173-
`
174-
const assigning = allSpecifiers
175-
.map(({ name }, index) => {
176-
return `const __vitest_mocked_${index}__ = __vitest_mocked_module__["${name}"]`
177-
})
178-
.join('\n')
179-
180-
const redeclarations = allSpecifiers
181-
.map(({ name, alias }, index) => {
182-
return ` __vitest_mocked_${index}__ as ${alias || name},`
183-
})
184-
.join('\n')
185-
const specifiersExports = `
186-
export {
187-
${redeclarations}
188-
}
189-
`
190-
m.append(moduleObject + assigning + specifiersExports)
191-
return m
192-
}

packages/mocker/src/node/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export { createManualModuleSource } from '../utils'
2-
export { automockModule, automockPlugin } from './automockPlugin'
2+
export { automockModule } from './automock'
33
export type { AutomockPluginOptions } from './automockPlugin'
4+
export { automockPlugin } from './automockPlugin'
45
export { dynamicImportPlugin } from './dynamicImportPlugin'
56
export { hoistMocks, hoistMocksPlugin } from './hoistMocksPlugin'
67
export type { HoistMocksPluginOptions, HoistMocksResult } from './hoistMocksPlugin'

packages/mocker/src/node/interceptorPlugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { readFile } from 'node:fs/promises'
44
import { join } from 'node:path/posix'
55
import { ManualMockedModule, MockerRegistry } from '../registry'
66
import { cleanUrl, createManualModuleSource } from '../utils'
7-
import { automockModule } from './automockPlugin'
7+
import { automockModule } from './automock'
88

99
export interface InterceptorPluginOptions {
1010
/**

0 commit comments

Comments
 (0)