Files
bun.sh/test/bundler/resolver/cache-node-compat.test.ts
Jarred Sumner d963a05907 Reduce # of redundant resolver syscalls #2 (#23506)
### What does this PR do?

### How did you verify your code works?

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-11 19:53:05 -07:00

561 lines
17 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { promises as fs } from "fs";
import { bunEnv, bunExe } from "harness";
import * as os from "os";
import * as path from "path";
// These tests check if the resolver cache fix introduces any regressions
// by comparing Bun's behavior with Node.js on edge cases involving module
// deletion and recreation.
describe.concurrent("Node.js compatibility", () => {
test("resolution after deleting and recreating module", async () => {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "resolver-compat-"));
try {
// Create test script that requires, deletes, recreates, and requires again
const testScript = `
const fs = require("fs");
const path = require("path");
const modulePath = path.join(__dirname, "testmodule.js");
// First require
const result1 = require("./testmodule");
console.log("First require:", result1.value);
if (result1.value !== 1) process.exit(1);
// Clear cache
delete require.cache[require.resolve("./testmodule")];
// Delete file
fs.rmSync(modulePath);
// Try to require deleted file - should fail
try {
require("./testmodule");
console.log("ERROR: Second require should have failed");
process.exit(1);
} catch (e) {
console.log("Second require failed as expected");
}
// Recreate with new content
fs.writeFileSync(modulePath, "module.exports = { value: 2 };");
// Third require - should succeed with new value
const result3 = require("./testmodule");
console.log("Third require:", result3.value);
if (result3.value !== 2) {
console.log("ERROR: Expected value 2, got", result3.value);
process.exit(1);
}
console.log("SUCCESS: All checks passed");
`;
await fs.writeFile(path.join(tmpDir, "test.js"), testScript);
await fs.writeFile(path.join(tmpDir, "testmodule.js"), `module.exports = { value: 1 };`);
// Run with Node.js
await using nodeProc = Bun.spawn({
cmd: ["node", "test.js"],
cwd: tmpDir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [nodeStdout, nodeStderr, nodeExit] = await Promise.all([
nodeProc.stdout.text(),
nodeProc.stderr.text(),
nodeProc.exited,
]);
const nodeSuccess = nodeExit === 0;
// Reset the file to initial state before running Bun
await fs.writeFile(path.join(tmpDir, "testmodule.js"), `module.exports = { value: 1 };`);
// Run with Bun
await using bunProc = Bun.spawn({
cmd: [bunExe(), "test.js"],
cwd: tmpDir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [bunStdout, bunStderr, bunExit] = await Promise.all([
bunProc.stdout.text(),
bunProc.stderr.text(),
bunProc.exited,
]);
const bunSuccess = bunExit === 0;
// Only log on failure
if (!nodeSuccess || !bunSuccess) {
console.log("\n=== Node.js output ===");
console.log(nodeStdout);
if (!nodeSuccess) {
console.log("Node.js stderr:", nodeStderr);
}
console.log("\n=== Bun output ===");
console.log(bunStdout);
if (!bunSuccess) {
console.log("Bun stderr:", bunStderr);
}
}
// Both should succeed
expect(nodeSuccess).toBe(true);
expect(bunSuccess).toBe(true);
// If there's a discrepancy, fail the test
if (nodeSuccess !== bunSuccess) {
throw new Error(
`Behavior mismatch! Node.js ${nodeSuccess ? "passed" : "failed"} but Bun ${bunSuccess ? "passed" : "failed"}`,
);
}
} finally {
await fs.rm(tmpDir, { recursive: true, force: true });
}
});
test("resolution of directory module after recreation", async () => {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "resolver-compat-dir-"));
try {
const testScript = `
const fs = require("fs");
const path = require("path");
const moduleDir = path.join(__dirname, "mymodule");
// First require
const result1 = require("./mymodule");
console.log("First require:", result1.name);
if (result1.name !== "original") process.exit(1);
// Clear cache
const resolved = require.resolve("./mymodule");
delete require.cache[resolved];
// Delete directory
fs.rmSync(moduleDir, { recursive: true });
// Try to require deleted module - should fail
try {
require("./mymodule");
console.log("ERROR: Second require should have failed");
process.exit(1);
} catch (e) {
console.log("Second require failed as expected");
}
// Recreate with new content
fs.mkdirSync(moduleDir);
fs.writeFileSync(path.join(moduleDir, "index.js"), "module.exports = { name: 'recreated' };");
// Third require - should succeed
const result3 = require("./mymodule");
console.log("Third require:", result3.name);
if (result3.name !== "recreated") {
console.log("ERROR: Expected 'recreated', got", result3.name);
process.exit(1);
}
console.log("SUCCESS: All checks passed");
`;
await fs.writeFile(path.join(tmpDir, "test.js"), testScript);
await fs.mkdir(path.join(tmpDir, "mymodule"));
await fs.writeFile(path.join(tmpDir, "mymodule", "index.js"), `module.exports = { name: "original" };`);
// Run with Node.js
await using nodeProc = Bun.spawn({
cmd: ["node", "test.js"],
cwd: tmpDir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [nodeStdout, nodeStderr, nodeExit] = await Promise.all([
nodeProc.stdout.text(),
nodeProc.stderr.text(),
nodeProc.exited,
]);
const nodeSuccess = nodeExit === 0;
// Reset to initial state before running Bun
await fs.mkdir(path.join(tmpDir, "mymodule"), { recursive: true });
await fs.writeFile(path.join(tmpDir, "mymodule", "index.js"), `module.exports = { name: "original" };`);
// Run with Bun
await using bunProc = Bun.spawn({
cmd: [bunExe(), "test.js"],
cwd: tmpDir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [bunStdout, bunStderr, bunExit] = await Promise.all([
bunProc.stdout.text(),
bunProc.stderr.text(),
bunProc.exited,
]);
const bunSuccess = bunExit === 0;
// Only log on failure
if (!nodeSuccess || !bunSuccess) {
console.log("\n=== Node.js output ===");
console.log(nodeStdout);
if (!nodeSuccess) {
console.log("Node.js stderr:", nodeStderr);
}
console.log("\n=== Bun output ===");
console.log(bunStdout);
if (!bunSuccess) {
console.log("Bun stderr:", bunStderr);
}
}
expect(nodeSuccess).toBe(true);
expect(bunSuccess).toBe(true);
if (nodeSuccess !== bunSuccess) {
throw new Error(
`Behavior mismatch! Node.js ${nodeSuccess ? "passed" : "failed"} but Bun ${bunSuccess ? "passed" : "failed"}`,
);
}
} finally {
await fs.rm(tmpDir, { recursive: true, force: true });
}
});
test("package.json main field resolution after deletion", async () => {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "resolver-compat-pkg-"));
try {
const testScript = `
const fs = require("fs");
const path = require("path");
// First require using package.json main
const result1 = require("./mypkg");
console.log("First require:", result1.value);
if (result1.value !== "main") process.exit(1);
// Clear cache
delete require.cache[require.resolve("./mypkg")];
// Delete the main file
fs.rmSync(path.join(__dirname, "mypkg", "main.js"));
// Should fail since main.js is gone
try {
require("./mypkg");
console.log("ERROR: Second require should have failed");
process.exit(1);
} catch (e) {
console.log("Second require failed as expected");
}
// Recreate main.js
fs.writeFileSync(path.join(__dirname, "mypkg", "main.js"), "module.exports = { value: 'restored' };");
// Should work again
const result3 = require("./mypkg");
console.log("Third require:", result3.value);
if (result3.value !== "restored") {
console.log("ERROR: Expected 'restored', got", result3.value);
process.exit(1);
}
console.log("SUCCESS: All checks passed");
`;
await fs.writeFile(path.join(tmpDir, "test.js"), testScript);
await fs.mkdir(path.join(tmpDir, "mypkg"));
await fs.writeFile(
path.join(tmpDir, "mypkg", "package.json"),
JSON.stringify({ name: "mypkg", main: "./main.js" }),
);
await fs.writeFile(path.join(tmpDir, "mypkg", "main.js"), `module.exports = { value: "main" };`);
// Run with Node.js
await using nodeProc = Bun.spawn({
cmd: ["node", "test.js"],
cwd: tmpDir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [nodeStdout, nodeStderr, nodeExit] = await Promise.all([
nodeProc.stdout.text(),
nodeProc.stderr.text(),
nodeProc.exited,
]);
const nodeSuccess = nodeExit === 0;
// Reset to initial state before running Bun
await fs.writeFile(path.join(tmpDir, "mypkg", "main.js"), `module.exports = { value: "main" };`);
// Run with Bun
await using bunProc = Bun.spawn({
cmd: [bunExe(), "test.js"],
cwd: tmpDir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [bunStdout, bunStderr, bunExit] = await Promise.all([
bunProc.stdout.text(),
bunProc.stderr.text(),
bunProc.exited,
]);
const bunSuccess = bunExit === 0;
// Only log on failure
if (!nodeSuccess || !bunSuccess) {
console.log("\n=== Node.js output ===");
console.log(nodeStdout);
if (!nodeSuccess) {
console.log("Node.js stderr:", nodeStderr);
}
console.log("\n=== Bun output ===");
console.log(bunStdout);
if (!bunSuccess) {
console.log("Bun stderr:", bunStderr);
}
}
expect(nodeSuccess).toBe(true);
expect(bunSuccess).toBe(true);
if (nodeSuccess !== bunSuccess) {
throw new Error(
`Behavior mismatch! Node.js ${nodeSuccess ? "passed" : "failed"} but Bun ${bunSuccess ? "passed" : "failed"}`,
);
}
} finally {
await fs.rm(tmpDir, { recursive: true, force: true });
}
});
// These tests document that Node.js caches resolution paths, not just module contents.
// Even after `delete require.cache[...]`, Node.js remembers which file path was resolved
// and will try to load from that cached path. This means switching between file/directory
// is not supported in Node.js, so Bun matching this behavior is correct.
test("directory path changes to direct file path (expected to fail)", async () => {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "resolver-compat-dir-to-file-"));
try {
const testScript = `
const fs = require("fs");
const path = require("path");
// First require - directory with index.js
const result1 = require("./mymodule");
console.log("First require (directory):", result1.type);
if (result1.type !== "directory") process.exit(1);
// Clear cache
delete require.cache[require.resolve("./mymodule")];
// Delete directory and create direct file instead
fs.rmSync(path.join(__dirname, "mymodule"), { recursive: true });
fs.writeFileSync(path.join(__dirname, "mymodule.js"), "module.exports = { type: 'file' };");
// Second require - will FAIL because Node.js cached the resolution path
// It still tries to load from mymodule/index.js even though that's now gone
try {
const result2 = require("./mymodule");
console.log("ERROR: Second require should have failed but got:", result2.type);
process.exit(1);
} catch (e) {
console.log("Second require failed as expected (cached resolution path)");
}
console.log("SUCCESS: Confirmed resolution path caching");
`;
await fs.writeFile(path.join(tmpDir, "test.js"), testScript);
await fs.mkdir(path.join(tmpDir, "mymodule"));
await fs.writeFile(path.join(tmpDir, "mymodule", "index.js"), `module.exports = { type: "directory" };`);
// Run with Node.js
await using nodeProc = Bun.spawn({
cmd: ["node", "test.js"],
cwd: tmpDir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [nodeStdout, nodeStderr, nodeExit] = await Promise.all([
nodeProc.stdout.text(),
nodeProc.stderr.text(),
nodeProc.exited,
]);
const nodeSuccess = nodeExit === 0;
// Reset to initial state before running Bun
await fs.mkdir(path.join(tmpDir, "mymodule"), { recursive: true });
await fs.writeFile(path.join(tmpDir, "mymodule", "index.js"), `module.exports = { type: "directory" };`);
try {
await fs.rm(path.join(tmpDir, "mymodule.js"));
} catch (e) {}
// Run with Bun
await using bunProc = Bun.spawn({
cmd: [bunExe(), "test.js"],
cwd: tmpDir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [bunStdout, bunStderr, bunExit] = await Promise.all([
bunProc.stdout.text(),
bunProc.stderr.text(),
bunProc.exited,
]);
const bunSuccess = bunExit === 0;
// Only log on failure
if (!nodeSuccess || !bunSuccess) {
console.log("\n=== Node.js output ===");
console.log(nodeStdout);
if (!nodeSuccess) {
console.log("Node.js stderr:", nodeStderr);
}
console.log("\n=== Bun output ===");
console.log(bunStdout);
if (!bunSuccess) {
console.log("Bun stderr:", bunStderr);
}
}
// Both should handle resolution path caching the same way
expect(nodeSuccess).toBe(true);
expect(bunSuccess).toBe(true);
if (nodeSuccess !== bunSuccess) {
throw new Error(
`Behavior mismatch! Node.js ${nodeSuccess ? "passed" : "failed"} but Bun ${bunSuccess ? "passed" : "failed"}`,
);
}
} finally {
await fs.rm(tmpDir, { recursive: true, force: true });
}
});
test("direct file changes to directory with index.js (expected to fail)", async () => {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "resolver-compat-file-to-dir-"));
try {
const testScript = `
const fs = require("fs");
const path = require("path");
// First require - direct file
const result1 = require("./mymodule");
console.log("First require (direct file):", result1.type);
if (result1.type !== "file") process.exit(1);
// Clear cache
delete require.cache[require.resolve("./mymodule")];
// Delete file and create directory with index.js
fs.rmSync(path.join(__dirname, "mymodule.js"));
fs.mkdirSync(path.join(__dirname, "mymodule"));
fs.writeFileSync(path.join(__dirname, "mymodule", "index.js"), "module.exports = { type: 'directory' };");
// Second require - will FAIL because Node.js cached the resolution path
// It still tries to load from mymodule.js even though it's now a directory
try {
const result2 = require("./mymodule");
console.log("ERROR: Second require should have failed but got:", result2.type);
process.exit(1);
} catch (e) {
console.log("Second require failed as expected (cached resolution path)");
}
console.log("SUCCESS: Confirmed resolution path caching");
`;
await fs.writeFile(path.join(tmpDir, "test.js"), testScript);
await fs.writeFile(path.join(tmpDir, "mymodule.js"), `module.exports = { type: "file" };`);
// Run with Node.js
await using nodeProc = Bun.spawn({
cmd: ["node", "test.js"],
cwd: tmpDir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [nodeStdout, nodeStderr, nodeExit] = await Promise.all([
nodeProc.stdout.text(),
nodeProc.stderr.text(),
nodeProc.exited,
]);
const nodeSuccess = nodeExit === 0;
// Reset to initial state before running Bun
try {
await fs.rm(path.join(tmpDir, "mymodule"), { recursive: true });
} catch (e) {}
await fs.writeFile(path.join(tmpDir, "mymodule.js"), `module.exports = { type: "file" };`);
// Run with Bun
await using bunProc = Bun.spawn({
cmd: [bunExe(), "test.js"],
cwd: tmpDir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [bunStdout, bunStderr, bunExit] = await Promise.all([
bunProc.stdout.text(),
bunProc.stderr.text(),
bunProc.exited,
]);
const bunSuccess = bunExit === 0;
// Only log on failure
if (!nodeSuccess || !bunSuccess) {
console.log("\n=== Node.js output ===");
console.log(nodeStdout);
if (!nodeSuccess) {
console.log("Node.js stderr:", nodeStderr);
}
console.log("\n=== Bun output ===");
console.log(bunStdout);
if (!bunSuccess) {
console.log("Bun stderr:", bunStderr);
}
}
// Both should handle resolution path caching the same way
expect(nodeSuccess).toBe(true);
expect(bunSuccess).toBe(true);
if (nodeSuccess !== bunSuccess) {
throw new Error(
`Behavior mismatch! Node.js ${nodeSuccess ? "passed" : "failed"} but Bun ${bunSuccess ? "passed" : "failed"}`,
);
}
} finally {
await fs.rm(tmpDir, { recursive: true, force: true });
}
});
});