Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/bun.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1438,6 +1438,7 @@ pub fn sliceTo(pointer: anytype, comptime end: std.meta.Elem(@TypeOf(pointer)))
pub const Semver = @import("./semver/semver.zig");
pub const ImportRecord = @import("./options_types/import_record.zig").ImportRecord;
pub const ImportKind = @import("./options_types/import_record.zig").ImportKind;
pub const ImportPhase = @import("./options_types/import_record.zig").ImportPhase;

pub const Watcher = @import("./watcher/Watcher.zig");

Expand Down
24 changes: 24 additions & 0 deletions src/bundler/analyze_transpiled_module.zig
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ pub const RecordKind = enum(u8) {
export_info_namespace,
/// module_name
export_info_star,
/// module_name, import_name = '*', local_name (import defer * as x from "...")
import_info_namespace_defer,
/// module_name (import defer requested-module entry)
requested_module_defer,
_,

pub fn len(record: RecordKind) !usize {
Expand All @@ -25,10 +29,12 @@ pub const RecordKind = enum(u8) {
.import_info_single => 3,
.import_info_single_type_script => 3,
.import_info_namespace => 3,
.import_info_namespace_defer => 3,
.export_info_indirect => 3,
.export_info_local => 3,
.export_info_namespace => 2,
.export_info_star => 1,
.requested_module_defer => 1,
else => return error.InvalidRecordKind,
};
}
Expand Down Expand Up @@ -174,6 +180,7 @@ pub const ModuleInfo = struct {
strings_buf: std.ArrayListUnmanaged(u8),
strings_lens: std.ArrayListUnmanaged(u32),
requested_modules: std.AutoArrayHashMap(StringID, FetchParameters),
requested_modules_defer: std.AutoArrayHashMapUnmanaged(StringID, void),
Comment thread
robobun marked this conversation as resolved.
buffer: std.ArrayListUnmanaged(StringID),
record_kinds: std.ArrayListUnmanaged(RecordKind),
flags: Flags,
Expand Down Expand Up @@ -225,6 +232,17 @@ pub const ModuleInfo = struct {
pub fn addImportInfoNamespace(self: *ModuleInfo, module_name: StringID, local_name: StringID) !void {
try self._addRecord(.import_info_namespace, &.{ module_name, .star_namespace, local_name });
}
pub fn addImportInfoNamespaceDefer(self: *ModuleInfo, module_name: StringID, local_name: StringID) !void {
try self._addRecord(.import_info_namespace_defer, &.{ module_name, .star_namespace, local_name });
}
Comment thread
robobun marked this conversation as resolved.
pub fn addRequestedModuleDefer(self: *ModuleInfo, module_name: StringID) !void {
// JSC's ModuleAnalyzer dedups per (specifier, phase); mirror that here
// so the debug fallbackParse() diff in BunAnalyzeTranspiledModule.cpp
// agrees when the same specifier is deferred more than once.
const gop = try self.requested_modules_defer.getOrPut(self.gpa, module_name);
if (gop.found_existing) return;
try self._addRecord(.requested_module_defer, &.{module_name});
}
pub fn addExportInfoIndirect(self: *ModuleInfo, export_name: StringID, import_name: StringID, module_name: StringID) !void {
if (try self._hasOrAddExportedName(export_name)) return; // a syntax error will be emitted later in this case
try self._addRecord(.export_info_indirect, &.{ export_name, import_name, module_name });
Expand Down Expand Up @@ -259,6 +277,7 @@ pub const ModuleInfo = struct {
.strings_lens = .{},
.exported_names = .{},
.requested_modules = std.AutoArrayHashMap(StringID, FetchParameters).init(allocator),
.requested_modules_defer = .{},
.buffer = .empty,
.record_kinds = .empty,
.flags = .{ .contains_import_meta = false, .is_typescript = is_typescript },
Expand All @@ -271,6 +290,7 @@ pub const ModuleInfo = struct {
self.strings_lens.deinit(self.gpa);
self.exported_names.deinit(self.gpa);
self.requested_modules.deinit();
self.requested_modules_defer.deinit(self.gpa);
self.buffer.deinit(self.gpa);
self.record_kinds.deinit(self.gpa);
}
Expand Down Expand Up @@ -326,6 +346,10 @@ pub const ModuleInfo = struct {
if (k == .import_info_single or k == .import_info_single_type_script) {
try local_name_to_module_name.put(self.buffer.items[i + 2], .{ .module_name = self.buffer.items[i], .import_name = self.buffer.items[i + 1], .record_kinds_idx = idx, .is_namespace = false });
} else if (k == .import_info_namespace) {
// Deliberately excludes .import_info_namespace_defer: a deferred
// namespace object lives in THIS module's environment, so
// `export { ns }` must stay a Local export (proposal ParseModule
// 11.a.ii). JSC's ModuleAnalyzer::exportVariable does the same.
try local_name_to_module_name.put(self.buffer.items[i + 2], .{ .module_name = self.buffer.items[i], .import_name = .star_namespace, .record_kinds_idx = idx, .is_namespace = true });
}
i += k.len() catch unreachable;
Expand Down
8 changes: 7 additions & 1 deletion src/bundler_jsc/analyze_jsc.zig
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export fn zig__ModuleInfoDeserialized__toJSModuleRecord(
switch (k) {
.declared_variable => declared_variables.add(vm, identifiers, res.buffer[i]),
.lexical_variable => lexical_variables.add(vm, identifiers, res.buffer[i]),
.import_info_single, .import_info_single_type_script, .import_info_namespace, .export_info_indirect, .export_info_local, .export_info_namespace, .export_info_star => {},
.import_info_single, .import_info_single_type_script, .import_info_namespace, .import_info_namespace_defer, .requested_module_defer, .export_info_indirect, .export_info_local, .export_info_namespace, .export_info_star => {},
else => return null,
}
i += k.len() catch unreachable; // handled above
Expand Down Expand Up @@ -70,6 +70,8 @@ export fn zig__ModuleInfoDeserialized__toJSModuleRecord(
.import_info_single => module_record.addImportEntrySingle(identifiers, res.buffer[i + 1], res.buffer[i + 2], res.buffer[i]),
.import_info_single_type_script => module_record.addImportEntrySingleTypeScript(identifiers, res.buffer[i + 1], res.buffer[i + 2], res.buffer[i]),
.import_info_namespace => module_record.addImportEntryNamespace(identifiers, res.buffer[i + 1], res.buffer[i + 2], res.buffer[i]),
.import_info_namespace_defer => module_record.addImportEntryNamespaceDefer(identifiers, res.buffer[i + 1], res.buffer[i + 2], res.buffer[i]),
.requested_module_defer => module_record.addRequestedModuleDefer(identifiers, res.buffer[i]),
.export_info_indirect => if (res.buffer[i + 1] == .star_namespace)
module_record.addNamespaceExport(identifiers, res.buffer[i + 0], res.buffer[i + 2])
else
Expand Down Expand Up @@ -138,6 +140,10 @@ const JSModuleRecord = opaque {
pub const addImportEntrySingleTypeScript = JSC_JSModuleRecord__addImportEntrySingleTypeScript;
extern fn JSC_JSModuleRecord__addImportEntryNamespace(module_record: *JSModuleRecord, identifier_array: *IdentifierArray, import_name: StringID, local_name: StringID, module_name: StringID) void;
pub const addImportEntryNamespace = JSC_JSModuleRecord__addImportEntryNamespace;
extern fn JSC_JSModuleRecord__addImportEntryNamespaceDefer(module_record: *JSModuleRecord, identifier_array: *IdentifierArray, import_name: StringID, local_name: StringID, module_name: StringID) void;
pub const addImportEntryNamespaceDefer = JSC_JSModuleRecord__addImportEntryNamespaceDefer;
extern fn JSC_JSModuleRecord__addRequestedModuleDefer(module_record: *JSModuleRecord, identifier_array: *IdentifierArray, module_name: StringID) void;
pub const addRequestedModuleDefer = JSC_JSModuleRecord__addRequestedModuleDefer;
};

const bun = @import("bun");
Expand Down
3 changes: 3 additions & 0 deletions src/js_parser/ast/E.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1399,6 +1399,9 @@ pub const Import = struct {
expr: ExprNodeIndex,
options: ExprNodeIndex = Expr.empty,
import_record_index: u32,
// https://github.com/tc39/proposal-defer-import-eval
// https://github.com/tc39/proposal-source-phase-imports
phase: bun.ImportPhase = .evaluation,
Comment thread
robobun marked this conversation as resolved.
Comment thread
robobun marked this conversation as resolved.

/// TODO:
/// Comments inside "import()" expressions have special meaning for Webpack.
Expand Down
1 change: 1 addition & 0 deletions src/js_parser/ast/Expr.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2488,6 +2488,7 @@ pub const Data = union(Tag) {
.expr = try el.expr.deepClone(allocator),
.options = try el.options.deepClone(allocator),
.import_record_index = el.import_record_index,
.phase = el.phase,
});
return .{ .e_import = item };
},
Expand Down
26 changes: 20 additions & 6 deletions src/js_parser/ast/ImportScanner.zig
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,12 @@ pub fn scan(
// system and used for type-only imports).
if (!keep_unused_imports) {
var found_imports = false;
var is_unused_in_typescript = true;
// Non-evaluation-phase imports (`import defer * as ns` /
// `import source x`) are never considered unused: the
// deferred subgraph is still loaded/linked and TLA deps
// eagerly evaluate, so the statement has observable effects
// independent of whether the binding is referenced.
var is_unused_in_typescript = record.phase == .evaluation;

if (st.default_name) |default_name| {
found_imports = true;
Expand All @@ -104,8 +109,11 @@ pub fn scan(
is_unused_in_typescript = false;
}

// Remove the symbol if it's never used outside a dead code region
if (symbol.use_count_estimate == 0) {
// Remove the symbol if it's never used outside a dead code region.
// Non-evaluation phases (`import source x from ...`) keep the
// binding: the statement is pass-through and dropping it would
// change load semantics.
if (symbol.use_count_estimate == 0 and record.phase == .evaluation) {
st.default_name = null;
}
}
Expand All @@ -121,7 +129,7 @@ pub fn scan(
}

// Remove the symbol if it's never used outside a dead code region
if (symbol.use_count_estimate == 0) {
if (symbol.use_count_estimate == 0 and record.phase == .evaluation) {
// Make sure we don't remove this if it was used for a property
// access while bundling
var has_any = false;
Expand Down Expand Up @@ -198,7 +206,10 @@ pub fn scan(
}

const namespace_ref = st.namespace_ref;
const convert_star_to_clause = !p.options.bundle and (p.symbols.items[namespace_ref.innerIndex()].use_count_estimate == 0);
// Deferred imports always keep the namespace binding even if it
// appears unused; evaluating the module is a side effect that the
// user may be relying on via later property access.
const convert_star_to_clause = !p.options.bundle and record.phase == .evaluation and (p.symbols.items[namespace_ref.innerIndex()].use_count_estimate == 0);
Comment thread
robobun marked this conversation as resolved.
Comment thread
robobun marked this conversation as resolved.

if (convert_star_to_clause and !keep_unused_imports) {
st.star_name_loc = null;
Expand All @@ -210,7 +221,10 @@ pub fn scan(
ImportItemForNamespaceMap.init(allocator);

if (p.options.bundle) {
if (st.star_name_loc != null and existing_items.count() > 0) {
// Lowering `ns.foo` to a direct named import bypasses the
// deferred namespace's evaluate-on-access gate, so only do
// this for evaluation-phase imports.
if (st.star_name_loc != null and existing_items.count() > 0 and record.phase == .evaluation) {
Comment thread
robobun marked this conversation as resolved.
const sorted = try allocator.alloc(string, existing_items.count());
defer allocator.free(sorted);
for (sorted, existing_items.keys()) |*result, alias| {
Expand Down
17 changes: 14 additions & 3 deletions src/js_parser/ast/P.zig
Original file line number Diff line number Diff line change
Expand Up @@ -599,12 +599,14 @@ pub fn NewParser_(
}

p.import_records.items[import_record_index].flags.handles_import_errors = (state.is_await_target and p.fn_or_arrow_data_visit.try_body_count != 0) or state.is_then_catch_target;
p.import_records.items[import_record_index].phase = state.phase;
p.import_records_for_current_part.append(p.allocator, import_record_index) catch unreachable;

return p.newExpr(E.Import{
.expr = arg,
.import_record_index = @intCast(import_record_index),
.options = state.import_options,
.phase = state.phase,
}, state.loc);
}

Expand All @@ -620,6 +622,7 @@ pub fn NewParser_(
.expr = arg,
.options = state.import_options,
.import_record_index = std.math.maxInt(u32),
.phase = state.phase,
}, state.loc);
}

Expand Down Expand Up @@ -2707,7 +2710,7 @@ pub fn NewParser_(
_ = children.pop();
}

pub fn processImportStatement(p: *P, stmt_: S.Import, path: ParsedPath, loc: logger.Loc, was_originally_bare_import: bool) anyerror!Stmt {
pub fn processImportStatement(p: *P, stmt_: S.Import, path: ParsedPath, loc: logger.Loc, was_originally_bare_import: bool, phase: bun.ImportPhase) anyerror!Stmt {
const is_macro = FeatureFlags.is_macro_enabled and (path.is_macro or js_ast.Macro.isMacroPath(path.text));
var stmt = stmt_;
if (is_macro) {
Expand Down Expand Up @@ -2780,6 +2783,7 @@ pub fn NewParser_(

stmt.import_record_index = p.addImportRecord(.stmt, path.loc, path.text);
p.import_records.items[stmt.import_record_index].flags.was_originally_bare_import = was_originally_bare_import;
p.import_records.items[stmt.import_record_index].phase = phase;

if (stmt.star_name_loc) |star| {
const name = p.loadNameFromRef(stmt.namespace_ref);
Expand Down Expand Up @@ -2945,8 +2949,15 @@ pub fn NewParser_(
try p.validateAndSetImportType(&path, &stmt);
}

// Track the items for this namespace
try p.import_items_for_namespace.put(p.allocator, stmt.namespace_ref, item_refs);
// Track the items for this namespace so maybeRewritePropertyAccess
// can later rewrite `ns.foo` to a direct import identifier under
// bundling. Skip non-evaluation phases: a deferred namespace must
// keep its property accesses intact so evaluation is gated on
// first access; registering it here would let the visit phase
// strip the E.Dot before ImportScanner ever runs.
if (phase == .evaluation) {
try p.import_items_for_namespace.put(p.allocator, stmt.namespace_ref, item_refs);
}
return p.s(stmt, loc);
}

Expand Down
22 changes: 20 additions & 2 deletions src/js_parser/ast/parseImportExport.zig
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,30 @@ pub fn ParseImportExport(
/// Note: The caller has already parsed the "import" keyword
pub fn parseImportExpr(noalias p: *P, loc: logger.Loc, level: Level) anyerror!Expr {
// Parse an "import.meta" expression
var phase: bun.ImportPhase = .evaluation;
if (p.lexer.token == .t_dot) {
p.esm_import_keyword = js_lexer.rangeOfIdentifier(p.source, loc);
try p.lexer.next();
if (p.lexer.isContextualKeyword("meta")) {
// `import.meta` is module-only syntax; mark the file as
// having ESM syntax. `import.defer()` / `import.source()`
// extend ImportCall and are valid in Script context (like
// plain `import()`), so they must not set this.
p.esm_import_keyword = js_lexer.rangeOfIdentifier(p.source, loc);
try p.lexer.next();
p.has_import_meta = true;
return p.newExpr(E.ImportMeta{}, loc);
} else if (p.lexer.isContextualKeyword("defer")) {
// "import.defer('path')"
// https://github.com/tc39/proposal-defer-import-eval
phase = .defer_;
try p.lexer.next();
} else if (p.lexer.isContextualKeyword("source")) {
// "import.source('path')"
// https://github.com/tc39/proposal-source-phase-imports
phase = .source;
try p.lexer.next();
Comment thread
robobun marked this conversation as resolved.
} else {
try p.lexer.expectedString("\"meta\"");
try p.lexer.expectedString("\"meta\", \"defer\", or \"source\"");
Comment thread
claude[bot] marked this conversation as resolved.
}
}

Expand Down Expand Up @@ -65,12 +80,14 @@ pub fn ParseImportExport(
if (comptime only_scan_imports_and_do_not_visit) {
if (value.data == .e_string and value.data.e_string.isUTF8() and value.data.e_string.isPresent()) {
const import_record_index = p.addImportRecord(.dynamic, value.loc, value.data.e_string.slice(p.allocator));
p.import_records.items[import_record_index].phase = phase;

return p.newExpr(E.Import{
.expr = value,
// .leading_interior_comments = comments,
.import_record_index = import_record_index,
.options = import_options,
.phase = phase,
}, loc);
}
}
Expand All @@ -82,6 +99,7 @@ pub fn ParseImportExport(
// .leading_interior_comments = comments,
.import_record_index = std.math.maxInt(u32),
.options = import_options,
.phase = phase,
}, loc);
}

Expand Down
Loading
Loading