diff --git a/src/ast/expr.rs b/src/ast/expr.rs index cfedeb4f9bf8..a26cc90151a4 100644 --- a/src/ast/expr.rs +++ b/src/ast/expr.rs @@ -1039,7 +1039,7 @@ impl IntoExprData for &E::EString { impl Expr { /// When the lifetime of an Expr.Data's pointer must exist longer than reset() is called, use this function. /// Be careful to free the memory (or use an arena that does it for you) - /// Also, prefer Expr.init or Expr.alloc when possible. This will be slower. + /// Prefer `Expr::init` for nodes that only need to live until the current parse's reset(). #[inline] pub fn allocate(bump: &Bump, st: T, loc: Loc) -> Expr { data::Store::assert(); diff --git a/src/runtime/api/JSTranspiler.rs b/src/runtime/api/JSTranspiler.rs index df5d4bc5f529..d3327a117f5c 100644 --- a/src/runtime/api/JSTranspiler.rs +++ b/src/runtime/api/JSTranspiler.rs @@ -878,6 +878,9 @@ fn export_replacement_value( global: &JSGlobalObject, arena: &Arena, ) -> JsResult> { + // The result outlives every parse, so it must not be built in the thread-local store. + let _guard = bun_ast::expr::Disabler::scope(); + if value.is_boolean() { return Ok(Some(Expr { data: bun_ast::ExprData::EBoolean(bun_ast::E::Boolean { @@ -916,7 +919,8 @@ fn export_replacement_value( // `E::EString::init` erases the borrow to `'static` per the AST // crate's `Str` convention (see ast/E.rs). let data = arena.alloc_slice_copy(utf8.slice()); - return Ok(Some(Expr::init( + return Ok(Some(Expr::allocate( + arena, bun_ast::E::EString::init(data), bun_ast::Loc::EMPTY, ))); diff --git a/test/bundler/transpiler/transpiler.test.js b/test/bundler/transpiler/transpiler.test.js index 2ec82493d4ff..5eca13297c30 100644 --- a/test/bundler/transpiler/transpiler.test.js +++ b/test/bundler/transpiler/transpiler.test.js @@ -2404,6 +2404,64 @@ export default class { expect(output.includes("localVarToReplace")).toBe(true); expect(output.includes("localVarToRemove")).toBe(false); }); + + it("string replacement values survive other modules being loaded after the transpiler is created", async () => { + // Release builds only reuse the AST store slot after a few dozen string declarations. + const resetLines = []; + for (let i = 0; i < 300; i++) { + resetLines.push(`const s${i} = "other string ${i}";`); + } + resetLines.push("module.exports = { s0, s299 };"); + + using dir = tempDir("transpiler-replace-string-values", { + "reset.cjs": resetLines.join("\n"), + "reset.json": `{}`, + "entry.mjs": ` + const transpiler = new Bun.Transpiler({ + exports: { + replace: { + foo: "bar", + getStaticProps: ["__N_SSG", "ssg"], + default: "dflt", + }, + }, + }); + + require("./reset.cjs"); + await import("./reset.json"); + + console.log( + JSON.stringify([ + transpiler.transformSync("export const foo = 1;"), + transpiler.transformSync("export function getStaticProps() {}"), + transpiler.transformSync("export default 1;"), + await transpiler.transform("export const foo = 1;"), + await transpiler.transform("export function getStaticProps() {}"), + await transpiler.transform("export default 1;"), + ]), + ); + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "entry.mjs"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(JSON.parse(stdout)).toEqual([ + 'export const foo = "bar";\n', + 'export var __N_SSG = "ssg";\n', + 'export default "dflt";\n', + 'export const foo = "bar";\n', + 'export var __N_SSG = "ssg";\n', + 'export default "dflt";\n', + ]); + expect(exitCode).toBe(0); + }); }); const bunTranspiler = new Bun.Transpiler({