Files
bun.sh/test/integration/next-pages/test/next-build.test.ts
Jarred Sumner a3fcfd3963 Bump WebKit (#22145)
### What does this PR do?

### How did you verify your code works?

---------

Co-authored-by: Jarred-Sumner <709451+Jarred-Sumner@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-08-26 17:38:15 -07:00

225 lines
7.0 KiB
TypeScript

import { install_test_helpers } from "bun:internal-for-testing";
import { expect, test } from "bun:test";
import { copyFileSync, cpSync, promises as fs, readFileSync, rmSync } from "fs";
import { cp } from "fs/promises";
import { join } from "path";
import { bunEnv, bunExe, isDebug, tmpdirSync, toMatchNodeModulesAt } from "../../../harness";
const { parseLockfile } = install_test_helpers;
expect.extend({ toMatchNodeModulesAt });
const root = join(import.meta.dir, "../");
async function tempDirToBuildIn() {
const dir = tmpdirSync(
"next-" + Math.ceil(performance.now() * 1000).toString(36) + Math.random().toString(36).substring(2, 8),
);
console.log("Temp dir: " + dir);
const copy = [
".eslintrc.json",
"bun.lock",
"next.config.js",
"package.json",
"postcss.config.js",
"public",
"src",
"tailwind.config.ts",
"bunfig.toml",
];
await Promise.all(copy.map(x => cp(join(root, x), join(dir, x), { recursive: true })));
cpSync(join(root, "src/Counter1.txt"), join(dir, "src/Counter.tsx"));
cpSync(join(root, "tsconfig_for_build.json"), join(dir, "tsconfig.json"));
const install = Bun.spawnSync([bunExe(), "i"], {
cwd: dir,
env: bunEnv,
stdin: "inherit",
stdout: "inherit",
stderr: "inherit",
});
if (!install.success) {
const reason = install.signalCode || `code ${install.exitCode}`;
throw new Error(`Failed to install dependencies: ${reason}`);
}
return dir;
}
async function hashFile(file: string, path: string, hashes: Record<string, string>) {
try {
let contents = (await fs.readFile(path)).toString();
const nums = contents.match(/,e.ids=\[(\d+)\,(\d+)\,(\d+)\],/);
if (nums) {
nums.sort((a, b) => parseInt(a) - parseInt(b));
contents.replace(/,e.ids=\[(\d+)\,(\d+)\,(\d+)\],/, `,e.ids=[${nums}],`);
}
hashes[file] = Bun.CryptoHasher.hash("sha256", contents, "hex");
} catch (error) {
console.error("error", error, "in", path);
throw error;
}
}
async function hashAllFiles(dir: string) {
console.time("Hashing");
try {
const files = (await fs.readdir(dir, { recursive: true, withFileTypes: true }))
.filter(x => x.isFile() || x.isSymbolicLink())
.sort((a, b) => {
return a.name.localeCompare(b.name);
});
const hashes: Record<string, string> = {};
const batchSize = 4;
while (files.length > 0) {
const batch = files.splice(0, batchSize);
await Promise.all(batch.map(file => hashFile(file.name, join(file.parentPath, file.name), hashes)));
}
return hashes;
} finally {
console.timeEnd("Hashing");
}
}
function normalizeOutput(stdout: string) {
return (
stdout
// remove timestamps from output
.replace(/\(\d+(?:\.\d+)? m?s\)/gi, data => " ".repeat(data.length))
// displayed file sizes are in post-gzip compression, however
// the gzip / node:zlib implementation is different in bun and node
.replace(/\d+(\.\d+)? [km]?b/gi, data => " ".repeat(data.length))
// normalize "Compiled successfully in Xms" timestamps
.replace(/Compiled successfully in (\d|\.)+(ms|s)/gi, "Compiled successfully in 1000ms")
// normalize counter logging that may appear in different spots
.replaceAll("\ncounter a", "")
.split("\n")
.map(x => x.trim())
.join("\n")
);
}
test(
"next build works",
async () => {
rmSync(join(root, ".next"), { recursive: true, force: true });
copyFileSync(join(root, "src/Counter1.txt"), join(root, "src/Counter.tsx"));
const bunDir = await tempDirToBuildIn();
let lockfile = parseLockfile(bunDir);
expect(lockfile).toMatchNodeModulesAt(bunDir);
expect(parseLockfile(bunDir)).toMatchSnapshot("bun");
const nodeDir = await tempDirToBuildIn();
lockfile = parseLockfile(nodeDir);
expect(lockfile).toMatchNodeModulesAt(nodeDir);
expect(lockfile).toMatchSnapshot("node");
console.log("Bun Dir: " + bunDir);
console.log("Node Dir: " + nodeDir);
const nextPath = "node_modules/next/dist/bin/next";
const tmp1 = tmpdirSync();
console.time("[bun] next build");
const bunBuild = Bun.spawn([bunExe(), "--bun", nextPath, "build"], {
cwd: bunDir,
stdio: ["ignore", "pipe", "inherit"],
env: {
...bunEnv,
NODE_NO_WARNINGS: "1",
NODE_ENV: "production",
TMPDIR: tmp1,
TEMP: tmp1,
TMP: tmp1,
},
});
const tmp2 = tmpdirSync();
console.time("[node] next build");
const nodeBuild = Bun.spawn(["node", nextPath, "build"], {
cwd: nodeDir,
env: {
...bunEnv,
NODE_NO_WARNINGS: "1",
NODE_ENV: "production",
TMPDIR: tmp2,
TEMP: tmp2,
TMP: tmp2,
},
stdio: ["ignore", "pipe", "inherit"],
});
await Promise.all([
bunBuild.exited.then(a => {
console.timeEnd("[bun] next build");
return a;
}),
nodeBuild.exited.then(a => {
console.timeEnd("[node] next build");
return a;
}),
]);
expect(nodeBuild.exitCode).toBe(0);
expect(bunBuild.exitCode).toBe(0);
const bunCliOutput = normalizeOutput(await new Response(bunBuild.stdout).text());
const nodeCliOutput = normalizeOutput(await new Response(nodeBuild.stdout).text());
console.log("bun", bunCliOutput);
console.log("node", nodeCliOutput);
expect(bunCliOutput).toBe(nodeCliOutput);
const bunBuildDir = join(bunDir, ".next");
const nodeBuildDir = join(nodeDir, ".next");
// Remove some build files that Next.js does not make deterministic.
const toRemove = [
// these have timestamps and absolute paths in them
"trace",
"cache",
"required-server-files.json",
// these have "signing keys", not sure what they are tbh
"prerender-manifest.json",
// these are similar but i feel like there might be something we can fix to make them the same
"next-minimal-server.js.nft.json",
"next-server.js.nft.json",
// this file is not deterministically sorted
"server/pages-manifest.json",
];
for (const key of toRemove) {
rmSync(join(bunBuildDir, key), { recursive: true });
rmSync(join(nodeBuildDir, key), { recursive: true });
}
console.log("Hashing files...");
const [bunBuildHash, nodeBuildHash] = await Promise.all([hashAllFiles(bunBuildDir), hashAllFiles(nodeBuildDir)]);
try {
expect(bunBuildHash).toEqual(nodeBuildHash);
} catch (error) {
console.log("bunBuildDir", bunBuildDir);
console.log("nodeBuildDir", nodeBuildDir);
// print diffs for every file if not the same
for (const key in bunBuildHash) {
if (bunBuildHash[key] !== nodeBuildHash[key]) {
console.log(key + ":");
try {
expect(readFileSync(join(bunBuildDir, key)).toString()).toBe(
readFileSync(join(nodeBuildDir, key)).toString(),
);
} catch (error) {
console.error(error);
}
}
}
throw error;
}
},
isDebug ? Infinity : 60_0000,
);