Skip to content

Commit e7fb667

Browse files
scttcpercodex
andcommitted
fix(emotion): Handle synthetic React Compiler spans
React Compiler can leave generated Emotion calls at BytePos(0), which makes pure comments and source map lookup panic. Skip those location-only bits when the span is synthetic and add a small wasm regression. Co-authored-by: Codex <noreply@openai.com>
1 parent 423f5e5 commit e7fb667

5 files changed

Lines changed: 122 additions & 70 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
},
99
"devDependencies": {
1010
"@changesets/cli": "^2.27.1",
11-
"@swc/core": "^1.15.40",
11+
"@swc/core": "^1.15.43",
1212
"@taplo/cli": "^0.7.0",
1313
"@types/node": "^22.13.9",
1414
"husky": "^9.0.11",

packages/emotion/__tests__/wasm.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,37 @@ test("Should keep plain keyframes label for namespace imports", async () => {
123123
);
124124
expect(output.code).not.toContain("label:pulse");
125125
});
126+
127+
test("Should transform css prop output from React Compiler", async () => {
128+
const output = await transform(
129+
`
130+
import { css } from "@emotion/react";
131+
132+
export function App() {
133+
return <div css={css\`width:120px;\`} />;
134+
}
135+
`,
136+
{
137+
envName: "development",
138+
filename: "percentInput.tsx",
139+
jsc: {
140+
parser: {
141+
syntax: "typescript",
142+
tsx: true,
143+
},
144+
transform: {
145+
react: {
146+
runtime: "automatic",
147+
importSource: "@emotion/react",
148+
},
149+
reactCompiler: true,
150+
},
151+
experimental: {
152+
plugins: [[pluginPath, {}]],
153+
},
154+
},
155+
} satisfies Options,
156+
);
157+
158+
expect(output.code).toContain('css("width:120px;", "")');
159+
});

packages/emotion/src/lib.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ use std::path::Path;
44

