Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions src/react_compiler/lowering/build_hir/jsx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,8 @@ pub(super) fn lower_jsx_call(
}
}

for prop in obj.properties.iter() {
let last_index = obj.properties.len().saturating_sub(1);
for (index, prop) in obj.properties.iter().enumerate() {
if matches!(prop.kind, G::PropertyKind::Spread) {
let value = prop.value.as_ref().ok_or_else(|| {
todo_err("(BuildHIR::lowerJsxCall) spread without value", loc)
Expand All @@ -274,6 +275,9 @@ pub(super) fn lower_jsx_call(
props.push(JsxAttribute::SpreadAttribute { argument });
continue;
}
if record_accessor_prop(builder, prop)? {
continue;
}
let Some(key_expr) = prop.key.as_ref() else {
continue;
};
Expand All @@ -293,7 +297,11 @@ pub(super) fn lower_jsx_call(
continue;
};

if name == b"children" {
// The visit pass appends the JSX children as the last property.
// Any earlier `children` key is an attribute (or came from an
// inlined `{...{children}}` spread) that a later key overrides at
// runtime, so it stays an attribute and keeps its position.
if name == b"children" && index == last_index {
lower_jsx_children(builder, value_expr, is_static_children, &mut children)?;
continue;
}
Expand All @@ -315,6 +323,9 @@ pub(super) fn lower_jsx_call(
props.push(JsxAttribute::SpreadAttribute { argument });
continue;
}
if record_accessor_prop(builder, prop)? {
continue;
}
let Some(key_expr) = prop.key.as_ref() else {
continue;
};
Expand Down Expand Up @@ -408,6 +419,29 @@ fn lower_jsx_children(
Ok(())
}

/// The visit pass inlines `<a {...{ get g() {} }} />` into the props object, so
/// a getter or setter can reach here. A `JsxAttribute` holds a value, not an
/// accessor, so the function is left uncompiled, as `lower_object_method` does
/// for an accessor in an object literal. Returns true when `prop` is one.
fn record_accessor_prop(
builder: &mut HirBuilder,
prop: &G::Property,
) -> Result<bool, CompilerError> {
let kind = match prop.kind {
G::PropertyKind::Get => "get",
G::PropertyKind::Set => "set",
_ => return Ok(false),
};
builder.record_error(CompilerErrorDetail {
category: ErrorCategory::Todo,
reason: format!("(BuildHIR::lowerJsxCall) Handle {kind} functions in JSX props"),
description: None,
loc: convert_loc(prop.key.as_ref().map_or(Loc::EMPTY, |key| key.loc)),
suggestions: None,
})?;
Ok(true)
}

fn todo_err(reason: &str, loc: Option<SourceLocation>) -> CompilerError {
let mut err = CompilerError::new();
err.push_error_detail(CompilerErrorDetail {
Expand Down
76 changes: 76 additions & 0 deletions test/bundler/transpiler/react-compiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1139,6 +1139,82 @@ describe("bundler", () => {
},
});

// The parser turns `<div children="x">y</div>` into
// `jsx("div", { children: "x", children: "y" })`: the JSX children are the
// last property, so they replace every earlier `children` key. It also
// inlines `{...{ k: v }}` into that object. The compiler reads the element
// back from the call, and has to keep each key in place and keep a getter a
// getter.
itBundled("react-compiler/ChildrenAttributeIsNotAJsxChild", {
files: {
"/entry.jsx": /* jsx */ `
function Attr(p) { return <div children="x">y</div>; }
function AttrExpr(p) { return <div children={p.a}>{p.b}</div>; }
function TwoAttrs(p) { return <div children="a" children="b" />; }
function InlineSpread(p) { return <div {...{ children: "sp" }}>real</div>; }
function AttrThenSpread(p) { return <div children="x" {...p} />; }
function AttrSpreadChildren(p) { return <div children="x" {...p}>{p.a}{p.b}</div>; }
function SpreadThenAttr(p) { return <div {...p} children="x" />; }
function Getter(p) { return <i {...{ get g() { return p.a; } }} />; }
console.log(JSON.stringify({
Attr: Attr({}).p.children,
AttrExpr: AttrExpr({ a: 1, b: "x" }).p.children,
TwoAttrs: TwoAttrs({}).p.children,
InlineSpread: InlineSpread({}).p.children,
AttrThenSpread: [AttrThenSpread({ children: "q" }).p.children, AttrThenSpread({}).p.children],
AttrSpreadChildren: AttrSpreadChildren({ a: 1, b: 2, children: "q" }).p.children,
SpreadThenAttr: SpreadThenAttr({ children: "q" }).p.children,
Getter: Getter({ a: 5 }).p.g,
}));
`,
...stubReact,
},
reactCompiler: true,
target: "browser",
backend: "api",
run: {
stdout: JSON.stringify({
Attr: "y",
AttrExpr: "x",
TwoAttrs: "b",
InlineSpread: "real",
AttrThenSpread: ["q", "x"],
AttrSpreadChildren: [1, 2],
SpreadThenAttr: "x",
Getter: 5,
}),
},
onAfterBundle(api) {
const out = api.readFile("/out.js");
const body = (name: string) => {
const start = out.indexOf(`function ${name}(`);
return out.slice(start, out.indexOf("\nfunction ", start + 1));
};
// A compiled component reads its memo cache. The getter has no JSX
// attribute form, so that one component is left as written.
const compiled = [
"Attr",
"AttrExpr",
"TwoAttrs",
"InlineSpread",
"AttrThenSpread",
"AttrSpreadChildren",
"SpreadThenAttr",
"Getter",
].filter(name => /\$\[\d+\]/.test(body(name)));
expect(compiled).toEqual([
"Attr",
"AttrExpr",
"TwoAttrs",
"InlineSpread",
"AttrThenSpread",
"AttrSpreadChildren",
"SpreadThenAttr",
]);
expect(body("Getter")).toContain("get g()");
},
});

// A 0-arg call to an unknown import is non-reactive in InferReactivePlaces
// (no operand is reactive, callee isn't a hook), so its scope's deps prune
// to empty and it becomes a sentinel-only block. Babel does the same; this
Expand Down
Loading