Skip to content

Commit

Permalink
feat(core): provide jest hoist plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
LongYinan authored and Brooooooklyn committed Aug 27, 2020
1 parent 7747878 commit f3638f2
Show file tree
Hide file tree
Showing 11 changed files with 294 additions and 17 deletions.
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@ serde_json = "1"
swc = { git = "https://github.com/swc-project/swc" }
swc_common = { git = "https://github.com/swc-project/swc", features = ["tty-emitter"] }
swc_ecmascript = { git = "https://github.com/swc-project/swc", features = ["parser", "transforms"] }
swc_ecma_visit = { git = "https://github.com/swc-project/swc" }

[target.'cfg(all(unix, not(target_env = "musl")))'.dependencies]
jemallocator = { version = "0.3", features = ["disable_initial_exec_tls"] }

[target.'cfg(windows)'.dependencies]
mimalloc = { version = "0.1" }

[build-dependencies]
napi-build = "0.2"

Expand Down
19 changes: 19 additions & 0 deletions jest-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const { transformJest } = require('@swc-node/core')

const fixture = `
import { timer } from 'utils'
jest.mock('./utils')
describe('timer', () => {
it('timer should work', () => {
expect(timer()).toBe(1)
})
})
jest.unmock()
`

const { code } = transformJest(fixture, 'timer.spec.ts')

