mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 10:58:56 +00:00
### 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>
390 lines
11 KiB
TypeScript
390 lines
11 KiB
TypeScript
// Test SSG pages router functionality
|
|
import { expect } from "bun:test";
|
|
import { devTest } from "../bake-harness";
|
|
|
|
devTest("SSG pages router - multiple static pages", {
|
|
framework: "react",
|
|
files: {
|
|
"pages/about.tsx": `
|
|
export default function AboutPage() {
|
|
return <h1>About Page</h1>;
|
|
}
|
|
`,
|
|
"pages/contact.tsx": `
|
|
export default function ContactPage() {
|
|
return <h1>Contact Page</h1>;
|
|
}
|
|
`,
|
|
},
|
|
async test(dev) {
|
|
// Test about page
|
|
await using c2 = await dev.client("/about");
|
|
expect(await c2.elemText("h1")).toBe("About Page");
|
|
|
|
// Test contact page
|
|
await using c3 = await dev.client("/contact");
|
|
expect(await c3.elemText("h1")).toBe("Contact Page");
|
|
},
|
|
});
|
|
|
|
devTest("SSG pages router - dynamic routes with [slug]", {
|
|
framework: "react",
|
|
files: {
|
|
"pages/[slug].tsx": `
|
|
type Props = Bun.SSGProps;
|
|
|
|
const Page: Bun.SSGPage = async ({ params }) => {
|
|
return (
|
|
<div>
|
|
<h1>Dynamic Page: {params.slug}</h1>
|
|
<p>Slug value: {params.slug}</p>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Page;
|
|
|
|
export const getStaticPaths: Bun.GetStaticPaths = async () => {
|
|
return {
|
|
paths: [
|
|
{ params: { slug: "first-post" } },
|
|
{ params: { slug: "second-post" } },
|
|
{ params: { slug: "third-post" } },
|
|
],
|
|
};
|
|
};
|
|
`,
|
|
},
|
|
async test(dev) {
|
|
// Test dynamic routes
|
|
await using c1 = await dev.client("/first-post");
|
|
expect(await c1.elemText("h1")).toBe("Dynamic Page: <!-- -->first-post");
|
|
expect(await c1.elemText("p")).toBe("Slug value: <!-- -->first-post");
|
|
|
|
await using c2 = await dev.client("/second-post");
|
|
expect(await c2.elemText("h1")).toBe("Dynamic Page: <!-- -->second-post");
|
|
|
|
await using c3 = await dev.client("/third-post");
|
|
expect(await c3.elemText("h1")).toBe("Dynamic Page: <!-- -->third-post");
|
|
},
|
|
});
|
|
|
|
devTest("SSG pages router - nested routes", {
|
|
framework: "react",
|
|
files: {
|
|
"pages/blog/index.tsx": `
|
|
export default function BlogIndex() {
|
|
return <h1>Blog Index</h1>;
|
|
}
|
|
`,
|
|
"pages/blog/[id].tsx": `
|
|
const BlogPost: Bun.SSGPage = ({ params }) => {
|
|
return <h1>Blog Post {params.id}</h1>;
|
|
};
|
|
|
|
export default BlogPost;
|
|
|
|
export const getStaticPaths: Bun.GetStaticPaths = async () => {
|
|
return {
|
|
paths: [
|
|
{ params: { id: "1" } },
|
|
{ params: { id: "2" } },
|
|
],
|
|
};
|
|
};
|
|
`,
|
|
"pages/blog/categories/[category].tsx": `
|
|
const CategoryPage: Bun.SSGPage = ({ params }) => {
|
|
return <h1>Category: {params.category}</h1>;
|
|
};
|
|
|
|
export default CategoryPage;
|
|
|
|
export const getStaticPaths: Bun.GetStaticPaths = async () => {
|
|
return {
|
|
paths: [
|
|
{ params: { category: "tech" } },
|
|
{ params: { category: "lifestyle" } },
|
|
],
|
|
};
|
|
};
|
|
`,
|
|
},
|
|
async test(dev) {
|
|
// Test blog index
|
|
await using c1 = await dev.client("/blog");
|
|
expect(await c1.elemText("h1")).toBe("Blog Index");
|
|
|
|
// Test blog posts
|
|
await using c2 = await dev.client("/blog/1");
|
|
expect(await c2.elemText("h1")).toBe("Blog Post <!-- -->1");
|
|
|
|
await using c3 = await dev.client("/blog/2");
|
|
expect(await c3.elemText("h1")).toBe("Blog Post <!-- -->2");
|
|
|
|
// Test categories
|
|
await using c4 = await dev.client("/blog/categories/tech");
|
|
expect(await c4.elemText("h1")).toBe("Category: <!-- -->tech");
|
|
|
|
await using c5 = await dev.client("/blog/categories/lifestyle");
|
|
expect(await c5.elemText("h1")).toBe("Category: <!-- -->lifestyle");
|
|
},
|
|
});
|
|
|
|
devTest("SSG pages router - hot reload on page changes", {
|
|
framework: "react",
|
|
files: {
|
|
"pages/index.tsx": `
|
|
export default function IndexPage() {
|
|
return <h1>Welcome to SSG</h1>;
|
|
}
|
|
`,
|
|
},
|
|
async test(dev) {
|
|
await using c = await dev.client("/");
|
|
expect(await c.elemText("h1")).toBe("Welcome to SSG");
|
|
|
|
// Update the page
|
|
await dev.write(
|
|
"pages/index.tsx",
|
|
`
|
|
export default function IndexPage() {
|
|
console.log("updated load");
|
|
return <h1>Updated Content</h1>;
|
|
}
|
|
`,
|
|
);
|
|
|
|
// this %c%s%c is a react devtools thing and I don't know how to turn it off
|
|
await c.expectMessage("%c%s%c updated load");
|
|
expect(await c.elemText("h1")).toBe("Updated Content");
|
|
},
|
|
});
|
|
|
|
devTest("SSG pages router - data fetching with async components", {
|
|
framework: "react",
|
|
files: {
|
|
"pages/data.tsx": `
|
|
async function fetchData() {
|
|
// Simulate API call
|
|
return new Promise(resolve => {
|
|
setTimeout(() => {
|
|
resolve({ message: "Data from API", items: ["Item 1", "Item 2", "Item 3"] });
|
|
}, 10);
|
|
});
|
|
}
|
|
|
|
export default async function DataPage() {
|
|
const data = await fetchData();
|
|
|
|
return (
|
|
<div>
|
|
<h1>{data.message}</h1>
|
|
<ul>
|
|
{data.items.map((item, index) => (
|
|
<li key={index}>{item}</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
);
|
|
}
|
|
`,
|
|
},
|
|
async test(dev) {
|
|
await using c = await dev.client("/data");
|
|
expect(await c.elemText("h1")).toBe("Data from API");
|
|
|
|
const items = await c.elemsText("li");
|
|
expect(items).toEqual(["Item 1", "Item 2", "Item 3"]);
|
|
},
|
|
});
|
|
|
|
devTest("SSG pages router - multiple dynamic segments", {
|
|
framework: "react",
|
|
files: {
|
|
"pages/[category]/[year]/[slug].tsx": `
|
|
const ArticlePage: Bun.SSGPage = ({ params }) => {
|
|
return (
|
|
<div>
|
|
<h1>{params.slug}</h1>
|
|
<p>Category: {params.category}</p>
|
|
<p>Year: {params.year}</p>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ArticlePage;
|
|
|
|
export const getStaticPaths: Bun.GetStaticPaths = async () => {
|
|
return {
|
|
paths: [
|
|
{ params: { category: "tech", year: "2024", slug: "bun-release" } },
|
|
{ params: { category: "news", year: "2024", slug: "breaking-story" } },
|
|
{ params: { category: "tech", year: "2023", slug: "year-review" } },
|
|
],
|
|
};
|
|
};
|
|
`,
|
|
},
|
|
async test(dev) {
|
|
// Test first path
|
|
await using c1 = await dev.client("/tech/2024/bun-release");
|
|
expect(await c1.elemText("h1")).toBe("bun-release");
|
|
expect(await c1.elemsText("p")).toEqual(["Category: <!-- -->tech", "Year: <!-- -->2024"]);
|
|
|
|
// Test second path
|
|
await using c2 = await dev.client("/news/2024/breaking-story");
|
|
expect(await c2.elemText("h1")).toBe("breaking-story");
|
|
expect(await c2.elemsText("p")).toEqual(["Category: <!-- -->news", "Year: <!-- -->2024"]);
|
|
|
|
// Test third path
|
|
await using c3 = await dev.client("/tech/2023/year-review");
|
|
expect(await c3.elemText("h1")).toBe("year-review");
|
|
expect(await c3.elemsText("p")).toEqual(["Category: <!-- -->tech", "Year: <!-- -->2023"]);
|
|
},
|
|
});
|
|
|
|
devTest("SSG pages router - file loading with Bun.file", {
|
|
framework: "react",
|
|
fixture: "ssg-pages-router",
|
|
files: {
|
|
"pages/[slug].tsx": `
|
|
import { join } from "path";
|
|
|
|
const PostPage: Bun.SSGPage = async ({ params }) => {
|
|
const content = await Bun.file(
|
|
join(process.cwd(), "posts", params.slug + ".txt")
|
|
).text();
|
|
|
|
return (
|
|
<div>
|
|
<h1>{params.slug}</h1>
|
|
<div>{content}</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default PostPage;
|
|
|
|
export const getStaticPaths: Bun.GetStaticPaths = async () => {
|
|
const glob = new Bun.Glob("**/*.txt");
|
|
const paths = [];
|
|
|
|
for (const file of Array.from(glob.scanSync({ cwd: join(process.cwd(), "posts") }))) {
|
|
const slug = file.replace(/\\.txt$/, "");
|
|
paths.push({ params: { slug } });
|
|
}
|
|
|
|
return { paths };
|
|
};
|
|
`,
|
|
"posts/hello-world.txt": "This is the content of hello world post",
|
|
"posts/second-post.txt": "This is the second post content",
|
|
},
|
|
async test(dev) {
|
|
// Test first post
|
|
await using c1 = await dev.client("/hello-world");
|
|
expect(await c1.elemText("h1")).toBe("hello-world");
|
|
expect(await c1.elemText("div div")).toBe("This is the content of hello world post");
|
|
|
|
// Test second post
|
|
await using c2 = await dev.client("/second-post");
|
|
expect(await c2.elemText("h1")).toBe("second-post");
|
|
expect(await c2.elemText("div div")).toBe("This is the second post content");
|
|
},
|
|
});
|
|
|
|
devTest("SSG pages router - named import edge case", {
|
|
framework: "react",
|
|
fixture: "ssg-pages-router",
|
|
files: {
|
|
"pages/index.tsx": `
|
|
import Markdoc, * as md from '../src/ooga'
|
|
|
|
console.log(md);
|
|
|
|
export default function IndexPage() {
|
|
return <h1>Welcome to SSG</h1>;
|
|
}
|
|
`,
|
|
"src/ooga.ts": `var Markdoc = function () {
|
|
return {
|
|
parse: () => {},
|
|
transform: () => {},
|
|
};
|
|
};
|
|
|
|
export { Markdoc as default };`,
|
|
"posts/hello-world.txt": "This is the content of hello world post",
|
|
"posts/second-post.txt": "This is the second post content",
|
|
},
|
|
async test(dev) {
|
|
// Should not error
|
|
await using c1 = await dev.client("/");
|
|
expect(await c1.elemText("h1")).toBe("Welcome to SSG");
|
|
},
|
|
});
|
|
|
|
devTest("SSG pages router - catch-all routes [...slug]", {
|
|
framework: "react",
|
|
files: {
|
|
"pages/[...slug].tsx": `
|
|
const CatchAllPage: Bun.SSGPage = ({ params }) => {
|
|
return (
|
|
<div>
|
|
<h1>Catch-all Route</h1>
|
|
<p id="params">{JSON.stringify(params)}</p>
|
|
<ul>
|
|
{params.slug && Array.isArray(params.slug) ? (
|
|
params.slug.map((segment, index) => (
|
|
<li key={index}>{segment}</li>
|
|
))
|
|
) : (
|
|
<li>No slug array</li>
|
|
)}
|
|
</ul>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default CatchAllPage;
|
|
|
|
export const getStaticPaths: Bun.GetStaticPaths = async () => {
|
|
return {
|
|
paths: [
|
|
{ params: { slug: ["docs"] } },
|
|
{ params: { slug: ["docs", "getting-started"] } },
|
|
{ params: { slug: ["docs", "api", "reference"] } },
|
|
{ params: { slug: ["blog", "2024", "january", "new-features"] } },
|
|
],
|
|
};
|
|
};
|
|
`,
|
|
},
|
|
async test(dev) {
|
|
// Test single segment
|
|
await using c1 = await dev.client("/docs");
|
|
expect(await c1.elemText("h1")).toBe("Catch-all Route");
|
|
expect(await c1.elemText("#params")).toBe('{"slug":"docs"}');
|
|
expect(await c1.elemsText("li")).toEqual(["No slug array"]);
|
|
|
|
// Test two segments
|
|
await using c2 = await dev.client("/docs/getting-started");
|
|
expect(await c2.elemText("h1")).toBe("Catch-all Route");
|
|
expect(await c2.elemText("#params")).toBe('{"slug":["docs","getting-started"]}');
|
|
expect(await c2.elemsText("li")).toEqual(["docs", "getting-started"]);
|
|
|
|
// Test three segments
|
|
await using c3 = await dev.client("/docs/api/reference");
|
|
expect(await c3.elemText("h1")).toBe("Catch-all Route");
|
|
expect(await c3.elemText("#params")).toBe('{"slug":["docs","api","reference"]}');
|
|
expect(await c3.elemsText("li")).toEqual(["docs", "api", "reference"]);
|
|
|
|
// Test four segments
|
|
await using c4 = await dev.client("/blog/2024/january/new-features");
|
|
expect(await c4.elemText("h1")).toBe("Catch-all Route");
|
|
expect(await c4.elemText("#params")).toBe('{"slug":["blog","2024","january","new-features"]}');
|
|
expect(await c4.elemsText("li")).toEqual(["blog", "2024", "january", "new-features"]);
|
|
},
|
|
});
|