Compare commits

...

6 Commits

Author SHA1 Message Date
Jarred Sumner
e90070866e Merge branch 'main' into claude/cjs-code-splitting 2026-01-22 15:13:53 -08:00
Claude Bot
790a758885 test(bundler): run /test.js runtime files in splitting tests
Address review feedback by actually running the /test.js runtime files
that verify shared module behavior across entry points:

- CJSSharedModuleSideEffects: verifies shared module side effects run
  only once when requiring both entry points
- LiveBindingSetters: verifies shared state is preserved when importing
  both entry points sequentially

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 13:31:43 +00:00
Claude Bot
a072337234 test(bundler): add comprehensive ESM/CJS syntax edge case tests for code splitting
Add 42 new edge case tests covering all permutations of ESM and CJS syntax:

- require() variations: require of ESM, destructuring default, mixed with import
- await import() variations: top-level await, inside async functions, conditional
- module.exports variations: object pattern, class, function, exports.x pattern
- export { x } from: basic, renamed, chained through multiple files
- export * from: basic, multiple sources, as namespace
- export { default } from: basic, renamed, named to default
- Circular dependencies: ESM, CJS, export star, three-way
- Mixed syntax: ESM importing CJS requiring ESM, CJS requiring ESM importing CJS
- __esModule interop: CJS with __esModule flag, dynamic imports
- Live bindings: getter/setter patterns, CJS behavior differences

All tests include both ESM and CJS output format variants where applicable.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 09:38:18 +00:00
Claude Bot
750a8ae412 test(bundler): strengthen splitting tests with assertNotPresent assertions
Add assertNotPresent assertions to verify that shared code is actually
extracted to separate chunks rather than just testing runtime output.

Tests improved:
- SharedVendorChunk / SharedVendorChunkCJS
- SharedDefaultExport / SharedDefaultExportCJS
- SharedJSONImport / SharedJSONImportCJS
- AsyncAwaitDynamicImport / AsyncAwaitDynamicImportCJS

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 05:19:23 +00:00
Claude Bot
e2120e0bb7 test(bundler): add comprehensive code splitting tests adapted from webpack
Add 31 new test cases (with ESM and CJS variants) adapted from webpack's
test suite covering:

- Basic dynamic imports
- CJS module dynamic imports
- Duplicate dynamic imports to same module
- Circular dynamic imports
- Shared vendor chunks
- Multiple shared dependencies
- Deep dependency chains
- Mixed ESM and CJS sources
- Re-exports and star exports
- Default export sharing
- Class export sharing
- JSON imports
- Async/await with dynamic imports
- Many entry points sharing code
- Partially shared dependencies
- Node modules style imports

All tests run in both ESM and CJS output formats to ensure code splitting
works correctly in both modes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 04:39:58 +00:00
Claude Bot
a963c7c4ae feat(bundler): add code splitting support for CommonJS output format
Previously, code splitting was only supported with ESM output format.
This change adds support for code splitting when using --format=cjs.

When code splitting with CJS output:
- Shared chunks export symbols via `module.exports = { alias: symbol, ... }`
- Entry points import from chunks via `var { alias } = require("./chunk.js")`
- CommonJS module caching ensures shared chunks are only executed once

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 04:23:23 +00:00
2 changed files with 2396 additions and 0 deletions

View File

