mirror of
https://github.com/oven-sh/bun
synced 2026-02-15 05:12:29 +00:00
### 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>
225 lines
7.0 KiB
TypeScript
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,
|
|
);
|