Compare commits

...

12 Commits

Author SHA1 Message Date
Jarred Sumner
c98fb41271 Merge branch 'main' into claude/comprehensive-banner-sourcemap-tests 2025-11-01 22:44:12 -07:00
Jarred Sumner
20a4016303 Merge branch 'main' into claude/comprehensive-banner-sourcemap-tests 2025-10-19 21:34:55 -07:00
Jarred Sumner
68d7552ea0 Update no-validate-exceptions.txt 2025-10-19 21:34:28 -07:00
Claude Bot
0689feb2b7 Add bundler_banner_sourcemap test to no-validate-exceptions.txt
The test triggers reifyStaticProperties which calls PropertyCallback
functions that currently have DECLARE_THROW_SCOPE. This causes
exception validation errors when PropertyCallbacks throw sequentially
without checks between them.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 18:53:46 +00:00
Claude Bot
63faa5f2c3 Add exception checks to NodeModuleModule PropertyCallback functions
Add DECLARE_THROW_SCOPE + RETURN_IF_EXCEPTION to getBuiltinModulesObject()
and getGlobalPathsObject() which call constructArray/constructEmptyArray.

These functions can throw exceptions that need to be checked.

Note: With BUN_JSC_validateExceptionChecks=1, this still fails because
reifyAllStaticProperties() calls multiple PropertyCallbacks without
checking exceptions between them. The exception from getBuiltinModulesObject
is unchecked when getGlobalPathsObject declares its THROW_SCOPE.

TODO: Investigate if DECLARE_CATCH_SCOPE is more appropriate here, or if
reifyAllStaticProperties needs modification.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 18:34:47 +00:00
Claude Bot
c0865cbe7a Fix exception scope validation in NodeModuleModule property callbacks
Add DECLARE_CATCH_SCOPE and EXCEPTION_ASSERT_UNUSED in property callback
functions (getGlobalPathsObject and getBuiltinModulesObject) that call
constructEmptyArray and constructArray.

These PropertyCallback functions are called during reifyAllStaticProperties,
which is itself called from generateNativeModule_NodeModule where a
CATCH_SCOPE clears any exceptions (lines 508-514). Using CATCH_SCOPE here
documents that exceptions are expected and handled by the caller.

The EXCEPTION_ASSERT_UNUSED ensures that if an exception occurs, the
returned array is null, which is the correct behavior.

Fixes exception validation errors when running with:
BUN_JSC_validateExceptionChecks=1 BUN_JSC_dumpSimulatedThrows=1

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 18:25:07 +00:00
Claude Bot
0cf69239d0 Address final review comments
- Use await using with await tempDir() for proper async disposal
- Fix result.logs formatting to show meaningful error messages (map objects to strings)
- Restrict banner content assertions to entry-point chunks only
- Verify external sourcemaps don't include sourceMappingURL in JS output

All 576 tests passing with 13,020 assertions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 04:02:40 +00:00
Claude Bot
9139cf61db Fix line number references in comments and assertions
Corrected expected line numbers to match actual source files:
- flush() comment: line 35 (0-indexed: 34) [was: line 42, 0-indexed: 41]
- this.buffer.push comment: line 32 (0-indexed: 31) [was: line 39, 0-indexed: 38]
- connect() assertion: line 7 (0-indexed) [was: 11]
- validateEmail assertion: line 27 (0-indexed) [was: 31]

All comments now accurately reflect the actual line numbers in the
test file constants.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 03:58:09 +00:00
Claude Bot
74214cd188 Add banner content presence validation
Verify that:
- All non-shebang banner lines appear in output
- Shebang appears as first line of entry-point chunks (when present)

This ensures banners are correctly applied to all outputs, not just
validated via sourcemaps.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 03:50:36 +00:00
Claude Bot
fd3f1b5007 Use test.concurrent() for banner sourcemap tests
All 576 tests are independent and can run concurrently, reducing
wall time from ~60s to ~45s (25% speedup).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 03:47:02 +00:00
Claude Bot
8406ab74d0 Address code review feedback for banner sourcemap test
Fixes:
- Use basename() instead of split("/") for cross-platform path handling
- Filter outputs by kind !== "sourcemap" instead of kind === "chunk"
- Add charset parameter support in inline sourcemap regex
- Add comprehensive sourcemap structure validation
- Skip loadUtils test when minifyIdentifiers is enabled (export aliases don't have sourcemap entries)
- Use minification-resistant regex patterns throughout
- Ensure at least one anchor match per chunk type

All 576 tests now passing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 03:31:29 +00:00
Claude Bot
1ea35e8c18 Add comprehensive sourcemap tests for banner option
This adds 576 individual test cases covering all combinations of:
- Formats: cjs, esm, iife
- Targets: bun, node, browser
- Sourcemap types: inline, external, linked
- Code splitting: true/false (ESM only)
- Minification: none, identifiers, whitespace, syntax
- Banner types: simple, multiline, shebang-start, shebang-end

Each test validates that banners don't corrupt sourcemaps by:
1. Building with the specified configuration
2. Verifying Bun-specific directives are present
3. Parsing sourcemaps with node:module's SourceMap API
4. Checking line/column mappings at multiple points in the code
5. Testing complex code with classes, async/await, dynamic imports

The tests use realistic multi-file scenarios including:
- Base and derived classes with this bindings
- Factory patterns with closures
- Dynamic imports for code splitting
- Template literals and ES6 features

All 576 tests pass with 4,788 assertions total.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 02:56:11 +00:00
6 changed files with 1101 additions and 32 deletions

View File

@@ -586,7 +586,10 @@ static JSValue getBuiltinModulesObject(VM& vm, JSObject* moduleObject)
}
auto* globalObject = defaultGlobalObject(moduleObject->globalObject());
return JSC::constructArray(globalObject, static_cast<JSC::ArrayAllocationProfile*>(nullptr), JSC::ArgList(args));
auto scope = DECLARE_THROW_SCOPE(vm);
auto* array = JSC::constructArray(globalObject, static_cast<JSC::ArrayAllocationProfile*>(nullptr), JSC::ArgList(args));
RETURN_IF_EXCEPTION(scope, {});
return array;
}
static JSValue getConstantsObject(VM& vm, JSObject* moduleObject)
@@ -614,9 +617,13 @@ static JSValue getConstantsObject(VM& vm, JSObject* moduleObject)
static JSValue getGlobalPathsObject(VM& vm, JSObject* moduleObject)
{
return JSC::constructEmptyArray(
moduleObject->globalObject(),
auto* globalObject = moduleObject->globalObject();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* array = JSC::constructEmptyArray(
globalObject,
static_cast<ArrayAllocationProfile*>(nullptr), 0);
RETURN_IF_EXCEPTION(scope, {});
return array;
}
JSC_DEFINE_HOST_FUNCTION(jsFunctionSetCJSWrapperItem, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame))

