Skip to content

Commit 253df1c

Browse files
authoredJan 23, 2024
fix(vitest): throw an error if vi.mock is exported (#5034)
1 parent 7344870 commit 253df1c

File tree

4 files changed

+104
-38
lines changed

4 files changed

+104
-38
lines changed
 

‎packages/vite-node/src/source-map-handler.ts

+2-10
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,11 @@ import { TraceMap, originalPositionFor } from '@jridgewell/trace-mapping'
1111
// Only install once if called multiple times
1212
let errorFormatterInstalled = false
1313

14-
// If true, the caches are reset before a stack trace formatting operation
15-
const emptyCacheBetweenOperations = false
16-
1714
// Maps a file path to a string containing the file contents
18-
let fileContentsCache: Record<string, string> = {}
15+
const fileContentsCache: Record<string, string> = {}
1916

2017
// Maps a file path to a source map for that file
21-
let sourceMapCache: Record<string, { url: string | null; map: TraceMap | null }> = {}
18+
const sourceMapCache: Record<string, { url: string | null; map: TraceMap | null }> = {}
2219

2320
// Regex for detecting source maps
2421
const reSourceMap = /^data:application\/json[^,]+base64,/
@@ -405,11 +402,6 @@ function wrapCallSite(frame: CallSite, state: State) {
405402
// This function is part of the V8 stack trace API, for more info see:
406403
// https://v8.dev/docs/stack-trace-api
407404
function prepareStackTrace(error: Error, stack: CallSite[]) {
408-
if (emptyCacheBetweenOperations) {
409-
fileContentsCache = {}
410-
sourceMapCache = {}
411-
}
412-
413405
const name = error.name || 'Error'
414406
const message = error.message || ''
415407
const errorString = `${name}: ${message}`

‎packages/vitest/src/node/hoistMocks.ts

+30-28
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,25 @@ export function hoistMocks(code: string, id: string, parse: PluginContext['parse
162162
}
163163
}
164164

165+
function assertNotDefaultExport(node: Positioned<CallExpression>, error: string) {
166+
const defaultExport = findNodeAround(ast, node.start, 'ExportDefaultDeclaration')?.node as Positioned<ExportDefaultDeclaration> | undefined
167+
if (defaultExport?.declaration === node || (defaultExport?.declaration.type === 'AwaitExpression' && defaultExport.declaration.argument === node))
168+
throw createSyntaxError(defaultExport, error)
169+
}
170+
171+
function assertNotNamedExport(node: Positioned<VariableDeclaration>, error: string) {
172+
const nodeExported = findNodeAround(ast, node.start, 'ExportNamedDeclaration')?.node as Positioned<ExportNamedDeclaration> | undefined
173+
if (nodeExported?.declaration === node)
174+
throw createSyntaxError(nodeExported, error)
175+
}
176+
177+
function getVariableDeclaration(node: Positioned<CallExpression>) {
178+
const declarationNode = findNodeAround(ast, node.start, 'VariableDeclaration')?.node as Positioned<VariableDeclaration> | undefined
179+
const init = declarationNode?.declarations[0]?.init
180+
if (init && (init === node || (init.type === 'AwaitExpression' && init.argument === node)))
181+
return declarationNode
182+
}
183+
165184
esmWalker(ast, {
166185
onIdentifier(id, info, parentStack) {
167186
const binding = idToImportMap.get(id.name)
@@ -197,38 +216,21 @@ export function hoistMocks(code: string, id: string, parse: PluginContext['parse
197216
) {
198217
const methodName = node.callee.property.name
199218

200-
if (methodName === 'mock' || methodName === 'unmock')
219+
if (methodName === 'mock' || methodName === 'unmock') {
220+
const method = `${node.callee.object.name}.${methodName}`
221+
assertNotDefaultExport(node, `Cannot export the result of "${method}". Remove export declaration because "${method}" doesn\'t return anything.`)
222+
const declarationNode = getVariableDeclaration(node)
223+
if (declarationNode)
224+
assertNotNamedExport(declarationNode, `Cannot export the result of "${method}". Remove export declaration because "${method}" doesn\'t return anything.`)
201225
hoistedNodes.push(node)
226+
}
202227

203228
if (methodName === 'hoisted') {
204-
// check it's not a default export
205-
const defaultExport = findNodeAround(ast, node.start, 'ExportDefaultDeclaration')?.node as Positioned<ExportDefaultDeclaration> | undefined
206-
if (defaultExport?.declaration === node || (defaultExport?.declaration.type === 'AwaitExpression' && defaultExport.declaration.argument === node))
207-
throw createSyntaxError(defaultExport, 'Cannot export hoisted variable. You can control hoisting behavior by placing the import from this file first.')
208-
209-
const declarationNode = findNodeAround(ast, node.start, 'VariableDeclaration')?.node as Positioned<VariableDeclaration> | undefined
210-
const init = declarationNode?.declarations[0]?.init
211-
const isViHoisted = (node: CallExpression) => {
212-
return node.callee.type === 'MemberExpression'
213-
&& isIdentifier(node.callee.object)
214-
&& (node.callee.object.name === 'vi' || node.callee.object.name === 'vitest')
215-
&& isIdentifier(node.callee.property)
216-
&& node.callee.property.name === 'hoisted'
217-
}
229+
assertNotDefaultExport(node, 'Cannot export hoisted variable. You can control hoisting behavior by placing the import from this file first.')
218230

219-
const canMoveDeclaration = (init
220-
&& init.type === 'CallExpression'
221-
&& isViHoisted(init)) /* const v = vi.hoisted() */
222-
|| (init
223-
&& init.type === 'AwaitExpression'
224-
&& init.argument.type === 'CallExpression'
225-
&& isViHoisted(init.argument)) /* const v = await vi.hoisted() */
226-
227-
if (canMoveDeclaration) {
228-
// export const variable = vi.hoisted()
229-
const nodeExported = findNodeAround(ast, declarationNode.start, 'ExportNamedDeclaration')?.node as Positioned<ExportNamedDeclaration> | undefined
230-
if (nodeExported?.declaration === declarationNode)
231-
throw createSyntaxError(nodeExported, 'Cannot export hoisted variable. You can control hoisting behavior by placing the import from this file first.')
231+
const declarationNode = getVariableDeclaration(node)
232+
if (declarationNode) {
233+
assertNotNamedExport(declarationNode, 'Cannot export hoisted variable. You can control hoisting behavior by placing the import from this file first.')
232234
// hoist "const variable = vi.hoisted(() => {})"
233235
hoistedNodes.push(declarationNode)
234236
}

‎test/core/test/__snapshots__/injector-mock.test.ts.snap

+40
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,43 @@ exports[`throws an error when nodes are incompatible > correctly throws an error
120120
5| })
121121
6| "
122122
`;
123+
124+
exports[`throws an error when nodes are incompatible > correctly throws an error if vi.mock is exported as a named export 1`] = `"Cannot export the result of "vi.mock". Remove export declaration because "vi.mock" doesn't return anything."`;
125+
126+
exports[`throws an error when nodes are incompatible > correctly throws an error if vi.mock is exported as a named export 2`] = `
127+
" 1| import { vi } from 'vitest'
128+
2|
129+
3| export const mocked = vi.mock('./mocked')
130+
| ^
131+
4| "
132+
`;
133+
134+
exports[`throws an error when nodes are incompatible > correctly throws an error if vi.mock is exported as default export 1`] = `"Cannot export the result of "vi.mock". Remove export declaration because "vi.mock" doesn't return anything."`;
135+
136+
exports[`throws an error when nodes are incompatible > correctly throws an error if vi.mock is exported as default export 2`] = `
137+
" 1| import { vi } from 'vitest'
138+
2|
139+
3| export default vi.mock('./mocked')
140+
| ^
141+
4| "
142+
`;
143+
144+
exports[`throws an error when nodes are incompatible > correctly throws an error if vi.unmock is exported as a named export 1`] = `"Cannot export the result of "vi.unmock". Remove export declaration because "vi.unmock" doesn't return anything."`;
145+
146+
exports[`throws an error when nodes are incompatible > correctly throws an error if vi.unmock is exported as a named export 2`] = `
147+
" 1| import { vi } from 'vitest'
148+
2|
149+
3| export const mocked = vi.unmock('./mocked')
150+
| ^
151+
4| "
152+
`;
153+
154+
exports[`throws an error when nodes are incompatible > correctly throws an error if vi.unmock is exported as default export 1`] = `"Cannot export the result of "vi.unmock". Remove export declaration because "vi.unmock" doesn't return anything."`;
155+
156+
exports[`throws an error when nodes are incompatible > correctly throws an error if vi.unmock is exported as default export 2`] = `
157+
" 1| import { vi } from 'vitest'
158+
2|
159+
3| export default vi.unmock('./mocked')
160+
| ^
161+
4| "
162+
`;

‎test/core/test/injector-mock.test.ts

+32
Original file line numberDiff line numberDiff line change
@@ -1323,6 +1323,38 @@ import { vi } from 'vitest'
13231323
export default await vi.hoisted(async () => {
13241324
return {}
13251325
})
1326+
`,
1327+
],
1328+
[
1329+
'vi.mock is exported as default export',
1330+
`\
1331+
import { vi } from 'vitest'
1332+
1333+
export default vi.mock('./mocked')
1334+
`,
1335+
],
1336+
[
1337+
'vi.unmock is exported as default export',
1338+
`\
1339+
import { vi } from 'vitest'
1340+
1341+
export default vi.unmock('./mocked')
1342+
`,
1343+
],
1344+
[
1345+
'vi.mock is exported as a named export',
1346+
`\
1347+
import { vi } from 'vitest'
1348+
1349+
export const mocked = vi.mock('./mocked')
1350+
`,
1351+
],
1352+
[
1353+
'vi.unmock is exported as a named export',
1354+
`\
1355+
import { vi } from 'vitest'
1356+
1357+
export const mocked = vi.unmock('./mocked')
13261358
`,
13271359
],
13281360
])('correctly throws an error if %s', (_, code) => {

0 commit comments

Comments
 (0)
Please sign in to comment.