Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
9 changes: 7 additions & 2 deletions src/bundler/defines.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,13 @@ fn env_string_store_put(
// `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.
// instead of leaking a `Box` per env var.
//
// The bytes are copied into `bump` as well: `value` borrows a `Box<[u8]>`
// inside the env map, which `Bun__setEnvValue` replaces (freeing the old
// box) every time JS assigns one of the proxy vars, while this `Define`
// table lives as long as the transpiler (the whole DevServer in bake).
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);
});
});