Skip to content

Commit a81a01f

Browse files
authored
fix(es/jest): Handle @jest/globals (#8994)
**Description:** - We have two `jest` pass. One in `@swc/core` (via _hidden.jest flag), and one in the plugin. This PR fixes only the core one. However, the linked issue is wrong because the code tries to break the rules of ESM specification. So, although this PR closes the issue, the final form is not the issue the author wanted. **Related issue:** - Closes swc-project/plugins#310
1 parent f02be9f commit a81a01f

File tree

5 files changed

+131
-19
lines changed

5 files changed

+131
-19
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"$schema": "https://json.schemastore.org/swcrc",
3+
"jsc": {
4+
"parser": {
5+
"syntax": "typescript",
6+
"tsx": true,
7+
"dynamicImport": true
8+
},
9+
"transform": {
10+
"react": {
11+
"runtime": "automatic"
12+
}
13+
},
14+
"baseUrl": "./",
15+
"target": "es2021",
16+
"keepClassNames": true
17+
},
18+
"isModule": true,
19+
"module": {
20+
"type": "commonjs",
21+
"ignoreDynamic": false
22+
}
23+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
2+
import { setTimeout } from 'timers/promises';
3+
4+
import { describe, expect, it, jest } from '@jest/globals';
5+
6+
jest.mock('timers/promises', () => ({
7+
setTimeout: jest.fn(() => Promise.resolve())
8+
}));
9+
10+
describe('suite', () => {
11+
it('my-test', () => {
12+
const totalDelay = jest
13+
.mocked(setTimeout)
14+
.mock.calls.reduce((agg, call) => agg + (call[0] as number), 0);
15+
expect(totalDelay).toStrictEqual(0);
16+
})
17+
})
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"use strict";
2+
Object.defineProperty(exports, "__esModule", {
3+
value: true
4+
});
5+
const _promises = require("node:timers/promises");
6+
const _globals = require("@jest/globals");
7+
_globals.jest.mock('timers/promises', ()=>({
8+
setTimeout: _globals.jest.fn(()=>Promise.resolve())
9+
}));
10+
(0, _globals.describe)('suite', ()=>{
11+
(0, _globals.it)('my-test', ()=>{
12+
const totalDelay = _globals.jest.mocked(_promises.setTimeout).mock.calls.reduce((agg, call)=>agg + call[0], 0);
13+
(0, _globals.expect)(totalDelay).toStrictEqual(0);
14+
});
15+
});

crates/swc_ecma_ast/src/module_decl.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use is_macro::Is;
2+
use swc_atoms::Atom;
23
use swc_common::{ast_node, util::take::Take, EqIgnoreSpan, Span, DUMMY_SP};
34

45
use crate::{
@@ -313,3 +314,13 @@ pub enum ModuleExportName {
313314
#[tag("StringLiteral")]
314315
Str(Str),
315316
}
317+
318+
impl ModuleExportName {
319+
/// Get the atom of the export name.
320+
pub fn atom(&self) -> &Atom {
321+
match self {
322+
ModuleExportName::Ident(i) => &i.sym,
323+
ModuleExportName::Str(s) => &s.value,
324+
}
325+
}
326+
}

crates/swc_ecma_ext_transforms/src/jest.rs

Lines changed: 65 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@ static HOIST_METHODS: phf::Set<&str> = phf_set![
1313
];
1414

1515
pub fn jest() -> impl Fold + VisitMut {
16-
as_folder(Jest)
16+
as_folder(Jest::default())
1717
}
1818

19-
struct Jest;
19+
#[derive(Default)]
20+
struct Jest {
21+
imported: Vec<Id>,
22+
}
2023

2124
impl Jest {
2225
fn visit_mut_stmt_like<T>(&mut self, orig: &mut Vec<T>)
@@ -38,21 +41,13 @@ impl Jest {
3841
Expr::Call(CallExpr {
3942
callee: Callee::Expr(callee),
4043
..
41-
}) => match &**callee {
42-
Expr::Member(
43-
callee @ MemberExpr {
44-
prop: MemberProp::Ident(prop),
45-
..
46-
},
47-
) => {
48-
if is_jest(&callee.obj) && HOIST_METHODS.contains(&*prop.sym) {
49-
hoisted.push(T::from_stmt(stmt))
50-
} else {
51-
new.push(T::from_stmt(stmt));
52-
}
44+
}) => {
45+
if self.should_hoist(callee) {
46+
hoisted.push(T::from_stmt(stmt))
47+
} else {
48+
new.push(T::from_stmt(stmt))
5349
}
54-
_ => new.push(T::from_stmt(stmt)),
55-
},
50+
}
5651
_ => new.push(T::from_stmt(stmt)),
5752
},
5853

@@ -66,12 +61,63 @@ impl Jest {
6661

6762
*orig = new;
6863
}
64+
65+
fn should_hoist(&self, e: &Expr) -> bool {
66+
match e {
67+
Expr::Ident(i) => self.imported.iter().any(|imported| *imported == i.to_id()),
68+
69+
Expr::Member(
70+
callee @ MemberExpr {
71+
prop: MemberProp::Ident(prop),
72+
..
73+
},
74+
) => is_global_jest(&callee.obj) && HOIST_METHODS.contains(&*prop.sym),
75+
76+
_ => false,
77+
}
78+
}
6979
}
7080

7181
impl VisitMut for Jest {
7282
noop_visit_mut_type!();
7383

7484
fn visit_mut_module_items(&mut self, items: &mut Vec<ModuleItem>) {
85+
for item in items.iter() {
86+
if let ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl {
87+
specifiers, src, ..
88+
})) = item
89+
{
90+
if src.value == "@jest/globals" {
91+
for s in specifiers {
92+
match s {
93+
ImportSpecifier::Named(ImportNamedSpecifier {
94+
local,
95+
imported: None,
96+
is_type_only: false,
97+
..
98+
}) => {
99+
if HOIST_METHODS.contains(&*local.sym) {
100+
self.imported.push(local.to_id());
101+
}
102+
}
103+
104+
ImportSpecifier::Named(ImportNamedSpecifier {
105+
local,
106+
imported: Some(exported),
107+
is_type_only: false,
108+
..
109+
}) => {
110+
if HOIST_METHODS.contains(exported.atom()) {
111+
self.imported.push(local.to_id());
112+
}
113+
}
114+
_ => {}
115+
}
116+
}
117+
}
118+
}
119+
}
120+
75121
self.visit_mut_stmt_like(items)
76122
}
77123

@@ -80,14 +126,14 @@ impl VisitMut for Jest {
80126
}
81127
}
82128

83-
fn is_jest(e: &Expr) -> bool {
129+
fn is_global_jest(e: &Expr) -> bool {
84130
match e {
85131
Expr::Ident(i) => i.sym == *"jest",
86-
Expr::Member(MemberExpr { obj, .. }) => is_jest(obj),
132+
Expr::Member(MemberExpr { obj, .. }) => is_global_jest(obj),
87133
Expr::Call(CallExpr {
88134
callee: Callee::Expr(callee),
89135
..
90-
}) => is_jest(callee),
136+
}) => is_global_jest(callee),
91137
_ => false,
92138
}
93139
}

0 commit comments

Comments
 (0)