Files
bun.sh/test/bake/dev/server-sourcemap.test.ts
Zack Radisic a89e61fcaa ssg 3 (#22138)
### What does this PR do?

Fixes a crash related to the dev server overwriting the uws user context
pointer when setting abort callback.

Adds support for `return new Response(<jsx />, { ... })` and `return
Response.render(...)` and `return Response.redirect(...)`:
- Created a `SSRResponse` class to handle this (see
`JSBakeResponse.{h,cpp}`)
- `SSRResponse` is designed to "fake" being a React component 
- This is done in JSBakeResponse::create inside of
src/bun.js/bindings/JSBakeResponse.cpp
- And `src/js/builtins/BakeSSRResponse.ts` defines a `wrapComponent`
function which wraps
the passed in component (when doing `new Response(<jsx />, ...)`). It
does
    this to throw an error (in redirect()/render() case) or return the
    component.
- Created a `BakeAdditionsToGlobal` struct which contains some
properties
    needed for this
- Added some of the properties we need to fake to BunBuiltinNames.h
(e.g.
    `$$typeof`), the rationale behind this is that we couldn't use
`structure->addPropertyTransition` because JSBakeResponse is not a final
    JSObject.
- When bake and server-side, bundler rewrites `Response ->
Bun.SSRResponse` (see `src/ast/P.zig` and `src/ast/visitExpr.zig`)
- Created a new WebCore body variant (`Render: struct { path: []const u8
}`)
  - Created when `return Response.render(...)`
  - When handled, it re-invokes dev server to render the new path

Enables server-side sourcemaps for the dev server:
- New source providers for server-side:
(`DevServerSourceProvider.{h,cpp}`)
- IncrementalGraph and SourceMapStore are updated to support this

There are numerous other stuff:
- allow `app` configuration from Bun.serve(...)
- fix errors stopping dev server
- fix use after free related to in
RequestContext.finishRunningErrorHandler
- Request.cookies
- Make `"use client";` components work
- Fix some bugs using `require(...)` in dev server
- Fix catch-all routes not working in the dev server
- Updates `findSourceMappingURL(...)` to use `std.mem.lastIndexOf(...)`
because
  the sourcemap that should be used is the last one anyway

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Alistair Smith <hi@alistair.sh>
2025-09-30 05:26:32 -07:00

150 lines
4.5 KiB
TypeScript

import { expect } from "bun:test";
import { devTest } from "../bake-harness";
devTest("server-side source maps show correct error lines", {
files: {
"pages/[...slug].tsx": `export default async function MyPage(params) {
myFunc();
return <h1>{JSON.stringify(params)}</h1>;
}
function myFunc() {
throw new Error("Test error for source maps!");
}
export async function getStaticPaths() {
return {
paths: [
{
params: {
slug: ["test-error"],
},
},
],
};
}`,
},
framework: "react",
async test(dev) {
// Make a request that will trigger the error
await dev.fetch("/test-error").catch(() => {});
// The output we saw shows the stack trace with correct source mapping
// We need to check that the error shows the right file:line:column
const lines = dev.output.lines.join("\n");
// Check that we got the error
expect(lines).toContain("Test error for source maps!");
// Check that the stack trace shows correct file and line numbers
// The source maps are working if we see the correct patterns
// We need to check for the patterns because ANSI codes might be embedded
// Strip ANSI codes for cleaner checking
const cleanLines = lines.replace(/\x1b\[[0-9;]*m/g, "");
const hasCorrectThrowLine = cleanLines.includes("myFunc") && cleanLines.includes("6:16");
// const hasCorrectCallLine = cleanLines.includes("MyPage") && cleanLines.includes("2") && cleanLines.includes("3");
const hasCorrectFileName = cleanLines.includes("pages/[...slug].tsx");
expect(hasCorrectThrowLine).toBe(true);
// TODO: renable this when async stacktraces are enabled?
// expect(hasCorrectCallLine).toBe(true);
expect(hasCorrectFileName).toBe(true);
},
timeoutMultiplier: 2, // Give more time for the test
});
devTest("server-side source maps work with HMR updates", {
files: {
"pages/error-page.tsx": `export default function ErrorPage() {
return <div>Initial content</div>;
}
export async function getStaticPaths() {
return {
paths: [{ params: {} }],
};
}`,
},
framework: "react",
async test(dev) {
// First fetch should work
const response1 = await dev.fetch("/error-page");
expect(response1.status).toBe(200);
expect(await response1.text()).toContain("Initial content");
// Update the file to throw an error
await dev.write(
"pages/error-page.tsx",
`export default function ErrorPage() {
throwError();
return <div>Updated content</div>;
}
function throwError() {
throw new Error("HMR error test");
}
export async function getStaticPaths() {
return {
paths: [{ params: {} }],
};
}`,
);
await Promise.all([dev.fetch("/error-page").catch(() => {}), dev.output.waitForLine(/HMR error test/)]);
// Check source map points to correct lines after HMR
const lines = dev.output.lines.join("\n");
// Strip ANSI codes for cleaner checking
const cleanLines = lines.replace(/\x1b\[[0-9;]*m/g, "");
const hasCorrectThrowLine = cleanLines.includes("throwError") && cleanLines.includes("6:1");
const hasCorrectCallLine = cleanLines.includes("ErrorPage") && cleanLines.includes("1:16");
expect(hasCorrectThrowLine).toBe(true);
expect(hasCorrectCallLine).toBe(true);
},
});
devTest("server-side source maps handle nested imports", {
files: {
"pages/nested.tsx": `import { doSomething } from "../lib/utils";
export default function NestedPage() {
const result = doSomething();
return <div>{result}</div>;
}
export async function getStaticPaths() {
return {
paths: [{ params: {} }],
};
}`,
"lib/utils.ts": `export function doSomething() {
return helperFunction();
}
function helperFunction() {
throw new Error("Nested error");
}`,
},
framework: "react",
async test(dev) {
await Promise.all([dev.fetch("/nested").catch(() => {}), dev.output.waitForLine(/Nested error/)]);
// Check that stack trace shows both files with correct lines
const lines = dev.output.lines.join("\n");
// Strip ANSI codes for cleaner checking
const cleanLines = lines.replace(/\x1b\[[0-9;]*m/g, "");
const hasUtilsThrowLine = cleanLines.includes("helperFunction") && cleanLines.includes("5:1");
const hasUtilsCallLine = cleanLines.includes("doSomething2") && cleanLines.includes("1:28");
const hasPageCallLine = cleanLines.includes("NestedPage") && cleanLines.includes("3:38");
expect(hasUtilsThrowLine).toBe(true);
expect(hasUtilsCallLine).toBe(true);
expect(hasPageCallLine).toBe(true);
},
});