import { describe, expect, test } from "bun:test"; import { existsSync } from "fs"; import { bunEnv, bunExe } from "harness"; import path from "path"; import { tempDirWithBakeDeps } from "../bake-harness"; const normalizePath = (path: string) => (process.platform === "win32" ? path.replaceAll("\\", "/") : path); const platformPath = (path: string) => (process.platform === "win32" ? path.replaceAll("/", "\\") : path); /** * Production build tests */ describe("production", () => { test("works with sourcemaps - error thrown in React component", async () => { const dir = await tempDirWithBakeDeps("bake-production-sourcemap", { "src/index.tsx": `export default { app: { framework: "react" } };`, "pages/index.tsx": `export default function IndexPage() { throw new Error("oh no!"); return
Hello World
; }`, "package.json": JSON.stringify({ "name": "test-app", "version": "1.0.0", "devDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0", }, }), }); // Run the build command const { exitCode: buildExitCode, stdout: buildStdout, stderr: buildStderr, } = await Bun.$`${bunExe()} build --app ./src/index.tsx`.cwd(dir).throws(false); // The build should fail due to the runtime error during SSG expect(buildExitCode).toBe(1); // Check that the error message shows the proper source location expect(buildStderr.toString()).toContain("throw new Error"); expect(buildStderr.toString()).toContain("oh no!"); }); test("import.meta properties are inlined in production build", async () => { const dir = await tempDirWithBakeDeps("bake-production-import-meta", { "src/index.tsx": `export default { app: { framework: "react", } };`, "pages/index.tsx": ` export default function IndexPage() { const metaInfo = { dir: import.meta.dir, dirname: import.meta.dirname, file: import.meta.file, path: import.meta.path, url: import.meta.url, }; return (

Import Meta Test

{JSON.stringify(metaInfo, null, 2)}
{JSON.stringify(metaInfo)}
); } `, "pages/api/test.tsx": ` export default function TestPage() { const values = [ "dir=" + import.meta.dir, "dirname=" + import.meta.dirname, "file=" + import.meta.file, "path=" + import.meta.path, "url=" + import.meta.url, ]; return (

API Test

{values.join("\\n")}
{values.join("|")}
); } `, }); // Run the build command const buildProc = await Bun.$`${bunExe()} build --app ./src/index.tsx --outdir ./dist` .cwd(dir) .env(bunEnv) .throws(false); expect(buildProc.exitCode).toBe(0); // Check that the build output contains the generated files const distFiles = await Bun.$`ls -la dist/`.cwd(dir).text(); expect(distFiles).toContain("index.html"); expect(distFiles).toContain("_bun"); // In production SSG, the import.meta values are inlined during build time // and rendered into the static HTML. The values should appear in the HTML output. // Check the generated static HTML files const indexHtml = await Bun.file(path.join(dir, "dist", "index.html")).text(); const apiTestHtml = await Bun.file(path.join(dir, "dist", "api", "test", "index.html")).text(); // The HTML output should contain the rendered import.meta values // Check for the presence of the expected values in the HTML // For the index page, check that it contains the expected file paths expect(indexHtml).toContain("index.tsx"); expect(indexHtml).toContain("pages"); // Check if the HTML contains evidence of import.meta values being used // The exact format might be HTML-escaped, so we check for key patterns const hasIndexPath = indexHtml.includes("pages/index.tsx") || indexHtml.includes("pages/index.tsx") || indexHtml.includes("pages\\index.tsx"); expect(hasIndexPath).toBe(true); // For the API test page expect(apiTestHtml).toContain("test.tsx"); expect(apiTestHtml).toContain("pages"); const hasApiPath = apiTestHtml.includes("pages/api/test.tsx") || apiTestHtml.includes("pages/api/test.tsx") || apiTestHtml.includes("pages\\api\\test.tsx"); expect(hasApiPath).toBe(true); }); test("import.meta properties are inlined in catch-all routes during production build", async () => { const dir = await tempDirWithBakeDeps("bake-production-catch-all", { "src/index.tsx": `export default { app: { framework: "react", } };`, "pages/blog/[...slug].tsx": ` export default function BlogPost({ params }) { const slug = params.slug || []; const metaInfo = { file: import.meta.file, dir: import.meta.dir, path: import.meta.path, url: import.meta.url, dirname: import.meta.dirname, }; return (

Blog Post: {slug.join(' / ')}

You are reading: {slug.length === 0 ? 'the blog index' : slug.join('/')}

{JSON.stringify(metaInfo, null, 2)}
); } export async function getStaticPaths() { return { paths: [ { params: { slug: ['2024', 'hello-world'] } }, { params: { slug: ['2024', 'tech', 'bun-framework'] } }, { params: { slug: ['tutorials', 'getting-started'] } }, ], fallback: false, }; } `, "pages/docs/[...path].tsx": ` export default function DocsPage({ params }) { const path = params.path || []; return (

Documentation

Reading docs at: /{path.join('/')}

); } export async function getStaticPaths() { return { paths: [ { params: { path: ['api', 'reference'] } }, { params: { path: ['guides', 'advanced', 'optimization'] } }, { params: { path: [] } }, // docs index ], fallback: false, }; } `, "pages/docs/getting-started.tsx": ` export default function GettingStarted() { return (

Getting Started

This is a static page, not a catch-all route.

); } `, }); console.error("DIR", dir); // Run the build command const buildProc = await Bun.$`${bunExe()} build --app ./src/index.tsx --outdir ./dist` .cwd(dir) .env(bunEnv) .throws(false); expect(buildProc.exitCode).toBe(0); // Check that the build output contains the generated files const htmlFiles = Array.from(new Bun.Glob("dist/**/*.html").scanSync(dir)) .sort() .map(p => normalizePath(p)); // Should have generated all the static paths // Note: React's routing may flatten the paths expect(htmlFiles).toContain("dist/blog/2024/hello-world/index.html"); expect(htmlFiles).toContain("dist/blog/2024/tech/bun-framework/index.html"); expect(htmlFiles).toContain("dist/blog/tutorials/getting-started/index.html"); expect(htmlFiles).toContain("dist/docs/api/reference/index.html"); expect(htmlFiles).toContain("dist/docs/guides/advanced/optimization/index.html"); expect(htmlFiles).toContain("dist/docs/index.html"); expect(htmlFiles).toContain("dist/docs/getting-started/index.html"); // Check blog post with multiple segments const blogPostHtml = await Bun.file( path.join(dir, "dist", "blog", "2024", "tech", "bun-framework", "index.html"), ).text(); // Verify the content is rendered (may include HTML comments) expect(blogPostHtml).toContain("Blog Post:"); expect(blogPostHtml).toContain("2024 / tech / bun-framework"); expect(blogPostHtml).toContain("You are reading:"); expect(blogPostHtml).toContain("2024/tech/bun-framework"); // Check that import.meta values are inlined in the HTML expect(blogPostHtml).toContain('data-file="[...slug].tsx"'); expect(blogPostHtml).toContain("data-dir="); expect(blogPostHtml).toContain(platformPath('/pages/blog"')); // The full path will include the temp directory expect(blogPostHtml).toContain("data-path="); expect(blogPostHtml).toContain(platformPath('/pages/blog/[...slug].tsx"')); // Check docs catch-all route const docsHtml = await Bun.file( path.join(dir, "dist", "docs", "guides", "advanced", "optimization", "index.html"), ).text(); expect(docsHtml).toContain("Reading docs at:"); expect(docsHtml).toContain("guides/advanced/optimization"); expect(docsHtml).toContain('data-file="[...path].tsx"'); expect(docsHtml).toContain(platformPath('/pages/docs/[...path].tsx"')); // Check that the static getting-started page uses its own file name, not the catch-all const staticHtml = await Bun.file(path.join(dir, "dist", "docs", "getting-started", "index.html")).text(); expect(staticHtml).toContain("Getting Started"); expect(staticHtml).toContain("This is a static page"); expect(staticHtml).toContain('data-file="getting-started.tsx"'); expect(staticHtml).toContain(platformPath('/pages/docs/getting-started.tsx"')); expect(staticHtml).not.toContain("[...path].tsx"); // Verify that import.meta values are consistent across all catch-all instances const blogIndex = await Bun.file( path.join(dir, "dist", "blog", "tutorials", "getting-started", "index.html"), ).text(); expect(blogIndex).toContain('data-file="[...slug].tsx"'); expect(blogIndex).toContain(platformPath('/pages/blog/[...slug].tsx"')); }); test("handles build with no pages directory without crashing", async () => { const dir = await tempDirWithBakeDeps("bake-production-no-pages", { "app.ts": `export default { app: { framework: "react" } };`, "package.json": JSON.stringify({ "name": "test-app", "version": "1.0.0", "devDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0", }, }), }); // Run the build command - should not crash even with no pages const { exitCode, stderr } = await Bun.$`${bunExe()} build --app ./app.ts`.cwd(dir).throws(false); // The build should complete successfully (or fail gracefully, not crash) // We're testing that it doesn't crash with the StringBuilder assertion expect(exitCode).toBeDefined(); // If it fails, it should be a graceful failure, not a crash if (exitCode !== 0) { expect(stderr.toString()).not.toContain("reached unreachable code"); expect(stderr.toString()).not.toContain("assert(this.cap > 0)"); } }); test("client-side component with default import should work", async () => { const dir = await tempDirWithBakeDeps("bake-production-client-import", { "src/index.tsx": `export default { app: { framework: "react" } };`, "pages/index.tsx": `import Client from "../components/Client"; export default function IndexPage() { return (
LMAOHello World
); }`, "components/Client.tsx": `"use client"; export default function Client() { console.log("Client-side!"); return
Hello World
; }`, "package.json": JSON.stringify({ "name": "test-app", "version": "1.0.0", "devDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0", }, }), }); // Run the build command const { exitCode, stderr } = await Bun.$`${bunExe()} build --app ./src/index.tsx`.cwd(dir).throws(false); expect(exitCode).toBe(0); // Check the generated HTML file for pages/index.tsx const htmlPage = path.join(dir, "dist", "index.html"); expect(existsSync(htmlPage)).toBe(true); const htmlContent = await Bun.file(htmlPage).text(); // Verify the static content is rendered expect(htmlContent).toContain("LMAO"); expect(htmlContent).toContain("Hello World"); }); test("importing useState server-side", async () => { const dir = await tempDirWithBakeDeps("bake-production-react-import", { "src/index.tsx": `export default { app: { framework: "react" } };`, "pages/index.tsx": `import { useState } from 'react'; export default function IndexPage() { const [count, setCount] = useState(0); return (
LMAOHello World
); }`, "package.json": JSON.stringify({ "name": "test-app", "version": "1.0.0", "devDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0", }, }), }); // Run the build command const { exitCode, stderr } = await Bun.$`${bunExe()} build --app ./src/index.tsx`.cwd(dir).throws(false); // The build should succeed - client components should support default imports expect(stderr.toString()).toContain( '"useState" is not available in a server component. If you need interactivity, consider converting part of this to a Client Component (by adding `"use client";` to the top of the file).', ); expect(exitCode).toBe(1); }); test("importing useState from client component", async () => { const dir = await tempDirWithBakeDeps("bake-production-client-useState", { "src/index.tsx": ` const bundlerOptions = { sourcemap: "inline", minify: { whitespace: false, identifiers: false, syntax: false, }, }; export default { app: { framework: "react", bundlerOptions: { server: bundlerOptions, client: bundlerOptions, ssr: bundlerOptions } } };`, "pages/index.tsx": `import Counter from "../components/Counter"; export default function IndexPage() { return (

Counter Example

); }`, "components/Counter.tsx": `"use client"; import { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0); return (

Count: {count}

); }`, "package.json": JSON.stringify({ "name": "test-app", "version": "1.0.0", "devDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0", }, }), }); // Run the build command const { exitCode, stderr } = await Bun.$`${bunExe()} build --app ./src/index.tsx`.cwd(dir).throws(false); // The build should succeed - client components CAN use useState expect(stderr.toString()).not.toContain("useState"); expect(exitCode).toBe(0); // Check the generated HTML file const htmlPage = path.join(dir, "dist", "index.html"); expect(existsSync(htmlPage)).toBe(true); const htmlContent = await Bun.file(htmlPage).text(); // Verify the static content is rendered expect(htmlContent).toContain("

Counter Example

"); // Verify client component script tags exist expect(htmlContent).toContain(" { const dir = await tempDirWithBakeDeps("bake-production-no-client-js", { "src/index.tsx": `export default { app: { framework: "react" } };`, "pages/index.tsx": ` export default function IndexPage() { return (
Hello World
); }`, "package.json": JSON.stringify({ "name": "test-app", "version": "1.0.0", "devDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0", }, }), }); // Run the build command const { exitCode, stderr } = await Bun.$`${bunExe()} build --app ./src/index.tsx`.cwd(dir).throws(false); // The build should succeed // expect(stderr.toString()).toBe(""); expect(exitCode).toBe(0); // Check the generated HTML file const htmlPage = path.join(dir, "dist", "index.html"); expect(existsSync(htmlPage)).toBe(true); const htmlContent = await Bun.file(htmlPage).text(); // Verify the content is rendered expect(htmlContent).toContain("Hello World"); // Verify NO JavaScript imports are included in the HTML expect(htmlContent).not.toContain('