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
22 changes: 12 additions & 10 deletions src/js_parser/p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"`.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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, ());
Comment thread
robobun marked this conversation as resolved.
}
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/js_parser/scan/scan_imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
38 changes: 38 additions & 0 deletions test/bake/dev/production.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <div>status={response.status} redirect={redirect.headers.get("location")} render={render}</div>;
}`,
"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);
});
});
189 changes: 177 additions & 12 deletions test/bake/dev/response-to-bake-response.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 () => {
Expand All @@ -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<string, string>, { 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 () => {
Comment thread
robobun marked this conversation as resolved.
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.
Expand Down
Loading