Skip to content
Open
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
29 changes: 19 additions & 10 deletions src/react_compiler/lowering/build_hir/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1273,6 +1273,8 @@ fn convert_template_contents(
E::TemplateContents::Cooked(s) => {
let cooked = if s.is_utf16 {
arena_utf8_from_utf16(s.slice16(), loc)?
} else if s.next.is_some() {
arena_str_from_rope(s)
} else {
StoreStr::new(s.slice8())
};
Expand Down Expand Up @@ -1301,6 +1303,22 @@ fn arena_utf8_from_utf16(
Ok(StoreStr::new(buf.leak()))
}

/// Flatten a rope. The parser's string folding ("a" + "b", "a" + `b${x}`,
/// `${"a"}b${x}`) links the operands through `next` instead of copying, so
/// `data` holds only the first segment. String literals and template
/// heads/tails both reach lowering in that shape; ropes are always 8-bit
/// (`EString::push` asserts it).
fn arena_str_from_rope(s: &E::EString) -> StoreStr {
let mut buf: HirVec<u8> = AstAlloc::vec_with_capacity(s.len());
let mut cur = Some(s);
while let Some(seg) = cur {
debug_assert!(!seg.is_utf16);
buf.extend_from_slice(seg.slice8());
cur = seg.next.as_ref().map(|r| r.get());
}
StoreStr::new(buf.leak())
}

// =============================================================================
// lower_reorderable_expression (build_hir.rs:6553-6713)
// =============================================================================
Expand Down Expand Up @@ -1481,16 +1499,7 @@ fn convert_js_string(s: StoreRef<E::EString>) -> JsString {
if s.get().next.is_none() {
return JsString::new(s);
}
// Roped literal (rare; only from parser-level constant folding): flatten so
// every HIR consumer can ignore ropes.
let mut joined: Vec<u8> = Vec::with_capacity(s.get().len());
let mut cur = Some(s.get());
while let Some(seg) = cur {
debug_assert!(!seg.is_utf16);
joined.extend_from_slice(seg.slice8());
cur = seg.next.as_ref().map(|r| r.get());
}
JsString::from_wtf8_bytes(&joined)
JsString::from_wtf8_bytes(arena_str_from_rope(s.get()).slice())
}

fn unsupported_node(node_type: &'static str, loc: Option<SourceLocation>) -> InstructionValue {
Expand Down
7 changes: 4 additions & 3 deletions src/react_compiler/lowering/build_hir/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1132,11 +1132,12 @@ pub(super) fn lower_object_property_key(
computed: bool,
) -> Result<Option<ObjectPropertyKey>, CompilerError> {
match &key.data {
Data::EString(s) => {
// A folded computed key (`{["a" + "b"]: x}`) arrives as a rope; it takes
// the computed arm below, where `lower_expression` flattens it, just as
// upstream lowers the unfolded `"a" + "b"` to a computed key.
Data::EString(s) if s.next.is_none() => {
let name = if s.is_utf16 {
arena_str(&bun_core::strings::to_utf8_alloc(s.slice16()))
} else if s.next.is_some() {
return Err(cold_todo("rope property key", convert_loc(key.loc)));
} else {
StoreStr::new(s.slice8())
};
Expand Down
84 changes: 84 additions & 0 deletions test/bundler/transpiler/react-compiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1063,4 +1063,88 @@ describe("bundler", () => {
expect(out).toMatch(/__MEMO_CACHE_SENTINEL\)\s*\{[^}]*globalFn\(\)/);
},
});

// The parser's constant folding (on under minify.syntax) joins strings as a
// rope: the E::String keeps only its first segment in `data` and links the
// rest through `next`. Template heads and tails come out of folding in that
// shape too, and lowering used to read `data` alone, so every segment after
// the first was dropped from the compiled output: "pre" + `fix/${id}` came
// out as `pre${id}`. A folded computed object key is the same rope and used
// to make the whole function bail out of compilation.
itBundled("react-compiler/FoldedTemplateAndKeyKeepAllSegments", {
files: {
"/entry.tsx": /* tsx */ `
const enum Route { Users = "users" }
export function Links({ id }: { id: string }) {
const head = "pre" + \`fix/\${id}\`;
const tail = \`\${id}/mid\` + "dle";
const foldedHead = \`a\${"b"}c/\${id}\`;
const foldedTail = \`\${id}/x\${"y"}z\`;
const joined = \`\${id}/one\` + \`two/\${id}\`;
const emptyHead = \`\${Route.Users}/\${id}\`;
return (
<a href={head} data-tail={tail} data-fh={foldedHead} data-ft={foldedTail} data-j={joined} data-e={emptyHead}>
{id}
</a>
);
}
export function ComputedKey({ id }: { id: string }) {
const o = { ["a" + "b"]: id };
return <a data-keys={Object.keys(o).join()}>{o.ab}</a>;
}
const { children: _a, ...links } = Links({ id: "7" }).props;
const { children: _b, ...computed } = ComputedKey({ id: "7" }).props;
console.log(JSON.stringify(links));
console.log(JSON.stringify(computed));
console.log(globalThis.memoCachesAllocated);
`,
"/node_modules/react/index.js": `module.exports = {};`,
"/node_modules/react/jsx-runtime.js": `exports.jsx = exports.jsxs = (t, p) => ({ t, props: p });`,
"/node_modules/react/jsx-dev-runtime.js": `exports.jsxDEV = (t, p) => ({ t, props: p });`,
"/node_modules/react/compiler-runtime.js": `
exports.c = n => {
globalThis.memoCachesAllocated = (globalThis.memoCachesAllocated ?? 0) + 1;
return new Array(n).fill(Symbol.for("react.memo_cache_sentinel"));
};
`,
"/node_modules/react/package.json": `{"name":"react","main":"./index.js"}`,
},
reactCompiler: true,
target: "browser",
backend: "cli",
minifySyntax: true,
run: {
stdout: [
'{"href":"prefix/7","data-tail":"7/middle","data-fh":"abc/7","data-ft":"7/xyz","data-j":"7/onetwo/7","data-e":"users/7"}',
'{"data-keys":"ab"}',
// One memo cache per component: both must have been compiled rather
// than left as written.
"2",
].join("\n"),
},
});

// import() arguments are folded even without minify.syntax, so this rope
// reaches the compiler in a default build; the emitted specifier used to lose
// its "/" and come out as `./pages${name}.js`.
itBundled("react-compiler/FoldedImportSpecifierKeepsAllSegments", {
files: {
"/entry.tsx": /* tsx */ `
const enum Dir { Pages = "./pages" }
export function Loader({ name }: { name: string }) {
const load = () => import(Dir.Pages + \`/\${name}.js\`);
return <button onClick={load}>{name}</button>;
}
`,
},
reactCompiler: true,
target: "browser",
backend: "cli",
external: ["react", "react/compiler-runtime", "react/jsx-runtime", "react/jsx-dev-runtime"],
onAfterBundle(api) {
const out = api.readFile("/out.js");
expect(out).toMatch(/\b_c\(\d+\)/);
expect(out).toMatch(/import\(`\.\/pages\/\$\{name\}\.js`\)/);
},
});
});
Loading