console.log(code)
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
},
"scripts": {
"build": "napi build --release --platform ./packages/core/swc",
"build:debug": "napi build --platform ./packages/core/swc",
"format": "run-p format:md format:json format:source format:yml",
"format:json": "prettier --parser json --write '**/*.json'",
"format:md": "prettier --parser markdown --write './*.md' './packages/**/*.md'",
Expand Down
9 changes: 9 additions & 0 deletions packages/core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ export function transformSync(
map: string
}

export function transformJest(
source: string | Buffer,
path: string,
options?: Options,
): {
code: string
map: string
}

export function transform(
source: string | Buffer,
path: string,
Expand Down
30 changes: 28 additions & 2 deletions packages/core/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ module.exports = {
const swcOptions = {
filename: path,
jsc: {
target: 'es2018',
target: opts.target || 'es2018',
parser: {
syntax: 'typescript',
tsx: typeof opts.jsx !== 'undefined' ? opts.jsx : path.endsWith('.tsx'),
Expand All @@ -64,12 +64,38 @@ module.exports = {
}
return bindings.transformSync(source, path, Buffer.from(JSON.stringify(swcOptions)))
},
transformJest: function transformJest(source, path, options) {
const opts = options == null ? {} : options
const swcOptions = {
filename: path,
jsc: {
target: opts.target || 'es2018',
parser: {
syntax: 'typescript',
tsx: typeof opts.jsx !== 'undefined' ? opts.jsx : path.endsWith('.tsx'),
decorators: Boolean(opts.experimentalDecorators),
dynamicImport: Boolean(opts.dynamicImport),
},
transform: {
legacyDecorator: Boolean(opts.experimentalDecorators),
decoratorMetadata: Boolean(opts.emitDecoratorMetadata),
},
},
isModule: true,
module: {
type: opts.module || 'commonjs',
},
sourceMaps: typeof opts.sourcemap === 'undefined' ? true : opts.sourcemap,
swcrc: false,
}
return bindings.transformJest(source, path, Buffer.from(JSON.stringify(swcOptions)))
},
transform: function transform(source, path, options) {
const opts = options == null ? {} : options
const swcOptions = {
filename: path,
jsc: {
target: 'es2018',
target: opts.target || 'es2018',
parser: {
syntax: 'typescript',
tsx: typeof opts.jsx !== 'undefined' ? opts.jsx : path.endsWith('.tsx'),
Expand Down
41 changes: 41 additions & 0 deletions packages/jest/__test__/hoist-top-level.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { transformJest } from '@swc-node/core'
import test from 'ava'

const BrorrwFromTsJest = `
const foo = 'foo'
console.log(foo)
jest.enableAutomock()
jest.disableAutomock()
jest.mock('./foo')
jest.mock('./foo/bar', () => 'bar')
jest.unmock('./bar/foo').dontMock('./bar/bar')
jest.deepUnmock('./foo')
jest.mock('./foo').mock('./bar')
const func = () => {
const bar = 'bar'
console.log(bar)
jest.unmock('./foo')
jest.mock('./bar')
jest.mock('./bar/foo', () => 'foo')
jest.unmock('./foo/bar')
jest.unmock('./bar/foo').dontMock('./bar/bar')
jest.deepUnmock('./bar')
jest.mock('./foo').mock('./bar')
}
const func2 = () => {
const bar = 'bar'
console.log(bar)
jest.mock('./bar')
jest.unmock('./foo/bar')
jest.mock('./bar/foo', () => 'foo')
jest.unmock('./foo')
jest.unmock('./bar/foo').dontMock('./bar/bar')
jest.deepUnmock('./bar')
jest.mock('./foo').mock('./bar')
}
`

test('should hoist top level jest mock call', (t) => {
const { code } = transformJest(BrorrwFromTsJest, 'jest.spec.ts', { target: 'es2018', sourcemap: false })
t.snapshot(code)
})
46 changes: 46 additions & 0 deletions packages/jest/__test__/hoist-top-level.spec.ts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Snapshot report for `packages/jest/__test__/hoist-top-level.spec.ts`

The actual snapshot is saved in `hoist-top-level.spec.ts.snap`.

Generated by [AVA](https://avajs.dev).

## should hoist top level jest mock call

> Snapshot 1
`jest.enableAutomock();␊
jest.disableAutomock();␊
jest.mock('./foo');␊
jest.mock('./foo/bar', ()=>'bar'␊
);␊
jest.deepUnmock('./foo');␊
"use strict";␊
const foo = 'foo';␊
console.log(foo);␊
jest.unmock('./bar/foo').dontMock('./bar/bar');␊
jest.mock('./foo').mock('./bar');␊
const func = ()=>{␊
const bar = 'bar';␊
console.log(bar);␊
jest.unmock('./foo');␊
jest.mock('./bar');␊
jest.mock('./bar/foo', ()=>'foo'␊
);␊
jest.unmock('./foo/bar');␊
jest.unmock('./bar/foo').dontMock('./bar/bar');␊
jest.deepUnmock('./bar');␊
jest.mock('./foo').mock('./bar');␊
};␊
const func2 = ()=>{␊
const bar = 'bar';␊
console.log(bar);␊
jest.mock('./bar');␊
jest.unmock('./foo/bar');␊
jest.mock('./bar/foo', ()=>'foo'␊
);␊
jest.unmock('./foo');␊
jest.unmock('./bar/foo').dontMock('./bar/bar');␊
jest.deepUnmock('./bar');␊
jest.mock('./foo').mock('./bar');␊
};␊
`
Binary file added packages/jest/__test__/hoist-top-level.spec.ts.snap
Binary file not shown.
4 changes: 2 additions & 2 deletions packages/jest/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
const { transformSync } = require('@swc-node/core')
const { transformJest } = require('@swc-node/core')

module.exports = {
process(src, path, jestConfig) {
const [, , transformOptions = {}] =
(jestConfig.transform || []).find(([, transformerPath]) => transformerPath === __filename) || []
if (/\.(t|j)sx?$/.test(path)) {
return transformSync(src, path, transformOptions)
return transformJest(src, path, transformOptions)
}
return src
},
Expand Down
90 changes: 90 additions & 0 deletions src/jest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use std::collections::HashSet;
use std::path::PathBuf;
use std::str::FromStr;

use napi::{Error, Result, Status};
use once_cell::sync::OnceCell;
use swc::{config::Options, TransformOutput};
use swc_common::{self, FileName};
use swc_ecma_visit::Fold;
use swc_ecmascript::ast::{Expr, ExprOrSuper, Module, ModuleItem, Stmt};

use crate::get_compiler;

const JEST: &'static str = "jest";

static HOIST_METHODS: OnceCell<HashSet<&'static str>> = OnceCell::new();

pub fn jest_transform(
source: String,
filename: &str,
register_options: &Options,
) -> Result<TransformOutput> {
let c = get_compiler();
let fm = c.cm.new_source_file(
FileName::Real(
PathBuf::from_str(filename)
.map_err(|e| Error::new(Status::InvalidArg, format!("Invalid path {}", e)))?,
),
source,
);

let jest_hoist_transformer = JestHoistTransformer;

c.process_js_with_custom_pass(fm, register_options, jest_hoist_transformer)
.map_err(|e| Error::new(Status::GenericFailure, format!("Process js failed {}", e)))
}

struct JestHoistTransformer;

impl Fold for JestHoistTransformer {
fn fold_module(&mut self, mut n: Module) -> Module {
let hoist_methods = HOIST_METHODS.get_or_init(|| {
let mut hash_set = HashSet::with_capacity(5);
hash_set.insert("mock");
hash_set.insert("unmock");
hash_set.insert("enableAutomock");
hash_set.insert("disableAutomock");
hash_set.insert("deepUnmock");
hash_set
});
let mut new_body = Vec::with_capacity(n.body.len());
let mut stmts_to_hoist = Vec::with_capacity(n.body.len());
n.body.iter().for_each(|item| match item {
ModuleItem::Stmt(Stmt::Expr(expr_stmt)) => match &*expr_stmt.expr {
Expr::Call(call_expr) => {
if let ExprOrSuper::Expr(expr) = &call_expr.callee {
if let Expr::Member(member) = expr.as_ref() {
if let ExprOrSuper::Expr(expr) = &member.obj {
if let Expr::Ident(ident) = expr.as_ref() {
let name = ident.sym.as_ref();
let is_jest = name == JEST;
if is_jest {
if let Expr::Ident(ident) = member.prop.as_ref() {
let func_name = ident.sym.as_ref();
if hoist_methods.get(func_name).is_some() {
stmts_to_hoist.insert(0, item.clone());
return ();
}
}
}
}
}
}
}
new_body.push(item.clone());
}
_ => new_body.push(item.clone()),
},
_ => new_body.push(item.clone()),
});

for stmt in stmts_to_hoist {
new_body.insert(0, stmt);
}

n.body = new_body;

n
}
}
Loading

0 comments on commit f3638f2

Please sign in to comment.