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
25 changes: 15 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,18 @@ fn arena_utf8_from_utf16(
Ok(StoreStr::new(buf.leak()))
}

/// The rope's bytes, concatenated into a fresh arena slice.
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 +1495,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
5 changes: 2 additions & 3 deletions src/react_compiler/lowering/build_hir/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1112,11 +1112,10 @@ pub(super) fn lower_object_property_key(
computed: bool,
) -> Result<Option<ObjectPropertyKey>, CompilerError> {
match &key.data {
Data::EString(s) => {
// A rope (a folded `["a" + "b"]` key) takes the computed arm below.
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
107 changes: 107 additions & 0 deletions test/bundler/transpiler/react-compiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1168,6 +1168,113 @@ describe("bundler", () => {
},
});

// 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 foldedBothEnds = \`p\${"q"}\${id}r\${"s"}\`;
const joined = \`\${id}/one\` + \`two/\${id}\`;
const emptyHead = \`\${Route.Users}/\${id}\`;
const numberInHead = \`n\${1 + 2}m/\${id}\`;
const numberInTail = \`\${id}:\${2 * 3}:\${id}\`;
return (
<a
href={head}
data-tail={tail}
data-fh={foldedHead}
data-ft={foldedTail}
data-fb={foldedBothEnds}
data-j={joined}
data-e={emptyHead}
data-nh={numberInHead}
data-nt={numberInTail}
>
{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: [
JSON.stringify({
"href": "prefix/7",
"data-tail": "7/middle",
"data-fh": "abc/7",
"data-ft": "7/xyz",
"data-fb": "pq7rs",
"data-j": "7/onetwo/7",
"data-e": "users/7",
"data-nh": "n3m/7",
"data-nt": "7:6: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`\)/);
},
});

// A temporary that has to survive as a variable is "promoted": the compiler
// names it `#t<n>` (or `#T<n>` for a JSX tag, which has to be capitalised to
// read as a component) after its declaration id, and the printer drops the
Expand Down
Loading