Files
bun.sh/test/bake/dev/production.test.ts
Jarred Sumner a868e859d7 Run formatter
2025-07-21 01:19:09 -07:00

544 lines
18 KiB
TypeScript

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 <div>Hello World</div>;
}`,
"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 (
<div>
<h1>Import Meta Test</h1>
<pre>{JSON.stringify(metaInfo, null, 2)}</pre>
<div id="meta-data" style={{display: 'none'}}>{JSON.stringify(metaInfo)}</div>
</div>
);
}
`,
"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 (
<div>
<h1>API Test</h1>
<pre>{values.join("\\n")}</pre>
<div id="api-meta-data" style={{display: 'none'}}>{values.join("|")}</div>
</div>
);
}
`,
});
// 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&#x2F;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&#x2F;api&#x2F;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 (
<article>
<h1>Blog Post: {slug.join(' / ')}</h1>
<p>You are reading: {slug.length === 0 ? 'the blog index' : slug.join('/')}</p>
<div id="blog-meta" data-file={metaInfo.file} data-dir={metaInfo.dir} data-path={metaInfo.path}>
<pre>{JSON.stringify(metaInfo, null, 2)}</pre>
</div>
</article>
);
}
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 (
<div>
<h1>Documentation</h1>
<nav aria-label="Breadcrumb">
<ol>
<li>Docs</li>
{path.map((segment, i) => (
<li key={i}>{segment}</li>
))}
</ol>
</nav>
<div id="docs-content">
<p>Reading docs at: /{path.join('/')}</p>
<div id="docs-meta" style={{display: 'none'}}>
<span data-file={import.meta.file}></span>
<span data-dir={import.meta.dir}></span>
<span data-path={import.meta.path}></span>
<span data-url={import.meta.url}></span>
</div>
</div>
</div>
);
}
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 (
<div>
<h1>Getting Started</h1>
<p>This is a static page, not a catch-all route.</p>
<div id="static-meta" style={{display: 'none'}}>
<span data-file={import.meta.file}></span>
<span data-path={import.meta.path}></span>
</div>
</div>
);
}
`,
});
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 (
<div>
<title>LMAO</title>Hello World
<Client />
</div>
);
}`,
"components/Client.tsx": `"use client";
export default function Client() {
console.log("Client-side!");
return <div>Hello World</div>;
}`,
"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("<title>LMAO</title>");
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 (
<div>
<title>LMAO</title>Hello World
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}`,
"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 (
<div>
<h1>Counter Example</h1>
<Counter />
</div>
);
}`,
"components/Counter.tsx": `"use client";
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}`,
"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("<h1>Counter Example</h1>");
// Verify client component script tags exist
expect(htmlContent).toContain("<script");
expect(htmlContent).toContain("/_bun/");
// Extract the JS bundle filename from the HTML
const scriptMatch = htmlContent.match(/src="[/]_bun[/]([a-z0-9]+\.js)"/);
expect(scriptMatch).toBeTruthy();
const bundleFilename = scriptMatch![1];
// Check that the client bundle was created
const clientBundle = path.join(dir, "dist", "_bun", bundleFilename);
expect(existsSync(clientBundle)).toBe(true);
// Also check for component-specific bundle by looking for all JS files
const bundles = await Bun.$`ls ${path.join(dir, "dist", "_bun")}/*.js`.cwd(dir).text();
const bundleFiles = bundles.trim().split("\n").filter(Boolean);
// Read all bundles to find the one with our component code
let foundCounterBundle = false;
for (const bundleFile of bundleFiles) {
const content = await Bun.file(bundleFile).text();
if (content.includes("useState") && content.includes("setCount") && content.includes("Click me")) {
foundCounterBundle = true;
break;
}
}
expect(foundCounterBundle).toBe(true);
});
test("don't include client code if fully static route", async () => {
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 (
<div>
Hello World
</div>
);
}`,
"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('<script type="module"');
});
});