From 075606b419841437164eaaf0e537aec80477ef6e Mon Sep 17 00:00:00 2001 From: LongYinan Date: Tue, 22 Feb 2022 23:34:51 +0800 Subject: [PATCH] Initialize emotion plugin --- packages/next-swc/Cargo.lock | 2 + packages/next-swc/crates/core/Cargo.toml | 12 +- .../next-swc/crates/core/src/emotion/hash.rs | 62 +++ .../next-swc/crates/core/src/emotion/mod.rs | 429 ++++++++++++++++++ packages/next-swc/crates/core/src/lib.rs | 54 ++- .../next-swc/crates/core/tests/fixture.rs | 57 ++- .../fixture/emotion/css-in-callback/input.tsx | 43 ++ .../fixture/emotion/css-in-callback/output.ts | 53 +++ packages/next-swc/crates/core/tests/full.rs | 1 + 9 files changed, 703 insertions(+), 10 deletions(-) create mode 100644 packages/next-swc/crates/core/src/emotion/hash.rs create mode 100644 packages/next-swc/crates/core/src/emotion/mod.rs create mode 100644 packages/next-swc/crates/core/tests/fixture/emotion/css-in-callback/input.tsx create mode 100644 packages/next-swc/crates/core/tests/fixture/emotion/css-in-callback/output.ts diff --git a/packages/next-swc/Cargo.lock b/packages/next-swc/Cargo.lock index dd6333b29e4f..eaaed4fb7b5d 100644 --- a/packages/next-swc/Cargo.lock +++ b/packages/next-swc/Cargo.lock @@ -796,12 +796,14 @@ checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" name = "next-swc" version = "0.0.0" dependencies = [ + "byteorder", "chrono", "easy-error", "either", "fxhash", "once_cell", "pathdiff", + "radix_fmt", "regex", "serde", "serde_json", diff --git a/packages/next-swc/crates/core/Cargo.toml b/packages/next-swc/crates/core/Cargo.toml index 063a0129010a..d4a0984d3546 100644 --- a/packages/next-swc/crates/core/Cargo.toml +++ b/packages/next-swc/crates/core/Cargo.toml @@ -7,25 +7,27 @@ version = "0.0.0" crate-type = ["cdylib", "rlib"] [dependencies] +byteorder = "1" chrono = "0.4" -once_cell = "1.8.0" easy-error = "1.0.0" either = "1" fxhash = "0.2.1" +once_cell = "1.8.0" pathdiff = "0.2.0" +radix_fmt = "1" +regex = "1.5" serde = "1" serde_json = "1" styled_components = "0.14.0" swc = "0.126.2" swc_atoms = "0.2.7" -swc_common = { version = "0.17.0", features = ["concurrent", "sourcemap"] } +swc_common = {version = "0.17.0", features = ["concurrent", "sourcemap"]} swc_css = "0.87.0" -swc_ecma_loader = { version = "0.28.0", features = ["node", "lru"] } -swc_ecmascript = { version = "0.114.2", features = ["codegen", "minifier", "optimization", "parser", "react", "transforms", "typescript", "utils", "visit"] } +swc_ecma_loader = {version = "0.28.0", features = ["node", "lru"]} +swc_ecmascript = {version = "0.114.2", features = ["codegen", "minifier", "optimization", "parser", "react", "transforms", "typescript", "utils", "visit"]} swc_node_base = "0.5.1" swc_stylis = "0.83.0" tracing = {version = "0.1.28", features = ["release_max_level_off"]} -regex = "1.5" [dev-dependencies] swc_ecma_transforms_testing = "0.60.0" diff --git a/packages/next-swc/crates/core/src/emotion/hash.rs b/packages/next-swc/crates/core/src/emotion/hash.rs new file mode 100644 index 000000000000..87c0621e75d8 --- /dev/null +++ b/packages/next-swc/crates/core/src/emotion/hash.rs @@ -0,0 +1,62 @@ +// Ported from https://github.com/aappleby/smhasher/blob/61a0530f28277f2e850bfc39600ce61d02b518de/src/MurmurHash2.cpp#L37-L86 + +use byteorder::{ByteOrder, LittleEndian}; + +const M: u32 = 0x5bd1_e995; + +pub(crate) fn murmurhash2(key: &[u8], initial_state: u32) -> u32 { + let mut h: u32 = initial_state; + + let mut four_bytes_chunks = key.chunks_exact(4); + for chunk in four_bytes_chunks.by_ref() { + let mut k: u32 = LittleEndian::read_u32(chunk); + k = k.wrapping_mul(M); + k ^= k >> 24; + h = k.wrapping_mul(M) ^ h.wrapping_mul(M); + } + let remainder = four_bytes_chunks.remainder(); + + // Handle the last few bytes of the input array + match remainder.len() { + 3 => { + h ^= u32::from(remainder[2]) << 16; + } + 2 => { + h ^= u32::from(remainder[1]) << 8; + } + 1 => { + h ^= u32::from(remainder[0]); + h = h.wrapping_mul(M); + } + _ => {} + } + h ^= h >> 13; + h = h.wrapping_mul(M); + h ^ (h >> 15) +} + +#[cfg(test)] +mod test { + + use super::murmurhash2; + + #[test] + fn test_murmur2() { + let s1 = "abcdef"; + let s2 = "abcdeg"; + for i in 0..5 { + assert_eq!( + murmurhash2(&s1[i..5].as_bytes(), 0), + murmurhash2(&s2[i..5].as_bytes(), 0) + ); + } + } + + #[test] + fn verify_hash() { + assert_eq!( + murmurhash2("something".as_bytes(), 0), + u32::from_str_radix("crsxd7", 36).unwrap() + ); + } +} diff --git a/packages/next-swc/crates/core/src/emotion/mod.rs b/packages/next-swc/crates/core/src/emotion/mod.rs new file mode 100644 index 000000000000..a26456c60fe1 --- /dev/null +++ b/packages/next-swc/crates/core/src/emotion/mod.rs @@ -0,0 +1,429 @@ +use std::path::PathBuf; +use std::sync::Arc; + +use fxhash::FxHashMap; +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; +use swc_common::{SourceMap, DUMMY_SP}; +use swc_ecmascript::ast::{ + ExprOrSpread, Ident, KeyValueProp, Lit, MemberProp, ObjectLit, Pat, Prop, PropName, + PropOrSpread, VarDeclarator, +}; +use swc_ecmascript::codegen::util::SourceMapperExt; +use swc_ecmascript::{ + ast::{Callee, Expr, ImportDecl, ImportSpecifier}, + visit::{swc_ecma_ast::CallExpr, Fold, FoldWith}, +}; + +mod hash; + +static EMOTION_OFFICIAL_LIBRARIES: Lazy> = Lazy::new(|| { + vec![ + EmotionModuleConfig { + module_name: "@emotion/styled".to_owned(), + exported_names: vec!["styled".to_owned()], + has_default_export: Some(true), + kind: ExprKind::Styled, + }, + EmotionModuleConfig { + module_name: "@emotion/react".to_owned(), + exported_names: vec!["css".to_owned()], + kind: ExprKind::Css, + ..Default::default() + }, + ] +}); + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EmotionOptions { + pub enabled: Option, + pub sourcemap: Option, + pub auto_label: Option, + pub label_format: Option, + pub css_prop_optimization: Option, + pub custom_modules: Option>, + pub jsx_factory: Option, + pub jsx_import_source: Option, +} + +impl Default for EmotionOptions { + fn default() -> Self { + EmotionOptions { + enabled: Some(false), + sourcemap: Some(true), + auto_label: Some(true), + label_format: Some("[local]".to_owned()), + css_prop_optimization: Some(true), + custom_modules: None, + jsx_import_source: Some("@emotion/react".to_owned()), + jsx_factory: None, + } + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct EmotionModuleConfig { + module_name: String, + exported_names: Vec, + has_default_export: Option, + kind: ExprKind, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +enum ImportType { + Named, + Namespace, + Default, +} + +impl Default for ImportType { + fn default() -> Self { + ImportType::Named + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +enum ExprKind { + Css, + Styled, +} + +impl Default for ExprKind { + fn default() -> Self { + ExprKind::Css + } +} + +#[derive(Debug)] +struct PackageMeta { + _type: ImportType, + kind: ExprKind, +} + +pub fn emotion( + emotion_options: EmotionOptions, + file_name: &PathBuf, + cm: Arc, + react_jsx_runtime: bool, + es_module_interop: bool, +) -> impl Fold { + EmotionTransformer::new( + emotion_options, + file_name, + cm, + react_jsx_runtime, + es_module_interop, + ) +} + +pub struct EmotionTransformer { + pub options: EmotionOptions, + filepath_hash: Option, + filepath: PathBuf, + dir: Option, + filename: Option, + cm: Arc, + _react_jsx_runtime: bool, + _es_module_interop: bool, + custom_modules: Vec, + import_packages: FxHashMap, + emotion_target_class_name_count: usize, + current_context: Option, +} + +impl EmotionTransformer { + pub fn new( + options: EmotionOptions, + path: &PathBuf, + cm: Arc, + react_jsx_runtime: bool, + es_module_interop: bool, + ) -> Self { + EmotionTransformer { + custom_modules: options.custom_modules.clone().unwrap_or_default(), + options, + filepath_hash: None, + filepath: path.to_owned(), + dir: path.parent().and_then(|p| p.to_str()).map(|s| s.to_owned()), + filename: path + .file_name() + .and_then(|filename| filename.to_str()) + .map(|s| s.to_owned()), + cm, + _react_jsx_runtime: react_jsx_runtime, + import_packages: FxHashMap::default(), + _es_module_interop: es_module_interop, + emotion_target_class_name_count: 0, + current_context: None, + } + } + + #[inline] + // Compute file hash on demand + // Memorize the hash of the file name + fn get_filename_hash(&mut self) -> u32 { + if self.filepath_hash.is_none() { + self.filepath_hash = Some(hash::murmurhash2( + self.filepath.to_string_lossy().as_bytes(), + 0, + )); + } + self.filepath_hash.unwrap() + } + + fn create_label(&self) -> String { + let mut label = format!( + "label:{}", + self.options + .label_format + .clone() + .unwrap_or("[local]".to_owned()) + ); + if let Some(current_context) = &self.current_context { + label = label.replace("[local]", current_context); + if let Some(filename) = self.filename.as_ref() { + label = label.replace("[filename]", filename); + } + if let Some(dir) = self.dir.as_ref() { + label = label.replace("[dir]", dir); + }; + } + label + } + + // Find the imported name from modules + // These import statements are supported: + // import styled from '@emotion/styled' + // import { default as whateverStyled } from '@emotion/styled' + // import * as styled from '@emotion/styled' // with `no_interop: true` + // import { css } from '@emotion/react' + // import emotionCss from '@emotion/react' + // import * as emotionCss from '@emotion/react' // with `no_interop: true` + fn generate_import_info(&mut self, expr: &ImportDecl) { + for c in EMOTION_OFFICIAL_LIBRARIES + .iter() + .chain(self.custom_modules.iter()) + { + if expr.src.value == c.module_name { + for specifier in expr.specifiers.iter() { + match specifier { + ImportSpecifier::Named(named) => { + for export_name in c.exported_names.iter() { + if named.local.as_ref() == export_name { + self.import_packages.insert( + named.local.as_ref().to_owned(), + PackageMeta { + _type: ImportType::Named, + kind: c.kind, + }, + ); + } + } + } + ImportSpecifier::Default(default) => { + if c.has_default_export.unwrap_or(false) { + self.import_packages.insert( + default.local.as_ref().to_owned(), + PackageMeta { + _type: ImportType::Default, + kind: c.kind, + }, + ); + } + } + ImportSpecifier::Namespace(namespace) => { + self.import_packages.insert( + namespace.local.to_string(), + PackageMeta { + _type: ImportType::Namespace, + kind: c.kind, + }, + ); + } + } + } + } + } + } + + fn create_target_arg_node(&mut self) -> PropOrSpread { + let stable_class_name = format!( + "e{}{}", + radix_fmt::radix_36(self.get_filename_hash()), + self.emotion_target_class_name_count + ); + self.emotion_target_class_name_count += 1; + PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { + key: PropName::Ident(Ident::new("target".into(), DUMMY_SP)), + value: Box::new(Expr::Lit(Lit::Str(stable_class_name.into()))), + }))) + } +} + +impl Fold for EmotionTransformer { + // Collect import modules that indicator if this file need to be transformed + fn fold_import_decl(&mut self, expr: ImportDecl) -> ImportDecl { + if expr.type_only { + return expr; + } + self.generate_import_info(&expr); + expr + } + + fn fold_var_declarator(&mut self, dec: VarDeclarator) -> VarDeclarator { + if let Pat::Ident(i) = &dec.name { + self.current_context = Some(i.id.as_ref().to_owned()); + } + dec.fold_children_with(self) + } + + fn fold_call_expr(&mut self, mut expr: CallExpr) -> CallExpr { + // If no package that we care about is imported, skip the following + // transformation logic. + if self.import_packages.is_empty() { + return expr; + } + if let Callee::Expr(e) = &mut expr.callee { + match e.as_mut() { + // css({}) + Expr::Ident(i) => { + if let Some(package) = self.import_packages.get(i.as_ref()) { + if !expr.args.is_empty() && matches!(package.kind, ExprKind::Css) { + if self.options.auto_label.unwrap_or(false) { + expr.args.push(ExprOrSpread { + spread: None, + expr: Box::new(Expr::Lit(Lit::Str(self.create_label().into()))), + }); + } + if self.options.sourcemap.unwrap_or(false) { + let _loc = self.cm.get_code_map().lookup_char_pos(expr.span.lo()); + // generate sourcemap + } + } + } + } + // styled('div')({}) + Expr::Call(c) => { + if let Callee::Expr(callee_exp) = &c.callee { + if let Expr::Ident(i) = callee_exp.as_ref() { + if let Some(package) = self.import_packages.get(i.as_ref()) { + if !c.args.is_empty() && matches!(package.kind, ExprKind::Styled) { + if self.options.auto_label.unwrap_or(false) { + c.args.push(ExprOrSpread { + spread: None, + expr: Box::new(Expr::Object(ObjectLit { + span: DUMMY_SP, + props: vec![ + self.create_target_arg_node(), + PropOrSpread::Prop(Box::new(Prop::KeyValue( + KeyValueProp { + key: PropName::Ident(Ident::new( + "label".into(), + DUMMY_SP, + )), + value: Box::new(Expr::Lit(Lit::Str( + self.create_label().into(), + ))), + }, + ))), + ], + })), + }); + } + if self.options.sourcemap.unwrap_or(false) { + let _loc = + self.cm.get_code_map().lookup_char_pos(expr.span.lo()); + // generate sourcemap + } + } + } + } + } + } + // styled.div({}) + // customEmotionReact.css({}) + Expr::Member(m) => { + if let Expr::Ident(i) = m.obj.as_ref() { + if let Some(package) = self.import_packages.get(i.as_ref()) { + if self.options.auto_label.unwrap_or(false) { + match package.kind { + ExprKind::Css => { + expr.args.push(ExprOrSpread { + spread: None, + expr: Box::new(Expr::Lit(Lit::Str( + self.create_label().into(), + ))), + }); + } + ExprKind::Styled => { + if let MemberProp::Ident(prop) = &m.prop { + return CallExpr { + span: expr.span, + type_args: expr.type_args, + args: expr.args, + callee: Callee::Expr(Box::new(Expr::Call( + CallExpr { + span: DUMMY_SP, + type_args: None, + callee: Callee::Expr(Box::new( + Expr::Ident(Ident::new( + i.sym.clone(), + i.span, + )), + )), + args: vec![ + ExprOrSpread { + spread: None, + expr: Box::new(Expr::Lit( + Lit::Str(prop.as_ref().into()), + )), + }, + ExprOrSpread { + spread: None, + expr: Box::new(Expr::Object( + ObjectLit { + span: DUMMY_SP, + props: vec![ + self.create_target_arg_node(), + PropOrSpread::Prop(Box::new( + Prop::KeyValue(KeyValueProp { + key: PropName::Ident( + Ident::new( + "label".into(), + DUMMY_SP, + ), + ), + value: Box::new(Expr::Lit( + Lit::Str( + self.create_label() + .into(), + ), + )), + }), + )), + ], + }, + )), + }, + ], + }, + ))), + }; + } + } + } + if self.options.sourcemap.unwrap_or(false) { + let _loc = + self.cm.get_code_map().lookup_char_pos(expr.span.lo()); + // generate sourcemap + } + } + } + } + } + _ => {} + } + } + expr + } +} diff --git a/packages/next-swc/crates/core/src/lib.rs b/packages/next-swc/crates/core/src/lib.rs index 6e788b3cd000..4d0b31d8a267 100644 --- a/packages/next-swc/crates/core/src/lib.rs +++ b/packages/next-swc/crates/core/src/lib.rs @@ -37,9 +37,10 @@ use std::rc::Rc; use std::{path::PathBuf, sync::Arc}; use swc::config::ModuleConfig; use swc_common::{self, chain, pass::Optional}; -use swc_common::{SourceFile, SourceMap}; +use swc_common::{FileName, SourceFile, SourceMap}; use swc_ecmascript::ast::EsVersion; use swc_ecmascript::transforms::pass::noop; +use swc_ecmascript::transforms::react::Runtime; use swc_ecmascript::{ parser::{lexer::Lexer, Parser, StringInput}, visit::Fold, @@ -48,6 +49,7 @@ use swc_ecmascript::{ pub mod amp_attributes; mod auto_cjs; pub mod disallow_re_export_all_in_page; +pub mod emotion; pub mod hook_optimizer; pub mod next_dynamic; pub mod next_ssg; @@ -99,6 +101,9 @@ pub struct TransformOptions { #[serde(default)] pub shake_exports: Option, + + #[serde(default)] + pub emotion: Option, } pub fn custom_before_pass( @@ -124,7 +129,7 @@ pub fn custom_before_pass( chain!( disallow_re_export_all_in_page::disallow_re_export_all_in_page(opts.is_page_file), - styled_jsx::styled_jsx(cm, file.name.clone()), + styled_jsx::styled_jsx(cm.clone(), file.name.clone()), hook_optimizer::hook_optimizer(), match &opts.styled_components { Some(config) => { @@ -166,7 +171,50 @@ pub fn custom_before_pass( match &opts.shake_exports { Some(config) => Either::Left(shake_exports::shake_exports(config.clone())), None => Either::Right(noop()), - } + }, + opts.emotion + .as_ref() + .and_then(|config| { + if !config.enabled.unwrap_or(false) { + return None; + } + let is_react_jsx_runtime = opts + .swc + .config + .jsc + .transform + .as_ref() + .and_then(|t| t.react.runtime) + .map(|r| matches!(r, Runtime::Automatic)) + .unwrap_or(false); + let es_module_interop = opts + .swc + .config + .module + .as_ref() + .map(|m| { + if let ModuleConfig::CommonJs(c) = m { + !c.no_interop + } else { + true + } + }) + .unwrap_or(true); + if let FileName::Real(path) = &file.name { + path.to_str().map(|_| { + Either::Left(emotion::EmotionTransformer::new( + config.clone(), + path, + cm, + is_react_jsx_runtime, + es_module_interop, + )) + }) + } else { + None + } + }) + .unwrap_or_else(|| Either::Right(noop())), ) } diff --git a/packages/next-swc/crates/core/tests/fixture.rs b/packages/next-swc/crates/core/tests/fixture.rs index 8b97b05ffdbb..032d2565217a 100644 --- a/packages/next-swc/crates/core/tests/fixture.rs +++ b/packages/next-swc/crates/core/tests/fixture.rs @@ -1,5 +1,6 @@ use next_swc::{ amp_attributes::amp_attributes, + emotion::{self, EmotionOptions}, next_dynamic::next_dynamic, next_ssg::next_ssg, page_config::page_config_test, @@ -13,8 +14,11 @@ use std::path::PathBuf; use swc_common::{chain, comments::SingleThreadedComments, FileName, Mark, Span, DUMMY_SP}; use swc_ecma_transforms_testing::{test, test_fixture}; use swc_ecmascript::{ - parser::{EsConfig, Syntax}, - transforms::{react::jsx, resolver}, + parser::{EsConfig, Syntax, TsConfig}, + transforms::{ + react::{jsx, Runtime}, + resolver, + }, }; use testing::fixture; @@ -25,6 +29,13 @@ fn syntax() -> Syntax { }) } +fn ts_syntax() -> Syntax { + Syntax::Typescript(TsConfig { + tsx: true, + ..Default::default() + }) +} + #[fixture("tests/fixture/amp/**/input.js")] fn amp_attributes_fixture(input: PathBuf) { let output = input.parent().unwrap().join("output.js"); @@ -260,3 +271,45 @@ fn shake_exports_fixture_default(input: PathBuf) { &output, ); } + +#[fixture("tests/fixture/emotion/**/input.tsx")] +fn next_emotion_fixture(input: PathBuf) { + let output = input.parent().unwrap().join("output.ts"); + test_fixture( + ts_syntax(), + &|tr| { + let top_level_mark = Mark::fresh(Mark::root()); + let jsx = jsx::( + tr.cm.clone(), + None, + swc_ecmascript::transforms::react::Options { + next: false, + runtime: Some(Runtime::Automatic), + throw_if_namespace: false, + development: false, + use_builtins: true, + use_spread: true, + ..Default::default() + }, + top_level_mark, + ); + chain!( + emotion::emotion( + EmotionOptions { + enabled: Some(true), + sourcemap: Some(true), + auto_label: Some(true), + ..Default::default() + }, + &input, + tr.cm.clone(), + true, + true, + ), + jsx, + ) + }, + &input, + &output, + ); +} diff --git a/packages/next-swc/crates/core/tests/fixture/emotion/css-in-callback/input.tsx b/packages/next-swc/crates/core/tests/fixture/emotion/css-in-callback/input.tsx new file mode 100644 index 000000000000..c909039002d1 --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/emotion/css-in-callback/input.tsx @@ -0,0 +1,43 @@ +import { css } from '@emotion/react' +import styled from '@emotion/styled' +import { PureComponent } from 'react' +import ReactDOM from 'react-dom' + +const stylesInCallback = (props: any) => + css({ + color: 'red', + background: 'yellow', + width: `${props.scale * 100}px`, + }) + +const styles = css({ + color: 'red', + width: '20px', +}) + +const DicContainer = styled.div({ + background: 'red', +}) + +const SpanContainer = styled('span')({ + background: 'yellow', +}) + +const Container = styled('button')` + ${stylesInCallback} + ${() => + css({ + background: 'red', + })} +` +export class SimpleComponent extends PureComponent { + render() { + return ( + + hello + + ) + } +} + +ReactDOM.render(, document.querySelector('#app')) diff --git a/packages/next-swc/crates/core/tests/fixture/emotion/css-in-callback/output.ts b/packages/next-swc/crates/core/tests/fixture/emotion/css-in-callback/output.ts new file mode 100644 index 000000000000..0e4961b9d54f --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/emotion/css-in-callback/output.ts @@ -0,0 +1,53 @@ +import { jsx as _jsx } from 'react/jsx-runtime' +import { css } from '@emotion/react' +import styled from '@emotion/styled' +import { PureComponent } from 'react' +import ReactDOM from 'react-dom' +const stylesInCallback = (props: any) => + css( + { + color: 'red', + background: 'yellow', + width: `${props.scale * 100}px`, + }, + 'label:stylesInCallback' + ) +const styles = css( + { + color: 'red', + width: '20px', + }, + 'label:styles' +) +const DicContainer = styled('div', { + target: 'ep3ww290', + label: 'label:DicContainer', +})({ + background: 'red', +}) +const SpanContainer = styled('span', { + target: 'ep3ww291', + label: 'label:SpanContainer', +})({ + background: 'yellow', +}) +const Container = styled('button')` + ${stylesInCallback} + ${() => + css( + { + background: 'red', + }, + 'label:Container' + )} +` +export class SimpleComponent extends PureComponent { + render() { + return _jsx(Container, { + children: _jsx('span', { + children: 'hello', + }), + }) + } +} +ReactDOM.render(_jsx(SimpleComponent, {}), document.querySelector('#app')) diff --git a/packages/next-swc/crates/core/tests/full.rs b/packages/next-swc/crates/core/tests/full.rs index f915188aace8..068008351674 100644 --- a/packages/next-swc/crates/core/tests/full.rs +++ b/packages/next-swc/crates/core/tests/full.rs @@ -61,6 +61,7 @@ fn test(input: &Path, minify: bool) { react_remove_properties: None, relay: None, shake_exports: None, + emotion: Some(assert_json("{}")), }; let options = options.patch(&fm);