diff --git a/src/js_parser/p.rs b/src/js_parser/p.rs
index 2383804eb65e..52f84320f52d 100644
--- a/src/js_parser/p.rs
+++ b/src/js_parser/p.rs
@@ -294,8 +294,7 @@ pub struct P<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> {
///
/// We need to declare this `response_ref` upfront
pub(crate) response_ref: Ref,
- /// We also need to declare the namespace ref for `bun:app` and attach
- /// it to the symbol so the code generated `e_import_identifier`'s
+ /// The namespace ref for `bun:app`. Only hot reloading attaches it to `response_ref`.
pub(crate) bun_app_namespace_ref: Ref,
/// Used to track the `feature` function from `import { feature } from "bun:bundle"`.
@@ -2426,7 +2425,6 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
symbol.namespace_alias.as_mut().unwrap().import_record_index = import_record_i;
}
- self.is_import_item.insert(self.response_ref, ());
self.named_imports.put(
self.response_ref,
js_ast::NamedImport {
@@ -3504,13 +3502,17 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
self.declare_common_js_symbol(js_ast::symbol::Kind::Import, b"Response")?;
self.bun_app_namespace_ref =
self.new_symbol(js_ast::symbol::Kind::Other, b"import_bun_app");
- let symbol = &mut self.symbols[self.response_ref.inner_index() as usize];
- symbol.namespace_alias = Some(bun_alloc::ast_box(js_ast::NamespaceAlias {
- namespace_ref: self.bun_app_namespace_ref,
- alias: js_ast::StoreStr::new(b"Response"),
- was_originally_property_access: false,
- import_record_index: u32::MAX,
- }));
+ if self.options.features.hot_module_reloading {
+ let symbol = &mut self.symbols[self.response_ref.inner_index() as usize];
+ symbol.namespace_alias = Some(bun_alloc::ast_box(js_ast::NamespaceAlias {
+ namespace_ref: self.bun_app_namespace_ref,
+ alias: js_ast::StoreStr::new(b"Response"),
+ was_originally_property_access: false,
+ import_record_index: u32::MAX,
+ }));
+ } else {
+ self.is_import_item.insert(self.response_ref, ());
+ }
}
}
}
diff --git a/src/js_parser/scan/scan_imports.rs b/src/js_parser/scan/scan_imports.rs
index c09ce48f911e..1a682b819765 100644
--- a/src/js_parser/scan/scan_imports.rs
+++ b/src/js_parser/scan/scan_imports.rs
@@ -480,6 +480,15 @@ impl<'a> ImportScanner<'a> {
let name: LocRef = item.name;
let name_ref = name.ref_;
+ // Only the linker declares the namespace that an alias reads.
+ debug_assert!(
+ HOT_MODULE_RELOADING_TRANSFORMATIONS
+ || p.symbols[name_ref.inner_index() as usize]
+ .namespace_alias
+ .is_none(),
+ "import clause item has a namespace alias before linking"
+ );
+
p.named_imports.put_assume_capacity(
name_ref,
js_ast::NamedImport {
diff --git a/test/bake/dev/production.test.ts b/test/bake/dev/production.test.ts
index 76d04d2e876a..925c3a71e87d 100644
--- a/test/bake/dev/production.test.ts
+++ b/test/bake/dev/production.test.ts
@@ -691,4 +691,42 @@ export default function IndexPage() {
expect(indexHtml).toContain("readyState=" + WebSocket.CONNECTING);
expect(buildProc.exitCode).toBe(0);
});
+
+ test("a page can read the global Response", async () => {
+ const dir = await tempDirWithBakeDeps("bake-production-response", {
+ "src/index.tsx": `export default { app: { framework: "react" } };`,
+ "pages/index.tsx": `export default function IndexPage() {
+ const response = new Response("x");
+ const redirect = Response.redirect("/login", 302);
+ let render;
+ try {
+ Response.render("/404");
+ } catch (error) {
+ render = error.message;
+ }
+ return
status={response.status} redirect={redirect.headers.get("location")} render={render}
;
+}`,
+ "package.json": JSON.stringify({
+ "name": "test-app",
+ "version": "1.0.0",
+ "devDependencies": {
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
+ },
+ }),
+ });
+
+ const buildProc = await Bun.$`${bunExe()} build --app ./src/index.tsx --outdir ./dist`
+ .cwd(dir)
+ .env(bunEnv)
+ .throws(false);
+
+ const indexHtml = await Bun.file(path.join(dir, "dist", "index.html"))
+ .text()
+ .catch(() => buildProc.stderr.toString());
+ expect(indexHtml).toContain(
+ "status=200 redirect=/login render=Response.render() is only available in the Bun dev server",
+ );
+ expect(buildProc.exitCode).toBe(0);
+ });
});
diff --git a/test/bake/dev/response-to-bake-response.test.ts b/test/bake/dev/response-to-bake-response.test.ts
index 7e99be1236da..524f0c7853ed 100644
--- a/test/bake/dev/response-to-bake-response.test.ts
+++ b/test/bake/dev/response-to-bake-response.test.ts
@@ -45,10 +45,11 @@ test("Response -> import { Response } from 'bun:app' transform in server compone
// Check that Response import was added from 'bun:app'
expect(serverResult).toContain('import { Response } from "bun:app"');
- // Response is transformed to import_bun_app.Response
- expect(serverResult).toContain("new import_bun_app.Response");
- expect(serverResult).toContain("import_bun_app.Response.redirect");
- expect(serverResult).toContain("import_bun_app.Response.render");
+ // Each read of Response is a read of the imported binding
+ expect(serverResult).toContain('= new Response("Hello"');
+ expect(serverResult).toContain('return Response.redirect("/login")');
+ expect(serverResult).toContain('return Response.render("/404")');
+ expect(serverResult).not.toContain("import_bun_app");
// Build client component (should not have the transform)
const clientResult = await Bun.$`${bunExe()} build ${path.join(dir, "client-component.js")} --target=browser`
@@ -94,11 +95,12 @@ test("Response import is added for global Response in various contexts", async (
// Check that import was added
expect(result).toContain('import { Response } from "bun:app"');
- // Response is transformed to import_bun_app.Response
- expect(result).toContain("new import_bun_app.Response");
- expect(result).toContain("instanceof import_bun_app.Response");
- expect(result).toContain("import_bun_app.Response.prototype.status");
- expect(result).toContain("import_bun_app.Response.json");
+ // Each read of Response is a read of the imported binding
+ expect(result).toContain("= new Response");
+ expect(result).toContain("obj instanceof Response");
+ expect(result).toContain("status = Response.prototype.status");
+ expect(result).toContain("json = Response.json(");
+ expect(result).not.toContain("import_bun_app");
});
test("Response import is not added when Response is already imported or shadowed", async () => {
@@ -215,7 +217,8 @@ test("Response import is NOT added in client components", async () => {
// Server component should have import from bun:app
expect(serverResult).toContain('import { Response } from "bun:app"');
- expect(serverResult).toContain("new import_bun_app.Response");
+ expect(serverResult).toContain('= new Response("Server"');
+ expect(serverResult).not.toContain("import_bun_app");
});
test("Response import is added when Response is global, but not when shadowed", async () => {
@@ -242,8 +245,170 @@ test("Response import is added when Response is global, but not when shadowed",
expect(serverResult).toContain('import { Response } from "bun:app"');
// Local shadowed Response should not be affected
expect(serverResult).toContain('new "ooga booga!"');
- // Global Response is transformed to import_bun_app.Response
- expect(serverResult).toContain("var lmao = new import_bun_app.Response");
+ // The global Response is a read of the imported binding
+ expect(serverResult).toContain("var lmao = new Response");
+ expect(serverResult).not.toContain("import_bun_app");
+});
+
+describe.concurrent("the built output runs", () => {
+ // The Response of "bun:app" is not the global one. It extends it and adds `render`.
+ const reads = `Response === globalThis.Response, new Response("x") instanceof globalThis.Response, typeof Response.render`;
+
+ async function buildAndRun(files: Record, { args = [] as string[], entries = ["./entry.js"] } = {}) {
+ using dir = tempDir("response-run", files);
+ await using build = Bun.spawn({
+ cmd: [bunExe(), "build", "--server-components", "--target=bun", ...args, ...entries, "--outdir", "out"],
+ env: bunEnv,
+ cwd: String(dir),
+ stdout: "ignore",
+ stderr: "pipe",
+ });
+ const [buildStderr, buildExitCode] = await Promise.all([build.stderr.text(), build.exited]);
+ expect(buildStderr).toBe("");
+ expect(buildExitCode).toBe(0);
+
+ await using proc = Bun.spawn({
+ cmd: [bunExe(), "out/entry.js"],
+ env: bunEnv,
+ cwd: String(dir),
+ stdout: "pipe",
+ stderr: "pipe",
+ });
+ const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
+ const output = await Bun.file(path.join(dir, "out", "entry.js")).text();
+ return { output, result: { stdout, stderr, exitCode } };
+ }
+
+ test("esm", async () => {
+ const { output, result } = await buildAndRun({
+ "entry.js": `
+ export default function Page() {
+ return new Response("x");
+ }
+ console.log(${reads}, Page() instanceof Response);
+ `,
+ });
+ expect(result).toEqual({ stdout: "false true function true\n", stderr: "", exitCode: 0 });
+ expect(output).toContain('import { Response } from "bun:app"');
+ });
+
+ test("Response.redirect() and Response.render()", async () => {
+ const { result } = await buildAndRun({
+ "entry.js": `
+ const response = Response.redirect("/login", 302);
+ console.log(response instanceof globalThis.Response, response.status, response.headers.get("location"));
+ try {
+ Response.render("/404");
+ } catch (error) {
+ console.log(error.message);
+ }
+ `,
+ });
+ expect(result).toEqual({
+ stdout: "true 302 /login\nResponse.render() is only available in the Bun dev server\n",
+ stderr: "",
+ exitCode: 0,
+ });
+ });
+
+ // The linker turns the read into `import_bun_app.Response` here, and declares `import_bun_app`.
+ // It does that only for a read that is an import item.
+ test("cjs", async () => {
+ const { output, result } = await buildAndRun({ "entry.js": `console.log(${reads});` }, { args: ["--format=cjs"] });
+ expect(result).toEqual({ stdout: "false true function\n", stderr: "", exitCode: 0 });
+ expect(output).toContain('var import_bun_app = require("bun:app")');
+ });
+
+ // The same conversion as cjs, with `__require` in place of `require`.
+ test("iife", async () => {
+ const { output, result } = await buildAndRun({ "entry.js": `console.log(${reads});` }, { args: ["--format=iife"] });
+ expect(result).toEqual({ stdout: "false true function\n", stderr: "", exitCode: 0 });
+ expect(output).toContain('var import_bun_app = __require("bun:app")');
+ });
+
+ test("two files that read Response, minified", async () => {
+ const { result } = await buildAndRun(
+ {
+ "entry.js": `
+ import { make } from "./other.js";
+ console.log(${reads}, make() instanceof Response);
+ `,
+ "other.js": `
+ export function make() {
+ return new Response("x");
+ }
+ `,
+ },
+ { args: ["--minify"] },
+ );
+ expect(result).toEqual({ stdout: "false true function true\n", stderr: "", exitCode: 0 });
+ });
+
+ test("a chunk shared by two entry points", async () => {
+ const { output, result } = await buildAndRun(
+ {
+ "entry.js": `
+ import { make } from "./shared.js";
+ console.log(${reads}, make() instanceof Response);
+ `,
+ "second.js": `
+ import { make } from "./shared.js";
+ console.log(make() instanceof Response);
+ `,
+ "shared.js": `
+ export function make() {
+ return new Response("x");
+ }
+ `,
+ },
+ { args: ["--splitting"], entries: ["./entry.js", "./second.js"] },
+ );
+ expect(result).toEqual({ stdout: "false true function true\n", stderr: "", exitCode: 0 });
+ expect(output).not.toContain("function make");
+ });
+
+ test("a re-export of Response", async () => {
+ const { output, result } = await buildAndRun({
+ "entry.js": `
+ import { Response as Named } from "./lib.js";
+ import * as ns from "./lib.js";
+ console.log(Named === globalThis.Response, ns.Response === Named, typeof Named.render, Object.keys(ns));
+ `,
+ "lib.js": `export { Response };`,
+ });
+ expect(result).toEqual({ stdout: 'false true function [ "Response" ]\n', stderr: "", exitCode: 0 });
+ expect(output).toContain('import { Response } from "bun:app"');
+ });
+
+ test("a CommonJS file", async () => {
+ const { output, result } = await buildAndRun({
+ "entry.js": `
+ module.exports = function Page() {
+ return new Response("x");
+ };
+ console.log(${reads}, module.exports() instanceof Response);
+ `,
+ });
+ expect(result).toEqual({ stdout: "false true function true\n", stderr: "", exitCode: 0 });
+ expect(output).toContain("__commonJS");
+ });
+
+ // The bundler keeps this statement, but the read is not counted as a use.
+ test("a read in dead code adds no import", async () => {
+ const { output, result } = await buildAndRun({
+ "entry.js": `
+ if (false) {
+ switch (new Response("x")) {
+ case 1:
+ }
+ }
+ console.log("ok");
+ `,
+ });
+ expect(output).toContain('switch (new Response("x"))');
+ expect(output).not.toContain("bun:app");
+ expect(result).toEqual({ stdout: "ok\n", stderr: "", exitCode: 0 });
+ });
});
// Only the dev server sets the AsyncLocalStorage instance that these calls read.