|
1 | 1 | import type { |
2 | 2 | ArrowFunctionExpression, |
3 | 3 | AwaitExpression, |
| 4 | + BlockStatement, |
4 | 5 | CallExpression, |
5 | 6 | ExportDefaultDeclaration, |
6 | 7 | ExportNamedDeclaration, |
7 | 8 | Expression, |
8 | 9 | FunctionExpression, |
9 | 10 | Identifier, |
| 11 | + IfStatement, |
10 | 12 | ImportDeclaration, |
11 | 13 | SpreadElement, |
12 | 14 | VariableDeclaration, |
@@ -256,17 +258,38 @@ export function hoistMocks( |
256 | 258 | } |
257 | 259 | } |
258 | 260 |
|
| 261 | + function isImportMetaVitestCheck(node: Node): node is Positioned<IfStatement> { |
| 262 | + if (node.type !== 'IfStatement') { |
| 263 | + return false |
| 264 | + } |
| 265 | + const test = node.test as Positioned<Expression> |
| 266 | + return code.slice(test.start, test.end) === 'import.meta.vitest' |
| 267 | + } |
| 268 | + |
| 269 | + const hoistConditions = new Map<Node, string>() |
| 270 | + const inSourceBlocks = new Set<BlockStatement>() |
| 271 | + |
| 272 | + // in-source `vi.mock` and `vi.hoisted` run only when their file is the test file |
| 273 | + function addHoistedNode( |
| 274 | + node: Positioned<CallExpression | VariableDeclaration | AwaitExpression>, |
| 275 | + parentStack: Node[], |
| 276 | + ) { |
| 277 | + hoistedNodes.add(node) |
| 278 | + const guard = parentStack.find(isImportMetaVitestCheck) |
| 279 | + if (guard) { |
| 280 | + const test = guard.test as Positioned<Expression> |
| 281 | + // copied because the native loader rewrites `import.meta.vitest` |
| 282 | + hoistConditions.set(node, s.slice(test.start, test.end)) |
| 283 | + if (guard.consequent.type === 'BlockStatement') { |
| 284 | + inSourceBlocks.add(guard.consequent) |
| 285 | + } |
| 286 | + } |
| 287 | + } |
| 288 | + |
259 | 289 | const usedUtilityExports = new Set<string>() |
260 | | - let hasImportMetaVitest = false |
261 | 290 | let hasMockApiCall = false |
262 | 291 |
|
263 | 292 | esmWalker(ast, { |
264 | | - onImportMeta(node) { |
265 | | - const property = code.slice(node.end, node.end + 7) // '.vitest'.length |
266 | | - if (property === '.vitest') { |
267 | | - hasImportMetaVitest = true |
268 | | - } |
269 | | - }, |
270 | 293 | onIdentifier(id, info, parentStack) { |
271 | 294 | const binding = idToImportMap.get(id.name) |
272 | 295 | if (!binding) { |
@@ -307,7 +330,7 @@ export function hoistMocks( |
307 | 330 | // ) |
308 | 331 | // s.overwrite(node.end - 1, node.end, '))') |
309 | 332 | }, |
310 | | - onCallExpression(node) { |
| 333 | + onCallExpression(node, parentStack) { |
311 | 334 | if ( |
312 | 335 | node.callee.type === 'MemberExpression' && |
313 | 336 | isIdentifier(node.callee.object) && |
@@ -373,7 +396,7 @@ export function hoistMocks( |
373 | 396 | s.overwrite(moduleInfo.start, moduleInfo.end, s.slice(source.start, source.end)) |
374 | 397 | } |
375 | 398 | } |
376 | | - hoistedNodes.add(node) |
| 399 | + addHoistedNode(node, parentStack) |
377 | 400 | } |
378 | 401 | // vi.doMock(import('./path')) -> vi.doMock('./path') |
379 | 402 | // vi.doMock(await import('./path')) -> vi.doMock('./path') |
@@ -421,14 +444,14 @@ export function hoistMocks( |
421 | 444 | 'Cannot export hoisted variable. You can control hoisting behavior by placing the import from this file first.', |
422 | 445 | ) |
423 | 446 | // hoist "const variable = vi.hoisted(() => {})" |
424 | | - hoistedNodes.add(declarationNode) |
| 447 | + addHoistedNode(declarationNode, parentStack) |
425 | 448 | } else { |
426 | 449 | const awaitedExpression = findNodeAround(ast, node.start, 'AwaitExpression')?.node as |
427 | 450 | | Positioned<AwaitExpression> |
428 | 451 | | undefined |
429 | 452 | // hoist "await vi.hoisted(async () => {})" or "vi.hoisted(() => {})" |
430 | 453 | const moveNode = awaitedExpression?.argument === node ? awaitedExpression : node |
431 | | - hoistedNodes.add(moveNode) |
| 454 | + addHoistedNode(moveNode, parentStack) |
432 | 455 | } |
433 | 456 | } |
434 | 457 | } |
@@ -503,45 +526,57 @@ export function hoistMocks( |
503 | 526 | } |
504 | 527 | } |
505 | 528 |
|
506 | | - // validate that hoisted nodes are defined on the top level |
507 | | - // ignore `import.meta.vitest` because it needs to be inside an IfStatement |
508 | | - // and it can be used anywhere in the code (inside methods too) |
509 | | - if (!hasImportMetaVitest) { |
510 | | - for (const node of ast.body as Node[]) { |
511 | | - hoistedNodes.delete(node as any) |
512 | | - if (node.type === 'ExpressionStatement') { |
513 | | - hoistedNodes.delete(node.expression as any) |
514 | | - } |
| 529 | + // validate that hoisted nodes are defined on the top level of the module |
| 530 | + // or of an `if (import.meta.vitest)` block |
| 531 | + const topLevelNodes: Node[] = [ |
| 532 | + ...ast.body, |
| 533 | + ...Array.from(inSourceBlocks).flatMap((block) => block.body as Node[]), |
| 534 | + ] |
| 535 | + for (const node of topLevelNodes) { |
| 536 | + hoistedNodes.delete(node as any) |
| 537 | + if (node.type === 'ExpressionStatement') { |
| 538 | + hoistedNodes.delete(node.expression as any) |
515 | 539 | } |
| 540 | + } |
516 | 541 |
|
517 | | - if (hoistedNodes.size) { |
518 | | - const locations = createIndexLocationsMap(code) |
519 | | - const map = options.getMap && new TraceMap(options.getMap() as any) |
520 | | - const plural = hoistedNodes.size > 1 |
521 | | - const message = [ |
522 | | - `${hoistedNodes.size} call${plural ? 's' : ''} in "${relative(options.root || process.cwd(), id)}" ${plural ? 'were' : 'was'} defined outside of the module's top level scope:`, |
523 | | - '', |
524 | | - ...Array.from(hoistedNodes, (invalidNode) => { |
525 | | - const currentLocation = locations.get(invalidNode.start) |
526 | | - const originalLocation = |
527 | | - map && currentLocation && originalPositionFor(map, currentLocation) |
528 | | - const location = |
529 | | - originalLocation?.column != null && originalLocation?.line != null |
530 | | - ? ` at ${relative(options.root || process.cwd(), id)}:${originalLocation.line}:${originalLocation.column + 1}` |
531 | | - : '' |
532 | | - return `- ${getNodeName(getNodeCall(invalidNode))}${location}` |
533 | | - }), |
534 | | - '', |
535 | | - `Although ${plural ? 'they appear nested, they' : 'it appears nested, it'} will be hoisted and executed before anything in this file. Move ${plural ? 'them' : 'it'} to the top level to reflect ${plural ? 'their' : 'its'} actual execution order.`, |
536 | | - 'See: https://vitest.dev/guide/mocking/modules#how-it-works', |
537 | | - ].join('\n') |
538 | | - throw new Error(message) |
539 | | - } |
| 542 | + if (hoistedNodes.size) { |
| 543 | + const locations = createIndexLocationsMap(code) |
| 544 | + const map = options.getMap && new TraceMap(options.getMap() as any) |
| 545 | + const plural = hoistedNodes.size > 1 |
| 546 | + const topLevel = inSourceBlocks.size |
| 547 | + ? 'the top level of the `if (import.meta.vitest)` block' |
| 548 | + : 'the top level' |
| 549 | + const message = [ |
| 550 | + `${hoistedNodes.size} call${plural ? 's' : ''} in "${relative(options.root || process.cwd(), id)}" ${plural ? 'were' : 'was'} defined outside of the module's top level scope:`, |
| 551 | + '', |
| 552 | + ...Array.from(hoistedNodes, (invalidNode) => { |
| 553 | + const currentLocation = locations.get(invalidNode.start) |
| 554 | + const originalLocation = map && currentLocation && originalPositionFor(map, currentLocation) |
| 555 | + const location = |
| 556 | + originalLocation?.column != null && originalLocation?.line != null |
| 557 | + ? ` at ${relative(options.root || process.cwd(), id)}:${originalLocation.line}:${originalLocation.column + 1}` |
| 558 | + : '' |
| 559 | + return `- ${getNodeName(getNodeCall(invalidNode))}${location}` |
| 560 | + }), |
| 561 | + '', |
| 562 | + `Although ${plural ? 'they appear nested, they' : 'it appears nested, it'} will be hoisted and executed before anything in this file. Move ${plural ? 'them' : 'it'} to ${topLevel} to reflect ${plural ? 'their' : 'its'} actual execution order.`, |
| 563 | + 'See: https://vitest.dev/guide/mocking/modules#how-it-works', |
| 564 | + ].join('\n') |
| 565 | + throw new Error(message) |
540 | 566 | } |
541 | 567 |
|
542 | 568 | // hoist vi.mock/vi.hoisted |
543 | 569 | for (const node of arrayNodes) { |
544 | 570 | const end = getNodeTail(code, node) |
| 571 | + const condition = hoistConditions.get(node) |
| 572 | + if (condition) { |
| 573 | + // `var` keeps the declared names visible outside of the `if` block |
| 574 | + if (node.type === 'VariableDeclaration') { |
| 575 | + s.update(node.start, node.start + node.kind.length, 'var') |
| 576 | + } |
| 577 | + s.prependRight(node.start, `if (${condition}) { `) |
| 578 | + s.appendLeft(node.end, ' }') |
| 579 | + } |
545 | 580 | // don't hoist into itself if it's already at the top |
546 | 581 | if (hoistIndex === end || hoistIndex === node.start) { |
547 | 582 | hoistIndex = end |
|
0 commit comments