View File

@@ -0,0 +1,432 @@
import { expect, test } from "bun:test";
import { tempDir } from "harness";
import { SourceMap } from "node:module";
import { basename, join } from "path";
// Define test file contents as constants
const testFiles = {
"input.js": `// Complex test file with classes, methods, and exports
import { readFileSync } from "fs";
// Base class with properties
export class Logger {
constructor(name) {
this.name = name;
this.level = "info";
}
log(message) {
console.log(\`[\${this.name}] \${message}\`);
}
debug(message) {
if (this.level === "debug") {
console.debug(\`[DEBUG][\${this.name}] \${message}\`);
}
}
}
// Derived class
export class FileLogger extends Logger {
constructor(name, filepath) {
super(name);
this.filepath = filepath;
this.buffer = [];
}
log(message) {
super.log(message);
this.buffer.push(message);
}
flush() {
const content = this.buffer.join("\\n");
console.log("Flushing to file:", this.filepath);
return content;
}
}
// Standalone function using this
export function createCounter(initial = 0) {
return {
value: initial,
increment() {
this.value++;
return this.value;
},
decrement() {
this.value--;
return this.value;
},
reset() {
this.value = initial;
}
};
}
// Factory with private state
export const loggerFactory = (() => {
const instances = new Map();
return {
create(name) {
if (!instances.has(name)) {
instances.set(name, new Logger(name));
}
return instances.get(name);
},
destroy(name) {
instances.delete(name);
}
};
})();
// Default export
export default class Application {
constructor(config) {
this.config = config;
this.logger = new Logger("App");
this.running = false;
}
start() {
this.running = true;
this.logger.log("Application started");
return this;
}
stop() {
this.running = false;
this.logger.log("Application stopped");
}
}
`,
// Additional file for dynamic import testing
"utils.js": `// Utility module for dynamic imports
export class DatabaseConnection {
constructor(connectionString) {
this.connectionString = connectionString;
this.connected = false;
}
async connect() {
console.log("Connecting to database...");
this.connected = true;
return this;
}
async query(sql) {
if (!this.connected) {
throw new Error("Not connected");
}
console.log("Executing query:", sql);
return { rows: [], count: 0 };
}
disconnect() {
this.connected = false;
console.log("Disconnected from database");
}
}
export function validateEmail(email) {
const regex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;
return regex.test(email);
}
export const constants = {
MAX_RETRIES: 3,
TIMEOUT: 5000,
API_VERSION: "v1"
};
`,
// Entry point with dynamic imports
"main.js": `// Main entry point with dynamic imports
import { Logger } from "./input.js";
const logger = new Logger("Main");
// Dynamic import for code splitting
async function loadUtils() {
const utils = await import("./utils.js");
const db = new utils.DatabaseConnection("postgres://localhost");
await db.connect();
logger.log("Utils loaded and DB connected");
return utils;
}
async function validateUser(email) {
const { validateEmail } = await import("./utils.js");
const isValid = validateEmail(email);
logger.log(\`Email \${email} is \${isValid ? "valid" : "invalid"}\`);
return isValid;
}
// Another dynamic import point
async function initializeApp() {
const { default: Application } = await import("./input.js");
const app = new Application({ name: "MyApp" });
app.start();
return app;
}
export { loadUtils, validateUser, initializeApp };
`,
};
const formats = ["cjs", "esm", "iife"] as const;
const targets = ["bun", "node", "browser"] as const;
const sourcemaps = ["inline", "external", "linked"] as const;
const splittingOptions = [false, true] as const;
const minifyOptions = [
{ name: "none", minifyIdentifiers: false, minifyWhitespace: false, minifySyntax: false },
{ name: "identifiers", minifyIdentifiers: true, minifyWhitespace: false, minifySyntax: false },
{ name: "whitespace", minifyIdentifiers: false, minifyWhitespace: true, minifySyntax: false },
{ name: "syntax", minifyIdentifiers: false, minifyWhitespace: false, minifySyntax: true },
] as const;
const banners = [
{ name: "simple", content: "// This is a banner comment\n// Line 2 of banner" },
{ name: "multiline", content: "// Multi-line banner\n// Line 2\n// Line 3\n// Line 4\n// Line 5" },
{ name: "shebang-start", content: "#!/usr/bin/env node\n// Banner after shebang\n// Line 3" },
{ name: "shebang-end", content: "// Banner before shebang\n// Line 2\n#!/usr/bin/env node" },
] as const;
for (const format of formats) {
for (const target of targets) {
for (const sourcemap of sourcemaps) {
for (const splitting of splittingOptions) {
for (const minify of minifyOptions) {
for (const banner of banners) {
// Code splitting only works with ESM format
if (splitting && format !== "esm") {
continue;
}
const testName = `format=${format}, target=${target}, sourcemap=${sourcemap}, splitting=${splitting}, minify=${minify.name}, banner=${banner.name}`;
test.concurrent(testName, async () => {
// Create temp directory for this test
await using dir = await tempDir(`banner-sourcemap-${format}-${target}-${sourcemap}`, testFiles);
// Build with banner
const entrypoint = splitting ? join(dir, "main.js") : join(dir, "input.js");
const result = await Bun.build({
entrypoints: [entrypoint],
outdir: dir,
naming: splitting
? `split-${target}-${sourcemap}-${minify.name}-${banner.name}/[name].[ext]`
: `output-${format}-${target}-${sourcemap}-${minify.name}-${banner.name}.js`,
format,
target,
sourcemap,
splitting,
minify: {
identifiers: minify.minifyIdentifiers,
whitespace: minify.minifyWhitespace,
syntax: minify.minifySyntax,
},
banner: banner.content,
});
expect(
result.success,
`${testName}: build failed\n${result.logs.map(log => (typeof log === "string" ? log : log.message || log.text || JSON.stringify(log))).join("\n")}`,
).toBe(true);
// Always filter to JS chunks only (not assets or sourcemaps)
// kind can be "entry-point", "chunk", etc. but not "asset" or "sourcemap"
const outputsToCheck = result.outputs.filter(o => o.kind !== "sourcemap" && o.path.endsWith(".js"));
expect(outputsToCheck.length, `${testName}: no JS outputs found`).toBeGreaterThan(0);
for (const output of outputsToCheck) {
const outputCode = await output.text();
const outfile = output.path;
const chunkName = splitting ? ` (chunk: ${basename(output.path)})` : "";
const chunkTestName = `${testName}${chunkName}`;
// Verify Bun-specific directives for target=bun
if (target === "bun") {
if (format === "cjs") {
expect(outputCode, `${chunkTestName}: should contain // @bun @bun-cjs directive`).toContain(
"// @bun @bun-cjs",
);
} else if (format === "esm") {
expect(outputCode, `${chunkTestName}: should contain // @bun directive`).toContain("// @bun");
// Make sure it's not the CJS variant
expect(outputCode, `${chunkTestName}: should not contain @bun-cjs for ESM`).not.toContain(
"@bun-cjs",
);
}
}
// Verify banner presence - only for entry-point chunks
// Non-entry chunks (helpers/runtime) may not receive banners
if (output.kind === "entry-point") {
const nonShebangBannerLines = banner.content
.split("\n")
.filter(l => !l.startsWith("#!"))
.filter(l => l.trim().length > 0);
for (const line of nonShebangBannerLines) {
expect(outputCode, `${chunkTestName}: banner line missing: ${JSON.stringify(line)}`).toContain(
line,
);
}
// Shebang (if present at start) should be the very first line
if (banner.name === "shebang-start") {
expect(outputCode.startsWith("#!"), `${chunkTestName}: shebang should be first line`).toBe(true);
}
}
// Extract sourcemap based on type
let sourcemapData: string;
if (sourcemap === "inline") {
// Extract inline sourcemap from data URL (accept optional charset)
const match = outputCode.match(
/\/\/# sourceMappingURL=data:application\/json(?:;charset=[^;]+)?;base64,([^\s]+)/,
);
expect(match, `${chunkTestName}: inline sourcemap not found`).not.toBeNull();
sourcemapData = Buffer.from(match![1], "base64").toString("utf-8");
} else if (sourcemap === "linked") {
// Verify sourceMappingURL comment exists
expect(outputCode, `${chunkTestName}: linked sourcemap comment not found`).toMatch(
/\/\/# sourceMappingURL=.*\.js\.map/,
);
const mapfile = `${outfile}.map`;
sourcemapData = await Bun.file(mapfile).text();
} else {
// external - verify no sourceMappingURL in JS output
expect(
outputCode,
`${chunkTestName}: external sourcemap should not have sourceMappingURL comment in JS`,
).not.toMatch(/\/\/[#@] sourceMappingURL=/);
const mapfile = `${outfile}.map`;
sourcemapData = await Bun.file(mapfile).text();
}
// Parse and validate sourcemap structure
const sourceMapObj = JSON.parse(sourcemapData);
expect(typeof sourceMapObj, `${chunkTestName}: sourcemap should be an object`).toBe("object");
expect(sourceMapObj, `${chunkTestName}: sourcemap should not be null`).not.toBeNull();
expect(Number.isInteger(sourceMapObj.version), `${chunkTestName}: version should be an integer`).toBe(
true,
);
expect(sourceMapObj.version, `${chunkTestName}: version should be 3`).toBe(3);
expect(Array.isArray(sourceMapObj.sources), `${chunkTestName}: sources should be an array`).toBe(true);
// Skip runtime helper chunks (chunks with no sources - these are generated code)
if (!sourceMapObj.sources || sourceMapObj.sources.length === 0 || !sourceMapObj.mappings) {
// This is expected for runtime helper chunks in code splitting
continue;
}
expect(sourceMapObj.mappings, `${chunkTestName}: mappings should be a non-empty string`).toBeTruthy();
expect(typeof sourceMapObj.mappings, `${chunkTestName}: mappings should be string type`).toBe("string");
// Use node:module SourceMap to validate
const sm = new SourceMap(sourceMapObj);
// The banner should NOT affect the source mapping
// Different checks for different chunks
const isInputChunk = outfile.includes("input") || !splitting;
const isUtilsChunk = outfile.includes("utils");
const isMainChunk = outfile.includes("main");
// Test mappings based on which chunk we're in - require at least one anchor match
if (isInputChunk) {
// Test 1: Check mapping in the middle of the file - the flush() method (line 35, 0-indexed: 34)
// Use minification-resistant pattern
const flushMatch = outputCode.match(/flush\s*\(/);
expect(flushMatch, `${chunkTestName}: flush method not found in input chunk`).not.toBeNull();
const flushIndex = flushMatch!.index!;
const linesBeforeFlush = outputCode.substring(0, flushIndex).split("\n").length;
const flushLineStart = outputCode.lastIndexOf("\n", flushIndex - 1) + 1;
const flushColumn = flushIndex - flushLineStart;
const flushPosition = sm.findEntry(linesBeforeFlush - 1, flushColumn);
expect(
flushPosition?.originalLine,
`${chunkTestName}: flush() should map to original line 34 (0-indexed), got ${flushPosition?.originalLine}`,
).toBe(34);
expect(flushPosition?.originalSource, `${chunkTestName}: source should be input.js`).toMatch(
/input\.js$/,
);
// Test 2: Check mapping for this.buffer.push (line 32, 0-indexed: 31)
// Use minification-resistant pattern - match the call structure, not argument names
const bufferPushMatch = outputCode.match(/this\.buffer\.push\s*\(/);
if (bufferPushMatch) {
const bufferPushIndex = bufferPushMatch.index!;
const linesBeforePush = outputCode.substring(0, bufferPushIndex).split("\n").length;
const pushLineStart = outputCode.lastIndexOf("\n", bufferPushIndex - 1) + 1;
const pushColumn = bufferPushIndex - pushLineStart;
const pushPosition = sm.findEntry(linesBeforePush - 1, pushColumn);
expect(
pushPosition?.originalLine,
`${chunkTestName}: this.buffer.push should map to original line 31 (0-indexed), got ${pushPosition?.originalLine}`,
).toBe(31);
}
}
if (isUtilsChunk) {
// Test for utils.js - connect() at line 8 (0-indexed: 7)
const connectMatch = outputCode.match(/\bconnect\s*\(/);
expect(connectMatch, `${chunkTestName}: connect method not found in utils chunk`).not.toBeNull();
const connectIndex = connectMatch!.index!;
const linesBeforeConnect = outputCode.substring(0, connectIndex).split("\n").length;
const connectLineStart = outputCode.lastIndexOf("\n", connectIndex - 1) + 1;
const connectColumn = connectIndex - connectLineStart;
const connectPosition = sm.findEntry(linesBeforeConnect - 1, connectColumn);
expect(
connectPosition?.originalLine,
`${chunkTestName}: connect() should map to utils.js line 7 (0-indexed), got ${connectPosition?.originalLine}`,
).toBe(7);
// Test validateEmail - match identifier only (line 28, 0-indexed: 27)
const validateMatch = outputCode.match(/\bvalidateEmail\b/);
if (validateMatch) {
const validateIndex = validateMatch.index!;
const linesBeforeValidate = outputCode.substring(0, validateIndex).split("\n").length;
const validateLineStart = outputCode.lastIndexOf("\n", validateIndex - 1) + 1;
const validateColumn = validateIndex - validateLineStart;
const validatePosition = sm.findEntry(linesBeforeValidate - 1, validateColumn);
expect(
validatePosition?.originalLine,
`${chunkTestName}: validateEmail should map to utils.js line 27 (0-indexed), got ${validatePosition?.originalLine}`,
).toBe(27);
}
}
if (isMainChunk) {
// Test for main.js - skip if identifiers are minified
// With minifyIdentifiers, the function name is mangled and "loadUtils" only exists as an export alias
// which doesn't have a sourcemap entry
if (!minify.minifyIdentifiers) {
const loadUtilsMatch = outputCode.match(/\bloadUtils\b/);
if (loadUtilsMatch) {
const loadUtilsIndex = loadUtilsMatch.index!;
const linesBeforeLoadUtils = outputCode.substring(0, loadUtilsIndex).split("\n").length;
const loadUtilsLineStart = outputCode.lastIndexOf("\n", loadUtilsIndex - 1) + 1;
const loadUtilsColumn = loadUtilsIndex - loadUtilsLineStart;
const loadUtilsPosition = sm.findEntry(linesBeforeLoadUtils - 1, loadUtilsColumn);
expect(
loadUtilsPosition?.originalLine,
`${chunkTestName}: loadUtils should map to main.js line 6 (0-indexed), got ${loadUtilsPosition?.originalLine}`,
).toBe(6);
}
}
}
}
});
}
}
}
}
}
}

View File

@@ -0,0 +1,276 @@
import { expect, test } from "bun:test";
import { tempDir } from "harness";
import { SourceMap } from "node:module";
import { join } from "path";
test("dual package hazard rewrites should preserve correct sourcemaps", async () => {
// Create a dual package hazard scenario:
// - package.json with both "main" (CJS) and "module" (ESM)
// - One file imports with ESM, another requires with CJS
// - This triggers scanForSecondaryPaths rewriting
await using dir = await tempDir("dual-package-hazard-sourcemap", {
"package.json": JSON.stringify({
name: "test-dual-package-hazard",
type: "module",
}),
// The dual package - has both ESM and CJS versions
"node_modules/dual-pkg/package.json": JSON.stringify({
name: "dual-pkg",
main: "./cjs-entry.js",
module: "./esm-entry.js",
}),
// ESM version of the package
"node_modules/dual-pkg/esm-entry.js": `export function hello() {
console.log("Hello from ESM");
return "esm-result";
}`,
// CJS version of the package
"node_modules/dual-pkg/cjs-entry.js": `module.exports = function hello() {
console.log("Hello from CJS");
return "cjs-result";
};`,
// File that imports with ESM
"esm-importer.js": `import { hello } from "dual-pkg";
export function callHelloESM() {
return hello();
}`,
// File that requires with CJS (use dynamic import to trigger dual package hazard)
"cjs-importer.js": `const hello = require("dual-pkg");
export function callHelloCJS() {
return hello();
}`,
// Entry point that uses both
"index.js": `import { callHelloESM } from "./esm-importer.js";
import { callHelloCJS } from "./cjs-importer.js";
console.log("ESM:", callHelloESM());
console.log("CJS:", callHelloCJS());
`,
});
// Build with sourcemaps
const result = await Bun.build({
entrypoints: [join(dir, "index.js")],
outdir: join(dir, "out"),
format: "esm",
target: "bun",
sourcemap: "external",
minify: false,
});
expect(result.success).toBe(true);
// Find the output file
const outputFile = result.outputs.find(o => o.kind === "entry-point" && o.path.endsWith(".js"));
expect(outputFile).toBeDefined();
const outputCode = await outputFile!.text();
const mapData = await Bun.file(outputFile!.path + ".map").text();
const sourceMapObj = JSON.parse(mapData);
const sm = new SourceMap(sourceMapObj);
console.log("Sources:", sourceMapObj.sources);
console.log("\n=== Output Code ===");
console.log(outputCode);
// Find callHelloESM in the output
const callHelloESMMatch = outputCode.match(/callHelloESM\s*\(/);
if (callHelloESMMatch) {
const index = callHelloESMMatch.index!;
const linesBeforeMatch = outputCode.substring(0, index).split("\n").length;
const lineStart = outputCode.lastIndexOf("\n", index - 1) + 1;
const column = index - lineStart;
const position = sm.findEntry(linesBeforeMatch - 1, column);
console.log("\ncallHelloESM mapping:", position);
// Verify it maps to the correct source file (esm-importer.js, not index.js or cjs-importer.js)
expect(position?.originalSource).toMatch(/esm-importer\.js$/);
}
// Find callHelloCJS in the output
const callHelloCJSMatch = outputCode.match(/callHelloCJS\s*\(/);
if (callHelloCJSMatch) {
const index = callHelloCJSMatch.index!;
const linesBeforeMatch = outputCode.substring(0, index).split("\n").length;
const lineStart = outputCode.lastIndexOf("\n", index - 1) + 1;
const column = index - lineStart;
const position = sm.findEntry(linesBeforeMatch - 1, column);
console.log("callHelloCJS mapping:", position);
// Verify it maps to the correct source file (cjs-importer.js, not index.js or esm-importer.js)
expect(position?.originalSource).toMatch(/cjs-importer\.js$/);
}
// Find the hello function call from the dual package
// After dual package hazard resolution, both should point to the same file (CJS version)
const helloMatches = Array.from(outputCode.matchAll(/\bhello\s*\(/g));
console.log(`\nFound ${helloMatches.length} hello() calls`);
for (const match of helloMatches) {
const index = match.index!;
const linesBeforeMatch = outputCode.substring(0, index).split("\n").length;
const lineStart = outputCode.lastIndexOf("\n", index - 1) + 1;
const column = index - lineStart;
const position = sm.findEntry(linesBeforeMatch - 1, column);
console.log(`hello() at output line ${linesBeforeMatch}:`, position);
// The key assertion: sourcemap should point to the ACTUAL source file
// not a misaligned file due to source_index changes in scanForSecondaryPaths
if (position?.originalSource) {
// Should map to either esm-importer.js, cjs-importer.js, or the dual-pkg files
// NOT to index.js (which would indicate source_index misalignment)
const isValidSource =
position.originalSource.includes("esm-importer.js") ||
position.originalSource.includes("cjs-importer.js") ||
position.originalSource.includes("esm-entry.js") ||
position.originalSource.includes("cjs-entry.js");
expect(isValidSource, `hello() should not map to wrong source file: ${position.originalSource}`).toBe(true);
}
}
});
test("dual package hazard with tslib scenario", async () => {
// Reproduce the tslib scenario mentioned by the user
// tslib has dual package hazard and is commonly bundled
await using dir = await tempDir("tslib-dual-package-sourcemap", {
"package.json": JSON.stringify({
name: "test-tslib-scenario",
type: "module",
}),
// Simulate tslib with dual package hazard
"node_modules/tslib/package.json": JSON.stringify({
name: "tslib",
main: "./tslib.js",
module: "./tslib.es6.js",
}),
"node_modules/tslib/tslib.js": `// TypeScript runtime library (CJS)
exports.__extends = function (d, b) {
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
exports.__assign = function () {
exports.__assign = Object.assign || function (t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return exports.__assign.apply(this, arguments);
};`,
"node_modules/tslib/tslib.es6.js": `// TypeScript runtime library (ESM)
export function __extends(d, b) {
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
export var __assign = Object.assign || function (t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};`,
// File using tslib with ESM import
"class-a.ts": `import { __extends } from "tslib";
class BaseClass {
constructor(public name: string) {}
}
class DerivedClass extends BaseClass {
constructor(name: string, public value: number) {
super(name);
}
}
export { DerivedClass };`,
// File using tslib with CJS require
"class-b.js": `const tslib = require("tslib");
function createObject(base, overrides) {
return tslib.__assign({}, base, overrides);
}
module.exports = { createObject };`,
// Entry point
"index.js": `import { DerivedClass } from "./class-a.ts";
const { createObject } = require("./class-b.js");
const obj = new DerivedClass("test", 42);
const merged = createObject({ a: 1 }, { b: 2 });
console.log(obj, merged);
`,
});
const result = await Bun.build({
entrypoints: [join(dir, "index.js")],
outdir: join(dir, "out"),
format: "esm",
target: "bun",
sourcemap: "external",
minify: false,
});
expect(result.success).toBe(true);
const outputFile = result.outputs.find(o => o.kind === "entry-point" && o.path.endsWith(".js"));
expect(outputFile).toBeDefined();
const outputCode = await outputFile!.text();
const mapData = await Bun.file(outputFile!.path + ".map").text();
const sourceMapObj = JSON.parse(mapData);
const sm = new SourceMap(sourceMapObj);
console.log("\n=== tslib scenario ===");
console.log("Sources:", sourceMapObj.sources);
// Find __extends usage
const extendsMatch = outputCode.match(/__extends\s*\(/);
if (extendsMatch) {
const index = extendsMatch.index!;
const linesBeforeMatch = outputCode.substring(0, index).split("\n").length;
const lineStart = outputCode.lastIndexOf("\n", index - 1) + 1;
const column = index - lineStart;
const position = sm.findEntry(linesBeforeMatch - 1, column);
console.log("__extends mapping:", position);
// Should map to tslib or class-a.ts, not to a wrong file due to source_index misalignment
if (position?.originalSource) {
expect(
position.originalSource,
"tslib functions should map to correct source after dual package hazard resolution",
).toMatch(/tslib|class-a\.ts/);
}
}
// Find __assign usage
const assignMatch = outputCode.match(/__assign\s*\(/);
if (assignMatch) {
const index = assignMatch.index!;
const linesBeforeMatch = outputCode.substring(0, index).split("\n").length;
const lineStart = outputCode.lastIndexOf("\n", index - 1) + 1;
const column = index - lineStart;
const position = sm.findEntry(linesBeforeMatch - 1, column);
console.log("__assign mapping:", position);
if (position?.originalSource) {
expect(
position.originalSource,
"tslib functions should map to correct source after dual package hazard resolution",
).toMatch(/tslib|class-b\.js/);
}
}
});

View File

@@ -0,0 +1,263 @@
import { expect, test } from "bun:test";
import { tempDir } from "harness";
import { SourceMap } from "node:module";
import { join } from "path";
// This test tries to reproduce the scenario where source_index changes
// in scanForSecondaryPaths could lead to incorrect sourcemap source arrays
test("dual package hazard with multiple files - source_index alignment", async () => {
// The key insight: if source_index changes but sourcemap source arrays aren't updated,
// mappings could point to the wrong file in the sources array
await using dir = await tempDir("dual-pkg-complex-sourcemap", {
"package.json": JSON.stringify({
name: "test-complex-dual-pkg",
type: "module",
}),
// Dual package with distinctive content in each version
"node_modules/pkg/package.json": JSON.stringify({
name: "pkg",
main: "./index.cjs",
module: "./index.mjs",
}),
"node_modules/pkg/index.mjs": `// ESM VERSION - DISTINCTIVE MARKER
export function doSomethingESM() {
console.log("This is the ESM version");
return { type: "esm", value: 100 };
}
export function helperESM() {
console.log("Helper from ESM");
return "esm-helper";
}`,
"node_modules/pkg/index.cjs": `// CJS VERSION - DISTINCTIVE MARKER
module.exports = {
doSomethingCJS: function() {
console.log("This is the CJS version");
return { type: "cjs", value: 200 };
},
helperCJS: function() {
console.log("Helper from CJS");
return "cjs-helper";
}
};`,
// File A - uses ESM import
"file-a.js": `import { doSomethingESM, helperESM } from "pkg";
export function functionA() {
const resultESM = doSomethingESM();
const helperResultESM = helperESM();
return { resultESM, helperResultESM };
}
export function anotherFunctionA() {
return "function-a-marker";
}`,
// File B - uses CJS require
"file-b.js": `const pkg = require("pkg");
export function functionB() {
const resultCJS = pkg.doSomethingCJS();
const helperResultCJS = pkg.helperCJS();
return { resultCJS, helperResultCJS };
}
export function anotherFunctionB() {
return "function-b-marker";
}`,
// File C - neutral file (no dual package usage)
"file-c.js": `export function functionC() {
console.log("File C - neutral");
return "file-c-result";
}
export function helperC() {
return "helper-c";
}`,
// Entry point that imports all
"index.js": `import { functionA, anotherFunctionA } from "./file-a.js";
import { functionB, anotherFunctionB } from "./file-b.js";
import { functionC, helperC } from "./file-c.js";
console.log("Testing dual package hazard sourcemaps");
const a = functionA();
const b = functionB();
const c = functionC();
console.log({ a, b, c });
console.log(anotherFunctionA(), anotherFunctionB(), helperC());
`,
});
const result = await Bun.build({
entrypoints: [join(dir, "index.js")],
outdir: join(dir, "out"),
format: "esm",
target: "bun",
sourcemap: "external",
minify: false,
});
expect(result.success).toBe(true);
const outputFile = result.outputs.find(o => o.kind === "entry-point" && o.path.endsWith(".js"));
expect(outputFile).toBeDefined();
const outputCode = await outputFile!.text();
const mapData = await Bun.file(outputFile!.path + ".map").text();
const sourceMapObj = JSON.parse(mapData);
const sm = new SourceMap(sourceMapObj);
console.log("\n=== Complex Dual Package Scenario ===");
console.log("Sources array:", sourceMapObj.sources);
console.log("\nOutput has", outputCode.split("\n").length, "lines");
// Test 1: functionA should map to file-a.js
const functionAMatch = outputCode.match(/function\s+functionA\s*\(/);
if (functionAMatch) {
const index = functionAMatch.index!;
const linesBeforeMatch = outputCode.substring(0, index).split("\n").length;
const lineStart = outputCode.lastIndexOf("\n", index - 1) + 1;
const column = index - lineStart;
const position = sm.findEntry(linesBeforeMatch - 1, column);
console.log("\nfunctionA mapping:", position);
expect(position?.originalSource, "functionA should map to file-a.js").toMatch(/file-a\.js$/);
}
// Test 2: functionB should map to file-b.js
const functionBMatch = outputCode.match(/function\s+functionB\s*\(/);
if (functionBMatch) {
const index = functionBMatch.index!;
const linesBeforeMatch = outputCode.substring(0, index).split("\n").length;
const lineStart = outputCode.lastIndexOf("\n", index - 1) + 1;
const column = index - lineStart;
const position = sm.findEntry(linesBeforeMatch - 1, column);
console.log("functionB mapping:", position);
expect(position?.originalSource, "functionB should map to file-b.js").toMatch(/file-b\.js$/);
}
// Test 3: functionC should map to file-c.js
const functionCMatch = outputCode.match(/function\s+functionC\s*\(/);
if (functionCMatch) {
const index = functionCMatch.index!;
const linesBeforeMatch = outputCode.substring(0, index).split("\n").length;
const lineStart = outputCode.lastIndexOf("\n", index - 1) + 1;
const column = index - lineStart;
const position = sm.findEntry(linesBeforeMatch - 1, column);
console.log("functionC mapping:", position);
expect(position?.originalSource, "functionC should map to file-c.js").toMatch(/file-c\.js$/);
}
// Test 4: Check that dual package references map correctly
// After scanForSecondaryPaths, both ESM and CJS imports should resolve to index.cjs
const doSomethingMatches = Array.from(outputCode.matchAll(/doSomething(ESM|CJS)\s*\(/g));
console.log(`\nFound ${doSomethingMatches.length} doSomething calls`);
for (const match of doSomethingMatches) {
const index = match.index!;
const linesBeforeMatch = outputCode.substring(0, index).split("\n").length;
const lineStart = outputCode.lastIndexOf("\n", index - 1) + 1;
const column = index - lineStart;
const position = sm.findEntry(linesBeforeMatch - 1, column);
console.log(`${match[0]} at line ${linesBeforeMatch}:`, position);
// Critical: After dual package hazard resolution, the sourcemap should still
// point to the correct original source file (file-a.js or file-b.js)
// NOT to the dual package file itself (which could indicate source_index misalignment)
if (position?.originalSource) {
const isInUserFile =
position.originalSource.includes("file-a.js") || position.originalSource.includes("file-b.js");
if (!isInUserFile) {
// If it maps to the pkg files, that's okay too, but verify it's the RIGHT pkg file
const isInPkg = position.originalSource.includes("pkg/");
if (isInPkg) {
console.log(" -> Maps to pkg file (expected after dual package hazard resolution)");
}
}
}
}
// Test 5: Verify source array integrity
// All sources should be valid file paths, no undefined or duplicates
const sources = sourceMapObj.sources as string[];
expect(sources.length).toBeGreaterThan(0);
for (const source of sources) {
expect(typeof source, "All sources should be strings").toBe("string");
expect(source.length, "All sources should be non-empty").toBeGreaterThan(0);
}
// Check for unexpected duplicates (could indicate source_index issues)
const uniqueSources = new Set(sources);
if (uniqueSources.size !== sources.length) {
console.warn("WARNING: Duplicate sources detected:", sources);
}
console.log("\n✓ All sourcemap assertions passed");
});
test("banner + dual package hazard interaction", async () => {
// Test that banner doesn't interfere with dual package hazard sourcemap handling
await using dir = await tempDir("banner-dual-pkg-sourcemap", {
"package.json": JSON.stringify({
name: "test-banner-dual-pkg",
type: "module",
}),
"node_modules/lib/package.json": JSON.stringify({
name: "lib",
main: "./index.cjs",
module: "./index.mjs",
}),
"node_modules/lib/index.mjs": `export const value = "esm";`,
"node_modules/lib/index.cjs": `module.exports = { value: "cjs" };`,
"a.js": `import { value } from "lib";\nexport const a = value;`,
"b.js": `const lib = require("lib");\nexport const b = lib.value;`,
"index.js": `import { a } from "./a.js";\nimport { b } from "./b.js";\nconsole.log(a, b);`,
});
const result = await Bun.build({
entrypoints: [join(dir, "index.js")],
outdir: join(dir, "out"),
format: "esm",
target: "bun",
sourcemap: "external",
banner: "// BANNER LINE 1\n// BANNER LINE 2\n",
});
expect(result.success).toBe(true);
const outputFile = result.outputs.find(o => o.kind === "entry-point" && o.path.endsWith(".js"));
const outputCode = await outputFile!.text();
const mapData = await Bun.file(outputFile!.path + ".map").text();
const sourceMapObj = JSON.parse(mapData);
const sm = new SourceMap(sourceMapObj);
console.log("\n=== Banner + Dual Package ===");
console.log("Sources:", sourceMapObj.sources);
// Find console.log in the output
const consoleMatch = outputCode.match(/console\.log\(/);
if (consoleMatch) {
const index = consoleMatch.index!;
const linesBeforeMatch = outputCode.substring(0, index).split("\n").length;
const lineStart = outputCode.lastIndexOf("\n", index - 1) + 1;
const column = index - lineStart;
const position = sm.findEntry(linesBeforeMatch - 1, column);
console.log("console.log mapping:", position);
// Should map to index.js line 3 (0-indexed: 2)
expect(position?.originalSource, "console.log should map to index.js").toMatch(/index\.js$/);
expect(position?.originalLine, "console.log should map to line 2 (0-indexed)").toBe(2);
}
console.log("✓ Banner + dual package hazard test passed");
});

View File

@@ -0,0 +1,89 @@
import { expect, test } from "bun:test";
import { tempDir } from "harness";
import { join } from "path";
// This test specifically checks if source_index changes from scanForSecondaryPaths
// cause sourcemap misalignment
test("verify dual package hazard source_index issue", async () => {
await using dir = await tempDir("dual-pkg-source-index-debug", {
"package.json": JSON.stringify({
name: "test",
type: "module",
}),
"node_modules/pkg/package.json": JSON.stringify({
name: "pkg",
main: "./cjs.js",
module: "./esm.js",
}),
// ESM entry (will be resolved initially for ESM imports)
"node_modules/pkg/esm.js": `// FILE: esm.js
export function esmFunc() {
console.log("ESM version");
return "esm";
}`,
// CJS entry (will be used after dual package hazard resolution)
"node_modules/pkg/cjs.js": `// FILE: cjs.js
module.exports = {
cjsFunc: function() {
console.log("CJS version");
return "cjs";
}
};`,
// Import with ESM
"a.js": `import { esmFunc } from "pkg";
export function callA() {
return esmFunc();
}`,
// Require with CJS - triggers dual package hazard
"b.js": `const pkg = require("pkg");
export function callB() {
return pkg.cjsFunc();
}`,
"index.js": `import { callA } from "./a.js";
import { callB } from "./b.js";
console.log(callA(), callB());`,
});
const result = await Bun.build({
entrypoints: [join(dir, "index.js")],
outdir: join(dir, "out"),
format: "esm",
target: "bun",
sourcemap: "external",
minify: false,
});
expect(result.success).toBe(true);
const outputFile = result.outputs.find(o => o.kind === "entry-point");
const code = await outputFile!.text();
const mapData = await Bun.file(outputFile!.path + ".map").text();
const sourceMapObj = JSON.parse(mapData);
console.log("\n=== Debug Info ===");
console.log("Sources in sourcemap:", sourceMapObj.sources);
// Check if esm.js appears in sources (it shouldn't after dual package hazard resolution)
const hasESM = sourceMapObj.sources.some((s: string) => s.includes("esm.js"));
const hasCJS = sourceMapObj.sources.some((s: string) => s.includes("cjs.js"));
console.log("Has esm.js in sources:", hasESM);
console.log("Has cjs.js in sources:", hasCJS);
// After dual package hazard resolution, BOTH imports should use cjs.js
// So sources should contain cjs.js but NOT esm.js
if (hasESM) {
console.warn("WARNING: esm.js appears in sources, but dual package hazard should have resolved to cjs.js");
console.warn("This could indicate the source_index mismatch bug!");
}
console.log("\n=== Output Code ===");
console.log(code);
// Check what's actually imported in the code
const hasESMInCode = code.includes("esm.js") || code.includes("esmFunc");
const hasCJSInCode = code.includes("cjs.js") || code.includes("cjsFunc");
console.log("\nCode references esm:", hasESMInCode);
console.log("Code references cjs:", hasCJSInCode);
});

View File

@@ -1,53 +1,55 @@
# List of tests for which we do NOT set validateExceptionChecks=1 when running in ASAN CI
# List of tests that potentially throw inside of reifyStaticProperties
test/js/node/test/parallel/test-stream-some-find-every.mjs
test/js/node/test/parallel/test-stream-iterator-helpers-test262-tests.mjs
test/js/node/test/parallel/test-fs-stat-date.mjs
test/js/node/test/parallel/test-fs-readSync-position-validation.mjs
test/js/node/test/parallel/test-fs-read-promises-position-validation.mjs
test/js/node/test/parallel/test-fs-read-position-validation.mjs
test/js/node/test/parallel/test-net-server-async-dispose.mjs
test/js/node/test/parallel/test-net-connect-custom-lookup-non-string-address.mjs
test/bake/dev/import-meta-inline.test.ts
test/bake/dev/production.test.ts
test/bake/dev/vfile.test.ts
test/bundler/bundler_banner_sourcemap.test.ts
test/bundler/bundler_dual_package_hazard_sourcemap.test.ts
test/bundler/esbuild/default.test.ts
test/cli/install/bun-repl.test.ts
test/cli/install/bunx.test.ts
test/cli/run/run-eval.test.ts
test/cli/run/self-reference.test.ts
test/integration/vite-build/vite-build.test.ts
test/js/bun/resolve/import-meta-resolve.test.mjs
test/js/bun/resolve/import-meta.test.js
test/js/bun/resolve/resolve.test.ts
test/js/bun/util/BunObject.test.ts
test/js/bun/util/fuzzy-wuzzy.test.ts
test/js/node/events/event-emitter.test.ts
test/js/node/module/node-module-module.test.js
test/js/node/process/call-constructor.test.js
test/js/node/stubs.test.js
test/js/node/test/parallel/test-abortsignal-any.mjs
test/js/node/test/parallel/test-child-process-fork-url.mjs
test/js/node/test/parallel/test-debugger-invalid-json.mjs
test/js/node/test/parallel/test-dgram-async-dispose.mjs
test/js/node/test/parallel/test-events-add-abort-listener.mjs
test/js/node/test/parallel/test-fetch.mjs
test/js/node/test/parallel/test-fs-read-position-validation.mjs
test/js/node/test/parallel/test-fs-read-promises-position-validation.mjs
test/js/node/test/parallel/test-fs-readSync-position-validation.mjs
test/js/node/test/parallel/test-fs-stat-date.mjs
test/js/node/test/parallel/test-module-globalpaths-nodepath.js
test/js/node/test/parallel/test-net-connect-custom-lookup-non-string-address.mjs
test/js/node/test/parallel/test-net-server-async-dispose.mjs
test/js/node/test/parallel/test-parse-args.mjs
test/js/node/test/parallel/test-process-default.js
test/js/node/test/parallel/test-readline-promises-csi.mjs
test/js/node/test/parallel/test-require-dot.js
test/js/node/test/parallel/test-stream-iterator-helpers-test262-tests.mjs
test/js/node/test/parallel/test-stream-some-find-every.mjs
test/js/node/test/parallel/test-util-promisify-custom-names.mjs
test/js/node/test/parallel/test-vm-module-referrer-realm.mjs
test/js/node/test/parallel/test-whatwg-readablestream.mjs
test/js/node/test/parallel/test-worker.mjs
test/js/node/test/system-ca/test-native-root-certs.test.mjs
test/js/node/events/event-emitter.test.ts
test/js/node/module/node-module-module.test.js
test/js/node/process/call-constructor.test.js
test/js/node/stubs.test.js
test/js/node/timers/node-timers.test.ts
test/bake/dev/vfile.test.ts
test/bake/dev/import-meta-inline.test.ts
test/bake/dev/production.test.ts
test/cli/run/run-eval.test.ts
test/cli/run/self-reference.test.ts
test/js/bun/resolve/import-meta-resolve.test.mjs
test/js/bun/resolve/import-meta.test.js
test/js/bun/util/BunObject.test.ts
test/js/bun/util/fuzzy-wuzzy.test.ts
test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js
test/js/third_party/astro/astro-post.test.js
test/js/third_party/pg-gateway/pglite.test.ts
test/js/web/websocket/websocket.test.js
test/js/node/test/parallel/test-vm-module-referrer-realm.mjs
test/js/bun/resolve/resolve.test.ts
test/cli/install/bunx.test.ts
test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js
test/integration/vite-build/vite-build.test.ts
test/bundler/esbuild/default.test.ts
test/cli/install/bun-repl.test.ts
test/js/third_party/astro/astro-post.test.js
test/regression/issue/ctrl-c.test.ts
test/bundler/bundler_comments.test.ts
test/js/node/test/parallel/test-fs-promises-file-handle-readLines.mjs