mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
656 lines
16 KiB
TypeScript
656 lines
16 KiB
TypeScript
import { spawn } from "bun";
|
|
import { beforeEach, expect, it } from "bun:test";
|
|
import { copyFileSync, cpSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from "fs";
|
|
import { bunEnv, bunExe, isDebug, tmpdirSync, waitForFileToExist } from "harness";
|
|
import { join } from "path";
|
|
|
|
const timeout = isDebug ? Infinity : 10_000;
|
|
const longTimeout = isDebug ? Infinity : 30_000;
|
|
|
|
let hotRunnerRoot: string = "",
|
|
cwd = "";
|
|
beforeEach(() => {
|
|
const hotPath = tmpdirSync();
|
|
hotRunnerRoot = join(hotPath, "hot-runner-root.js");
|
|
rmSync(hotPath, { recursive: true, force: true });
|
|
cpSync(import.meta.dir, hotPath, { recursive: true, force: true });
|
|
cwd = hotPath;
|
|
});
|
|
|
|
it(
|
|
"should hot reload when file is overwritten",
|
|
async () => {
|
|
const root = hotRunnerRoot;
|
|
try {
|
|
var runner = spawn({
|
|
cmd: [bunExe(), "--hot", "run", root],
|
|
env: bunEnv,
|
|
cwd,
|
|
stdout: "pipe",
|
|
stderr: "inherit",
|
|
stdin: "ignore",
|
|
});
|
|
|
|
var reloadCounter = 0;
|
|
|
|
async function onReload() {
|
|
writeFileSync(root, readFileSync(root, "utf-8"));
|
|
}
|
|
|
|
var str = "";
|
|
for await (const line of runner.stdout) {
|
|
str += new TextDecoder().decode(line);
|
|
var any = false;
|
|
if (!/\[#!root\].*[0-9]\n/g.test(str)) continue;
|
|
|
|
for (let line of str.split("\n")) {
|
|
if (!line.includes("[#!root]")) continue;
|
|
reloadCounter++;
|
|
str = "";
|
|
|
|
if (reloadCounter === 3) {
|
|
runner.unref();
|
|
runner.kill();
|
|
break;
|
|
}
|
|
|
|
expect(line).toContain(`[#!root] Reloaded: ${reloadCounter}`);
|
|
any = true;
|
|
}
|
|
|
|
if (any) await onReload();
|
|
}
|
|
|
|
expect(reloadCounter).toBeGreaterThanOrEqual(3);
|
|
} finally {
|
|
// @ts-ignore
|
|
runner?.unref?.();
|
|
// @ts-ignore
|
|
runner?.kill?.(9);
|
|
}
|
|
},
|
|
timeout,
|
|
);
|
|
|
|
it.each(["hot-file-loader.file", "hot-file-loader.css"])(
|
|
"should hot reload when `%s` is overwritten",
|
|
async (targetFilename: string) => {
|
|
const root = hotRunnerRoot;
|
|
const target = join(cwd, targetFilename);
|
|
try {
|
|
var runner = spawn({
|
|
cmd: [bunExe(), "--hot", "run", root],
|
|
env: bunEnv,
|
|
cwd,
|
|
stdout: "pipe",
|
|
stderr: "inherit",
|
|
stdin: "ignore",
|
|
});
|
|
|
|
var reloadCounter = 0;
|
|
|
|
async function onReload() {
|
|
writeFileSync(target, readFileSync(target, "utf-8"));
|
|
}
|
|
|
|
var str = "";
|
|
for await (const line of runner.stdout) {
|
|
str += new TextDecoder().decode(line);
|
|
var any = false;
|
|
if (!/\[#!root\].*[0-9]\n/g.test(str)) continue;
|
|
|
|
for (let line of str.split("\n")) {
|
|
if (!line.includes("[#!root]")) continue;
|
|
reloadCounter++;
|
|
str = "";
|
|
|
|
if (reloadCounter === 3) {
|
|
runner.unref();
|
|
runner.kill();
|
|
break;
|
|
}
|
|
|
|
expect(line).toContain(`[#!root] Reloaded: ${reloadCounter}`);
|
|
any = true;
|
|
}
|
|
|
|
if (any) await onReload();
|
|
}
|
|
|
|
expect(reloadCounter).toBeGreaterThanOrEqual(3);
|
|
} finally {
|
|
// @ts-ignore
|
|
runner?.unref?.();
|
|
// @ts-ignore
|
|
runner?.kill?.(9);
|
|
}
|
|
},
|
|
timeout,
|
|
);
|
|
|
|
it(
|
|
"should recover from errors",
|
|
async () => {
|
|
const root = hotRunnerRoot;
|
|
try {
|
|
var runner = spawn({
|
|
cmd: [bunExe(), "--hot", "run", root],
|
|
env: bunEnv,
|
|
cwd,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
stdin: "ignore",
|
|
});
|
|
|
|
let reloadCounter = 0;
|
|
const input = readFileSync(root, "utf-8");
|
|
function onReloadGood() {
|
|
writeFileSync(root, input);
|
|
}
|
|
|
|
function onReloadError() {
|
|
writeFileSync(root, "throw new Error('error');\n");
|
|
}
|
|
|
|
var queue = [onReloadError, onReloadGood, onReloadError, onReloadGood];
|
|
var errors: string[] = [];
|
|
var onError: (...args: any[]) => void;
|
|
(async () => {
|
|
for await (let line of runner.stderr) {
|
|
var str = new TextDecoder().decode(line);
|
|
errors.push(str);
|
|
// @ts-ignore
|
|
onError && onError(str);
|
|
}
|
|
})();
|
|
|
|
var str = "";
|
|
for await (const line of runner.stdout) {
|
|
str += new TextDecoder().decode(line);
|
|
var any = false;
|
|
if (!/\[#!root\].*[0-9]\n/g.test(str)) continue;
|
|
|
|
for (let line of str.split("\n")) {
|
|
if (!line.includes("[#!root]")) continue;
|
|
reloadCounter++;
|
|
str = "";
|
|
|
|
if (reloadCounter === 3) {
|
|
runner.unref();
|
|
runner.kill();
|
|
break;
|
|
}
|
|
|
|
expect(line).toContain(`[#!root] Reloaded: ${reloadCounter}`);
|
|
any = true;
|
|
}
|
|
|
|
if (any) {
|
|
queue.shift()!();
|
|
await new Promise<void>((resolve, reject) => {
|
|
if (errors.length > 0) {
|
|
errors.length = 0;
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
onError = resolve;
|
|
});
|
|
|
|
queue.shift()!();
|
|
}
|
|
}
|
|
|
|
expect(reloadCounter).toBe(3);
|
|
} finally {
|
|
// @ts-ignore
|
|
runner?.unref?.();
|
|
// @ts-ignore
|
|
runner?.kill?.(9);
|
|
}
|
|
},
|
|
timeout,
|
|
);
|
|
|
|
it(
|
|
"should not hot reload when a random file is written",
|
|
async () => {
|
|
const root = hotRunnerRoot;
|
|
try {
|
|
var runner = spawn({
|
|
cmd: [bunExe(), "--hot", "run", root],
|
|
env: bunEnv,
|
|
cwd,
|
|
stdout: "pipe",
|
|
stderr: "inherit",
|
|
stdin: "ignore",
|
|
});
|
|
|
|
let reloadCounter = 0;
|
|
const code = readFileSync(root, "utf-8");
|
|
async function onReload() {
|
|
writeFileSync(root + ".another.yet.js", code);
|
|
unlinkSync(root + ".another.yet.js");
|
|
}
|
|
var finished = false;
|
|
await Promise.race([
|
|
Bun.sleep(200),
|
|
(async () => {
|
|
if (finished) {
|
|
return;
|
|
}
|
|
var str = "";
|
|
for await (const line of runner.stdout) {
|
|
if (finished) {
|
|
return;
|
|
}
|
|
|
|
str += new TextDecoder().decode(line);
|
|
if (!/\[#!root\].*[0-9]\n/g.test(str)) continue;
|
|
|
|
for (let line of str.split("\n")) {
|
|
if (!line.includes("[#!root]")) continue;
|
|
if (finished) {
|
|
return;
|
|
}
|
|
await onReload();
|
|
|
|
reloadCounter++;
|
|
str = "";
|
|
expect(line).toContain(`[#!root] Reloaded: ${reloadCounter}`);
|
|
}
|
|
}
|
|
})(),
|
|
]);
|
|
finished = true;
|
|
runner.kill(0);
|
|
runner.unref();
|
|
|
|
expect(reloadCounter).toBe(1);
|
|
} finally {
|
|
// @ts-ignore
|
|
runner?.unref?.();
|
|
// @ts-ignore
|
|
runner?.kill?.(9);
|
|
}
|
|
},
|
|
timeout,
|
|
);
|
|
|
|
it(
|
|
"should hot reload when a file is deleted and rewritten",
|
|
async () => {
|
|
try {
|
|
const root = hotRunnerRoot + ".tmp.js";
|
|
copyFileSync(hotRunnerRoot, root);
|
|
var runner = spawn({
|
|
cmd: [bunExe(), "--hot", "run", root],
|
|
env: bunEnv,
|
|
cwd,
|
|
stdout: "pipe",
|
|
stderr: "inherit",
|
|
stdin: "ignore",
|
|
});
|
|
|
|
var reloadCounter = 0;
|
|
|
|
async function onReload() {
|
|
const contents = readFileSync(root, "utf-8");
|
|
rmSync(root);
|
|
writeFileSync(root, contents);
|
|
}
|
|
|
|
var str = "";
|
|
for await (const line of runner.stdout) {
|
|
str += new TextDecoder().decode(line);
|
|
var any = false;
|
|
if (!/\[#!root\].*[0-9]\n/g.test(str)) continue;
|
|
|
|
for (let line of str.split("\n")) {
|
|
if (!line.includes("[#!root]")) continue;
|
|
reloadCounter++;
|
|
str = "";
|
|
|
|
if (reloadCounter === 3) {
|
|
runner.unref();
|
|
runner.kill();
|
|
break;
|
|
}
|
|
|
|
expect(line).toContain(`[#!root] Reloaded: ${reloadCounter}`);
|
|
any = true;
|
|
}
|
|
|
|
if (any) await onReload();
|
|
}
|
|
rmSync(root);
|
|
expect(reloadCounter).toBe(3);
|
|
} finally {
|
|
// @ts-ignore
|
|
runner?.unref?.();
|
|
// @ts-ignore
|
|
runner?.kill?.(9);
|
|
}
|
|
},
|
|
timeout,
|
|
);
|
|
|
|
it(
|
|
"should hot reload when a file is renamed() into place",
|
|
async () => {
|
|
const root = hotRunnerRoot + ".tmp.js";
|
|
copyFileSync(hotRunnerRoot, root);
|
|
try {
|
|
var runner = spawn({
|
|
cmd: [bunExe(), "--hot", "run", root],
|
|
env: bunEnv,
|
|
cwd,
|
|
stdout: "pipe",
|
|
stderr: "inherit",
|
|
stdin: "ignore",
|
|
});
|
|
|
|
var reloadCounter = 0;
|
|
|
|
async function onReload() {
|
|
const contents = readFileSync(root, "utf-8");
|
|
rmSync(root + ".tmpfile", { force: true });
|
|
await 1;
|
|
writeFileSync(root + ".tmpfile", contents);
|
|
await 1;
|
|
rmSync(root);
|
|
await 1;
|
|
renameSync(root + ".tmpfile", root);
|
|
await 1;
|
|
}
|
|
|
|
var str = "";
|
|
for await (const line of runner.stdout) {
|
|
str += new TextDecoder().decode(line);
|
|
var any = false;
|
|
if (!/\[#!root\].*[0-9]\n/g.test(str)) continue;
|
|
|
|
for (let line of str.split("\n")) {
|
|
if (!line.includes("[#!root]")) continue;
|
|
reloadCounter++;
|
|
str = "";
|
|
|
|
if (reloadCounter === 3) {
|
|
runner.unref();
|
|
runner.kill();
|
|
break;
|
|
}
|
|
|
|
expect(line).toContain(`[#!root] Reloaded: ${reloadCounter}`);
|
|
any = true;
|
|
}
|
|
|
|
if (any) await onReload();
|
|
}
|
|
rmSync(root);
|
|
expect(reloadCounter).toBe(3);
|
|
} finally {
|
|
// @ts-ignore
|
|
runner?.unref?.();
|
|
// @ts-ignore
|
|
runner?.kill?.(9);
|
|
}
|
|
},
|
|
timeout,
|
|
);
|
|
|
|
const comment_spam = ("//" + "B".repeat(2000) + "\n").repeat(1000);
|
|
it(
|
|
"should work with sourcemap generation",
|
|
async () => {
|
|
writeFileSync(
|
|
hotRunnerRoot,
|
|
`// source content
|
|
${comment_spam}
|
|
throw new Error('0');`,
|
|
);
|
|
await using runner = spawn({
|
|
cmd: [bunExe(), "--smol", "--hot", "run", hotRunnerRoot],
|
|
env: bunEnv,
|
|
cwd,
|
|
stdout: "ignore",
|
|
stderr: "pipe",
|
|
stdin: "ignore",
|
|
});
|
|
let reloadCounter = 0;
|
|
function onReload() {
|
|
writeFileSync(
|
|
hotRunnerRoot,
|
|
`// source content
|
|
${comment_spam}
|
|
${" ".repeat(reloadCounter * 2)}throw new Error(${reloadCounter});`,
|
|
);
|
|
}
|
|
let str = "";
|
|
outer: for await (const chunk of runner.stderr) {
|
|
str += new TextDecoder().decode(chunk);
|
|
var any = false;
|
|
if (!/error: .*[0-9]\n.*?\n/g.test(str)) continue;
|
|
|
|
let it = str.split("\n");
|
|
let line;
|
|
while ((line = it.shift())) {
|
|
if (!line.includes("error")) continue;
|
|
str = "";
|
|
|
|
if (reloadCounter === 50) {
|
|
runner.kill();
|
|
break;
|
|
}
|
|
|
|
if (line.includes(`error: ${reloadCounter - 1}`)) {
|
|
onReload(); // re-save file to prevent deadlock
|
|
continue outer;
|
|
}
|
|
expect(line).toContain(`error: ${reloadCounter}`);
|
|
reloadCounter++;
|
|
|
|
let next = it.shift()!;
|
|
if (!next) throw new Error(line);
|
|
const match = next.match(/\s*at.*?:1003:(\d+)$/);
|
|
if (!match) throw new Error("invalid string: " + next);
|
|
const col = match[1];
|
|
expect(Number(col)).toBe(1 + "throw ".length + (reloadCounter - 1) * 2);
|
|
any = true;
|
|
}
|
|
|
|
if (any) await onReload();
|
|
}
|
|
await runner.exited;
|
|
expect(reloadCounter).toBe(50);
|
|
},
|
|
timeout,
|
|
);
|
|
|
|
it(
|
|
"should work with sourcemap loading",
|
|
async () => {
|
|
let bundleIn = join(cwd, "bundle_in.ts");
|
|
rmSync(hotRunnerRoot);
|
|
writeFileSync(
|
|
bundleIn,
|
|
`// source content
|
|
//
|
|
//
|
|
throw new Error('0');`,
|
|
);
|
|
await using bundler = spawn({
|
|
cmd: [bunExe(), "build", "--watch", bundleIn, "--target=bun", "--sourcemap", "--outfile", hotRunnerRoot],
|
|
env: bunEnv,
|
|
cwd,
|
|
stdout: "inherit",
|
|
stderr: "inherit",
|
|
stdin: "ignore",
|
|
});
|
|
waitForFileToExist(hotRunnerRoot, 20);
|
|
await using runner = spawn({
|
|
cmd: [bunExe(), "--hot", "run", hotRunnerRoot],
|
|
env: bunEnv,
|
|
cwd,
|
|
stdout: "ignore",
|
|
stderr: "pipe",
|
|
stdin: "ignore",
|
|
});
|
|
let reloadCounter = 0;
|
|
function onReload() {
|
|
writeFileSync(
|
|
bundleIn,
|
|
`// source content
|
|
// etc etc
|
|
// etc etc
|
|
${" ".repeat(reloadCounter * 2)}throw new Error(${reloadCounter});`,
|
|
);
|
|
}
|
|
let str = "";
|
|
outer: for await (const chunk of runner.stderr) {
|
|
str += new TextDecoder().decode(chunk);
|
|
var any = false;
|
|
if (!/error: .*[0-9]\n.*?\n/g.test(str)) continue;
|
|
|
|
let it = str.split("\n");
|
|
let line;
|
|
while ((line = it.shift())) {
|
|
if (!line.includes("error")) continue;
|
|
str = "";
|
|
|
|
if (reloadCounter === 50) {
|
|
runner.kill();
|
|
break;
|
|
}
|
|
|
|
if (line.includes(`error: ${reloadCounter - 1}`)) {
|
|
onReload(); // re-save file to prevent deadlock
|
|
continue outer;
|
|
}
|
|
expect(line).toContain(`error: ${reloadCounter}`);
|
|
reloadCounter++;
|
|
|
|
let next = it.shift()!;
|
|
expect(next).toInclude("bundle_in.ts");
|
|
const col = next.match(/\s*at.*?:4:(\d+)$/)![1];
|
|
expect(Number(col)).toBe(1 + "throw ".length + (reloadCounter - 1) * 2);
|
|
any = true;
|
|
}
|
|
|
|
if (any) await onReload();
|
|
}
|
|
expect(reloadCounter).toBe(50);
|
|
bundler.kill();
|
|
},
|
|
timeout,
|
|
);
|
|
|
|
const long_comment = "BBBB".repeat(100000);
|
|
|
|
it(
|
|
"should work with sourcemap loading with large files",
|
|
async () => {
|
|
let bundleIn = join(cwd, "bundle_in.ts");
|
|
rmSync(hotRunnerRoot);
|
|
writeFileSync(
|
|
bundleIn,
|
|
`// ${long_comment}
|
|
//
|
|
console.error("RSS: %s", process.memoryUsage().rss);
|
|
throw new Error('0');`,
|
|
);
|
|
await using bundler = spawn({
|
|
cmd: [
|
|
//
|
|
bunExe(),
|
|
"build",
|
|
"--watch",
|
|
bundleIn,
|
|
"--target=bun",
|
|
"--sourcemap",
|
|
"--outfile",
|
|
hotRunnerRoot,
|
|
],
|
|
env: bunEnv,
|
|
cwd,
|
|
stdout: "ignore",
|
|
stderr: "ignore",
|
|
stdin: "ignore",
|
|
});
|
|
waitForFileToExist(hotRunnerRoot, 20);
|
|
await using runner = spawn({
|
|
cmd: [
|
|
//
|
|
bunExe(),
|
|
"--hot",
|
|
"run",
|
|
hotRunnerRoot,
|
|
],
|
|
env: bunEnv,
|
|
cwd,
|
|
stdout: "inherit",
|
|
stderr: "pipe",
|
|
stdin: "ignore",
|
|
});
|
|
let reloadCounter = 0;
|
|
function onReload() {
|
|
writeFileSync(
|
|
bundleIn,
|
|
`// ${long_comment}
|
|
console.error("RSS: %s", process.memoryUsage().rss);
|
|
//
|
|
${" ".repeat(reloadCounter * 2)}throw new Error(${reloadCounter});`,
|
|
);
|
|
}
|
|
let str = "";
|
|
let sampleMemory10: number | undefined;
|
|
let sampleMemory100: number | undefined;
|
|
outer: for await (const chunk of runner.stderr) {
|
|
str += new TextDecoder().decode(chunk);
|
|
var any = false;
|
|
if (!/error: .*[0-9]\n.*?\n/g.test(str)) continue;
|
|
|
|
let it = str.split("\n");
|
|
let line;
|
|
while ((line = it.shift())) {
|
|
if (!line.includes("error:")) continue;
|
|
let rssMatch = str.match(/RSS: (\d+(\.\d+)?)\n/);
|
|
let rss;
|
|
if (rssMatch) rss = Number(rssMatch[1]);
|
|
str = "";
|
|
|
|
if (reloadCounter == 10) {
|
|
sampleMemory10 = rss;
|
|
}
|
|
|
|
if (reloadCounter >= 50) {
|
|
sampleMemory100 = rss;
|
|
runner.kill();
|
|
break;
|
|
}
|
|
|
|
if (line.includes(`error: ${reloadCounter - 1}`)) {
|
|
onReload(); // re-save file to prevent deadlock
|
|
continue outer;
|
|
}
|
|
expect(line).toContain(`error: ${reloadCounter}`);
|
|
|
|
reloadCounter++;
|
|
let next = it.shift()!;
|
|
expect(next).toInclude("bundle_in.ts");
|
|
const col = next.match(/\s*at.*?:4:(\d+)$/)![1];
|
|
expect(Number(col)).toBe(1 + "throw ".length + (reloadCounter - 1) * 2);
|
|
any = true;
|
|
}
|
|
|
|
if (any) await onReload();
|
|
}
|
|
expect(reloadCounter).toBe(50);
|
|
bundler.kill();
|
|
await runner.exited;
|
|
// TODO: bun has a memory leak when --hot is used on very large files
|
|
// console.log({ sampleMemory10, sampleMemory100 });
|
|
},
|
|
longTimeout,
|
|
);
|