55
use serde::Deserialize;
66
use swc_atoms::{atom, Atom};
7-
use swc_common::{plugin::metadata::TransformPluginMetadataContextKind, SourceMapper, Spanned};
7+
use swc_common::{
8+
plugin::metadata::TransformPluginMetadataContextKind, BytePos, SourceMapper, Spanned,
9+
};
810
use swc_ecma_ast::Program;
911
use swc_emotion::EmotionOptions;
1012
use swc_plugin_macro::plugin_transform;
@@ -70,8 +72,12 @@ pub fn process_transform(program: Program, data: TransformPluginProgramMetadata)
7072
.unwrap_or_default();
7173
let path = Path::new(&file_name);
7274
let source_map = std::sync::Arc::new(data.source_map);
73-
let pos = source_map.lookup_char_pos(program.span().lo);
74-
let hash = pos.file.src_hash() as u32;
75+
let hash = if program.span().lo == BytePos(0) {
76+
0
77+
} else {
78+
let pos = source_map.lookup_char_pos(program.span().lo);
79+
pos.file.src_hash() as u32
80+
};
7581
program.apply(swc_emotion::emotion(
7682
&config,
7783
path,

packages/emotion/transform/src/lib.rs

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,14 @@ impl<'a, C: Comments> EmotionTransformer<'a, C> {
258258
label
259259
}
260260

261+
fn add_pure_comment(&self, pos: BytePos) {
262+
// React Compiler can synthesize Emotion calls with dummy spans.
263+
// SWC cannot attach comments at BytePos(0), so skip the hint there.
264+
if pos != BytePos(0) {
265+
self.comments.add_pure_comment(pos);
266+
}
267+
}
268+
261269
fn create_call_label_arg(&self, kind: ExprKind) -> ExprOrSpread {
262270
self.create_runtime_label(kind, false).as_arg()
263271
}
@@ -267,6 +275,10 @@ impl<'a, C: Comments> EmotionTransformer<'a, C> {
267275
}
268276

269277
fn create_sourcemap(&mut self, pos: BytePos) -> Option<String> {
278+
if pos == BytePos(0) {
279+
return None;
280+
}
281+
270282
if self.options.sourcemap.unwrap_or(false) {
271283
let loc = self.cm.get_code_map().lookup_char_pos(pos);
272284
let filename = self.filepath.to_str().map(BytesStr::from_str_slice);
@@ -527,7 +539,7 @@ impl<'a, C: Comments> EmotionTransformer<'a, C> {
527539
args.push(cm.as_arg());
528540
}
529541

530-
self.comments.add_pure_comment(expr_pos);
542+
self.add_pure_comment(expr_pos);
531543
let call_span = Span::new(expr_pos, expr_pos);
532544

533545
let callee = match &css_callee {
@@ -582,7 +594,7 @@ impl<C: Comments> Fold for EmotionTransformer<'_, C> {
582594
if matches!(*kind, ExprKind::Css | ExprKind::Keyframes)
583595
&& !self.in_jsx_element
584596
{
585-
self.comments.add_pure_comment(expr.span.lo());
597+
self.add_pure_comment(expr.span.lo());
586598
if self.options.auto_label.unwrap_or(false) {
587599
expr.args.push(self.create_call_label_arg(*kind));
588600
}
@@ -608,7 +620,7 @@ impl<C: Comments> Fold for EmotionTransformer<'_, C> {
608620
if !c.args.is_empty() {
609621
let mut args_props = Vec::with_capacity(2);
610622
args_props.push(self.create_label_prop_node("target"));
611-
self.comments.add_pure_comment(expr.span.lo());
623+
self.add_pure_comment(expr.span.lo());
612624
if self.options.auto_label.unwrap_or(false) {
613625
args_props.push(PropOrSpread::Prop(Box::new(
614626
Prop::KeyValue(KeyValueProp {
@@ -678,7 +690,7 @@ impl<C: Comments> Fold for EmotionTransformer<'_, C> {
678690
args_props.push(self.create_label_prop_node("target"));
679691
let mut args = vec![prop.sym.as_ref().as_arg()];
680692
if !self.in_jsx_element {
681-
self.comments.add_pure_comment(expr.span.lo());
693+
self.add_pure_comment(expr.span.lo());
682694
if self.options.auto_label.unwrap_or(false) {
683695
args_props.push(PropOrSpread::Prop(Box::new(
684696
Prop::KeyValue(KeyValueProp {
@@ -724,7 +736,7 @@ impl<C: Comments> Fold for EmotionTransformer<'_, C> {
724736
.iter()
725737
.find_map(|item| match_runtime_export(item, &m.prop))
726738
{
727-
self.comments.add_pure_comment(expr.span.lo());
739+
self.add_pure_comment(expr.span.lo());
728740
if self.options.auto_label.unwrap_or(false) {
729741
expr.args.push(self.create_call_label_arg(kind));
730742
}
@@ -787,7 +799,7 @@ impl<C: Comments> Fold for EmotionTransformer<'_, C> {
787799
let mut callee = call.take();
788800
let mut object_props = Vec::with_capacity(2);
789801
object_props.push(self.create_label_prop_node("target"));
790-
self.comments.add_pure_comment(callee.span.lo());
802+
self.add_pure_comment(callee.span.lo());
791803
if self.options.auto_label.unwrap_or(false) {
792804
object_props.push(PropOrSpread::Prop(Box::new(
793805
Prop::KeyValue(KeyValueProp {
@@ -864,7 +876,7 @@ impl<C: Comments> Fold for EmotionTransformer<'_, C> {
864876
if matches!(*kind, ExprKind::Css | ExprKind::Keyframes) {
865877
let mut args = self.create_args_from_tagged_tpl(&mut tagged_tpl.tpl);
866878
if !self.in_jsx_element {
867-
self.comments.add_pure_comment(i.span.lo());
879+
self.add_pure_comment(i.span.lo());
868880
if self.options.auto_label.unwrap_or(false) {
869881
args.push(self.create_tagged_tpl_label_arg(*kind));
870882
}
@@ -911,7 +923,7 @@ impl<C: Comments> Fold for EmotionTransformer<'_, C> {
911923
args.push(cm.as_arg());
912924
}
913925

914-
self.comments.add_pure_comment(member_expr.span.lo());
926+
self.add_pure_comment(member_expr.span.lo());
915927
return Expr::Call(CallExpr {
916928
callee: CallExpr {
917929
callee: i.take().as_callee(),
@@ -935,7 +947,7 @@ impl<C: Comments> Fold for EmotionTransformer<'_, C> {
935947
if let Some(kind) = c.exported_names.iter().find_map(|item| {
936948
match_runtime_export(item, &member_expr.prop)
937949
}) {
938-
self.comments.add_pure_comment(member_expr.span.lo());
950+
self.add_pure_comment(member_expr.span.lo());
939951
return Expr::Call(CallExpr {
940952
callee: member_expr.take().as_callee(),
941953
args: {

0 commit comments

Comments
 (0)