Skip to content

Commit

Permalink
fix(es/jest): Handle @jest/globals (#8994)
Browse files Browse the repository at this point in the history
**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
  • Loading branch information
kdy1 authored May 30, 2024
1 parent f02be9f commit a81a01f
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 19 deletions.
23 changes: 23 additions & 0 deletions crates/swc/tests/fixture/jest/mock-globals/input/.swcrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": true,
"dynamicImport": true
},
"transform": {
"react": {
"runtime": "automatic"
}
},
"baseUrl": "./",
"target": "es2021",
"keepClassNames": true
},
"isModule": true,
"module": {
"type": "commonjs",
"ignoreDynamic": false
}
}
17 changes: 17 additions & 0 deletions crates/swc/tests/fixture/jest/mock-globals/input/1.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@

import { setTimeout } from 'timers/promises';

import { describe, expect, it, jest } from '@jest/globals';

jest.mock('timers/promises', () => ({
setTimeout: jest.fn(() => Promise.resolve())
}));

describe('suite', () => {
it('my-test', () => {
const totalDelay = jest
.mocked(setTimeout)
.mock.calls.reduce((agg, call) => agg + (call[0] as number), 0);
expect(totalDelay).toStrictEqual(0);
})
})
15 changes: 15 additions & 0 deletions crates/swc/tests/fixture/jest/mock-globals/output/1.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
const _promises = require("node:timers/promises");
const _globals = require("@jest/globals");
_globals.jest.mock('timers/promises', ()=>({
setTimeout: _globals.jest.fn(()=>Promise.resolve())
}));
(0, _globals.describe)('suite', ()=>{
(0, _globals.it)('my-test', ()=>{
const totalDelay = _globals.jest.mocked(_promises.setTimeout).mock.calls.reduce((agg, call)=>agg + call[0], 0);
(0, _globals.expect)(totalDelay).toStrictEqual(0);
});
});
11 changes: 11 additions & 0 deletions crates/swc_ecma_ast/src/module_decl.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use is_macro::Is;
use swc_atoms::Atom;
use swc_common::{ast_node, util::take::Take, EqIgnoreSpan, Span, DUMMY_SP};

use crate::{
Expand Down Expand Up @@ -313,3 +314,13 @@ pub enum ModuleExportName {
#[tag("StringLiteral")]
Str(Str),
}

impl ModuleExportName {
/// Get the atom of the export name.
pub fn atom(&self) -> &Atom {
match self {
ModuleExportName::Ident(i) => &i.sym,
ModuleExportName::Str(s) => &s.value,
}
}
}
84 changes: 65 additions & 19 deletions crates/swc_ecma_ext_transforms/src/jest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ static HOIST_METHODS: phf::Set<&str> = phf_set![
];

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

struct Jest;
#[derive(Default)]
struct Jest {
imported: Vec<Id>,
}

impl Jest {
fn visit_mut_stmt_like<T>(&mut self, orig: &mut Vec<T>)
Expand All @@ -38,21 +41,13 @@ impl Jest {
Expr::Call(CallExpr {
callee: Callee::Expr(callee),
..
}) => match &**callee {
Expr::Member(
callee @ MemberExpr {
prop: MemberProp::Ident(prop),
..
},
) => {
if is_jest(&callee.obj) && HOIST_METHODS.contains(&*prop.sym) {
hoisted.push(T::from_stmt(stmt))
} else {
new.push(T::from_stmt(stmt));
}
}) => {
if self.should_hoist(callee) {
hoisted.push(T::from_stmt(stmt))
} else {
new.push(T::from_stmt(stmt))
}
_ => new.push(T::from_stmt(stmt)),
},
}
_ => new.push(T::from_stmt(stmt)),
},

Expand All @@ -66,12 +61,63 @@ impl Jest {

*orig = new;
}

fn should_hoist(&self, e: &Expr) -> bool {
match e {
Expr::Ident(i) => self.imported.iter().any(|imported| *imported == i.to_id()),

Expr::Member(
callee @ MemberExpr {
prop: MemberProp::Ident(prop),
..
},
) => is_global_jest(&callee.obj) && HOIST_METHODS.contains(&*prop.sym),

_ => false,
}
}
}

impl VisitMut for Jest {
noop_visit_mut_type!();

fn visit_mut_module_items(&mut self, items: &mut Vec<ModuleItem>) {
for item in items.iter() {
if let ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl {
specifiers, src, ..
})) = item
{
if src.value == "@jest/globals" {
for s in specifiers {
match s {
ImportSpecifier::Named(ImportNamedSpecifier {
local,
imported: None,
is_type_only: false,
..
}) => {
if HOIST_METHODS.contains(&*local.sym) {
self.imported.push(local.to_id());
}
}

ImportSpecifier::Named(ImportNamedSpecifier {
local,
imported: Some(exported),
is_type_only: false,
..
}) => {
if HOIST_METHODS.contains(exported.atom()) {
self.imported.push(local.to_id());
}
}
_ => {}
}
}
}
}
}

self.visit_mut_stmt_like(items)
}

Expand All @@ -80,14 +126,14 @@ impl VisitMut for Jest {
}
}

fn is_jest(e: &Expr) -> bool {
fn is_global_jest(e: &Expr) -> bool {
match e {
Expr::Ident(i) => i.sym == *"jest",
Expr::Member(MemberExpr { obj, .. }) => is_jest(obj),
Expr::Member(MemberExpr { obj, .. }) => is_global_jest(obj),
Expr::Call(CallExpr {
callee: Callee::Expr(callee),
..
}) => is_jest(callee),
}) => is_global_jest(callee),
_ => false,
}
}

0 comments on commit a81a01f

Please sign in to comment.