@@ -353,6 +353,65 @@ fn computeCrossChunkDependenciesWithChunkMetas(c: *LinkerContext, chunks: []Chun
repr.cross_chunk_suffix_stmts = stmts;
}
},
.cjs => {
// For CommonJS output format with code splitting, we need to generate
// module.exports = { alias: symbol, ... } at the end of the chunk
c.sortedCrossChunkExportItems(
chunk_meta.exports,
&stable_ref_list,
);
repr.exports_to_other_chunks.ensureUnusedCapacity(c.allocator(), stable_ref_list.items.len) catch unreachable;
r.clearRetainingCapacity();
var properties = BabyList(js_ast.G.Property).initCapacity(c.allocator(), stable_ref_list.items.len) catch unreachable;
properties.len = @as(u32, @truncate(stable_ref_list.items.len));
for (stable_ref_list.items, properties.slice()) |stable_ref, *prop| {
const ref = stable_ref.ref;
const alias = if (c.options.minify_identifiers) try r.nextMinifiedName(c.allocator()) else r.nextRenamedName(c.graph.symbols.get(ref).?.original_name);
// Create property: { alias: symbol }
prop.* = .{
.key = Expr.init(
js_ast.E.String,
js_ast.E.String{ .data = alias },
Logger.Loc.Empty,
),
.value = Expr.initIdentifier(ref, Logger.Loc.Empty),
};
repr.exports_to_other_chunks.putAssumeCapacity(
ref,
alias,
);
}
if (properties.len > 0) {
var stmts = BabyList(js_ast.Stmt).initCapacity(c.allocator(), 1) catch unreachable;
// Generate: module.exports = { ... }
stmts.appendAssumeCapacity(
Stmt.assign(
Expr.init(
js_ast.E.Dot,
.{
.target = Expr.initIdentifier(c.unbound_module_ref, Logger.Loc.Empty),
.name = "exports",
.name_loc = Logger.Loc.Empty,
},
Logger.Loc.Empty,
),
Expr.init(
js_ast.E.Object,
js_ast.E.Object{
.properties = properties,
},
Logger.Loc.Empty,
),
),
);
repr.cross_chunk_suffix_stmts = stmts;
}
},
else => {},
}
}
@@ -410,6 +469,63 @@ fn computeCrossChunkDependenciesWithChunkMetas(c: *LinkerContext, chunks: []Chun
},
) catch unreachable;
},
.cjs => {
// For CommonJS output format with code splitting, we generate:
// var { alias1, alias2, ... } = require("./chunk.js");
const import_record_index = @as(u32, @intCast(cross_chunk_imports.len));
cross_chunk_imports.append(c.allocator(), .{
.import_kind = .require,
.chunk_index = cross_chunk_import.chunk_index,
}) catch unreachable;
// Build destructuring binding pattern: { alias1: ref1, alias2: ref2, ... }
var binding_props = BabyList(js_ast.B.Property).initCapacity(c.allocator(), cross_chunk_import.sorted_import_items.len) catch unreachable;
for (cross_chunk_import.sorted_import_items.slice()) |item| {
binding_props.append(c.allocator(), .{
.key = Expr.init(
js_ast.E.String,
js_ast.E.String{ .data = item.export_alias },
Logger.Loc.Empty,
),
.value = js_ast.Binding.alloc(c.allocator(), js_ast.B.Identifier{ .ref = item.ref }, Logger.Loc.Empty),
}) catch unreachable;
}
// Create the require call expression
const require_expr = Expr.init(
js_ast.E.RequireString,
js_ast.E.RequireString{
.import_record_index = import_record_index,
},
Logger.Loc.Empty,
);
// Create variable declaration: var { ... } = require("./chunk.js");
var decls = js_ast.G.Decl.List.initCapacity(c.allocator(), 1) catch unreachable;
decls.appendAssumeCapacity(.{
.binding = js_ast.Binding.alloc(
c.allocator(),
js_ast.B.Object{
.properties = binding_props.slice(),
.is_single_line = true,
},
Logger.Loc.Empty,
),
.value = require_expr,
});
cross_chunk_prefix_stmts.append(
c.allocator(),
Stmt.alloc(
js_ast.S.Local,
.{
.decls = decls,
},
Logger.Loc.Empty,
),
) catch unreachable;
},
else => {},
}
}
@@ -436,6 +552,7 @@ const default_allocator = bun.default_allocator;
const renamer = bun.renamer;
const js_ast = bun.ast;
const Expr = js_ast.Expr;
const Part = js_ast.Part;
const S = js_ast.S;
const Stmt = js_ast.Stmt;

File diff suppressed because it is too large Load Diff