Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
11 changes: 5 additions & 6 deletions src/bundler/defines.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,11 @@ fn env_string_store_put(
key: &[u8],
value: &[u8],
) -> Result<(), crate::Error> {
// The `E.String` slab must NOT live in the thread-local
// `Expr.Data.Store` — `configureDefines` resets that store on return, so
// the env-define payloads must outlive it. Allocate from `bump` (the
// transpiler arena) so the slab is bulk-freed with the `Define` table
// instead of leaking a `Box` per env var. Value bytes alias the long-lived
// env-map storage.
// Both the `E.String` node and its bytes go in `bump`, which lives as long
// as the `Define` table: the thread-local `Expr.Data.Store` is reset when
// `configure_defines` returns, and `value` borrows an env-map entry that
// `Bun__setEnvValue` (`process.env.HTTPS_PROXY = ..`) frees on overwrite.
Comment thread
robobun marked this conversation as resolved.
Outdated
let value: &[u8] = bump.alloc_slice_copy(value);
let value: ExprData = ExprData::EString(bun_ast::StoreRef::from_bump(
bump.alloc(bun_ast::E::EString::init(value)),
));
Expand Down
56 changes: 55 additions & 1 deletion test/bake/dev/bundle.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Bundle tests are tests concerning bundling bugs that only occur in DevServer.
import { expect } from "bun:test";
import { devTest, emptyHtmlFile, minimalFramework } from "../bake-harness";
import { Dev, devTest, emptyHtmlFile, minimalFramework } from "../bake-harness";

devTest("import identifier doesnt get renamed", {
framework: minimalFramework,
Expand Down Expand Up @@ -865,3 +865,57 @@ devTest("barrel optimization: namespace re-export cycle through a star-exported
await c.expectMessage("result: object Y KEEP DEEP OTHER");
},
});

// With `[serve.static] env = "inline"`, DevServer builds its define table from
// the VM's env map once at startup and keeps it for the life of the server.
// Assigning a proxy variable on process.env replaces that variable's entry in
// the env map, so the define must own a copy of the value instead of pointing
// into the map; otherwise every rebuild of a module that reads the variable
// inlines freed memory.
const startupProxy = "http://proxy-at-startup.example:8080/" + Buffer.alloc(120, "a").toString();
async function clientBundle(dev: Dev) {
const html = await dev.fetch("/").text();
const scripts = [...html.matchAll(/src="([^"]+\.js)"/g)];
expect(scripts).toHaveLength(1);
return dev.fetch(scripts[0][1]).text();
}
devTest("inlined env var survives a runtime process.env write to a proxy variable", {
files: {
"bunfig.toml": `
[serve.static]
env = "inline"
`,
"index.html": emptyHtmlFile({ scripts: ["index.ts"] }),
"index.ts": `console.log("v1", process.env.HTTPS_PROXY);`,
"bun.app.ts": `
import html from "./index.html";
export default {
static: { "/": html },
fetch(req) {
if (new URL(req.url).pathname === "/set-proxy") {
process.env.HTTPS_PROXY = "http://changed-at-runtime.example:1/";
return new Response("ok");
}
return new Response("Not Found", { status: 404 });
},
};
`,
},
htmlFiles: [],
env: { HTTPS_PROXY: startupProxy },
async test(dev) {
const before = await clientBundle(dev);
expect(before).toContain('"v1"');
expect(before).toContain(startupProxy);

await dev.fetch("/set-proxy").equals("ok");

// Rebuilding index.ts prints the define again, now that the env map entry
// it was created from is gone.
await dev.write("index.ts", `console.log("v2", process.env.HTTPS_PROXY);`);
const after = await clientBundle(dev);
expect(after).toContain('"v2"');
expect(after).toContain(startupProxy);
expect(after).not.toContain("changed-at-runtime");
},
});
78 changes: 77 additions & 1 deletion test/bundler/bundler_env.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { describe } from "bun:test";
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
import { itBundled } from "./expectBundled";

for (let backend of ["api", "cli"] as const) {
Expand Down Expand Up @@ -118,3 +119,78 @@ for (let backend of ["api", "cli"] as const) {
});
});
}

// The define table is built from the env map when the build is configured.
// Assigning one of the proxy variables on process.env (the only process.env
// writes that reach the native env map) replaces the map entry that define was
// built from, so the define has to own its value: here that write happens
// while the build is still running, from a macro and from a plugin.
const proxyAtStart = "http://proxy-at-start.example:8080/" + Buffer.alloc(120, "a").toString();

describe("bundler/cli", () => {
itBundled("env/inline survives macro proxy write", {
backend: "cli",
dotenv: "inline",
env: { HTTPS_PROXY: proxyAtStart },
files: {
"/a.ts": /* ts */ `
import { setProxy } from "./macro.ts" with { type: "macro" };
setProxy();
console.log(process.env.HTTPS_PROXY);
`,
"/macro.ts": /* ts */ `
export function setProxy() {
process.env.HTTPS_PROXY = "http://changed-by-macro.example:1/";
return 0;
}
`,
},
onAfterBundle(api) {
api.expectFile("/out.js").toContain(proxyAtStart);
api.expectFile("/out.js").not.toContain("changed-by-macro");
},
run: {
env: { HTTPS_PROXY: "http://not-inlined.example:1/" },
stdout: proxyAtStart + "\n",
},
});
});

describe("bundler/api", () => {
test.concurrent("env: inline survives a plugin assigning a proxy variable during the build", async () => {
using dir = tempDir("bundler-env-inline-plugin-proxy", {
"entry.ts": `export const replaced = "by the plugin";`,
"build.ts": /* ts */ `
const result = await Bun.build({
entrypoints: ["./entry.ts"],
env: "inline",
plugins: [{
name: "assign-proxy",
setup(build) {
build.onLoad({ filter: /entry\\.ts$/ }, () => {
process.env.HTTPS_PROXY = "http://changed-by-plugin.example:1/";
return { loader: "ts", contents: "export const proxy = process.env.HTTPS_PROXY;" };
});
},
}],
});
if (!result.success) throw new AggregateError(result.logs, "build failed");
process.stdout.write(await result.outputs[0].text());
`,
});

await using proc = Bun.spawn({
cmd: [bunExe(), "build.ts"],
cwd: String(dir),
env: { ...bunEnv, HTTPS_PROXY: proxyAtStart },
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stdout).toContain(`var proxy = ${JSON.stringify(proxyAtStart)};`);
expect(stdout).not.toContain("changed-by-plugin");
expect(stderr).toBe("");
expect(exitCode).toBe(0);
});
});
Loading