diff --git a/src/bundler/barrel_imports.rs b/src/bundler/barrel_imports.rs index 86cc210a5783..099465bfa446 100644 --- a/src/bundler/barrel_imports.rs +++ b/src/bundler/barrel_imports.rs @@ -315,13 +315,9 @@ struct BarrelWorkItem<'a> { is_star: bool, } -/// Resolve, process, and patch import records for a single barrel. -/// Used to inline-resolve deferred records whose source_index is still invalid. -fn resolve_barrel_records( - this: &mut BundleV2, - barrel_idx: u32, - barrels_to_resolve: &mut ArrayHashMap, -) -> i32 { +/// Resolve the records the BFS just un-deferred in one barrel (`un_deferred`, +/// ascending indices), schedule their modules, and patch their source indices. +fn resolve_barrel_records(this: &mut BundleV2, barrel_idx: u32, un_deferred: &[u32]) -> i32 { let idx = barrel_idx as usize; let target = this.graph.ast.items_target()[idx]; let loader = this.graph.input_files.items_loader()[idx]; @@ -340,6 +336,7 @@ fn resolve_barrel_records( source: &source, loader, target, + only_records: Some(un_deferred), }); this.graph.input_files.items_source_mut()[idx] = source; @@ -353,22 +350,22 @@ fn resolve_barrel_records( source_path, loader, target, - force_save: true, + only_records: Some(un_deferred), ..Default::default() }, ); this.graph.ast.items_import_records_mut()[idx] = barrel_ir; - let _ = barrels_to_resolve.swap_remove(&barrel_idx); scheduled } /// After a new file's import records are patched with source_indices, /// record what this file requests from each target in requested_exports /// (eagerly, before barrels are known), then BFS through barrel chains -/// to un-defer needed records. Un-deferred records are re-resolved through -/// resolveImportRecords (same path as initial resolution). +/// to un-defer needed records. Each un-deferred record is resolved at once +/// through resolveImportRecords (same path as initial resolution), so the BFS +/// can continue into the module it points at. /// Returns the number of newly scheduled parse tasks. pub(crate) fn schedule_barrel_deferred_imports( this: &mut BundleV2, @@ -679,8 +676,6 @@ pub(crate) fn schedule_barrel_deferred_imports( // dedup via requested_exports to prevent cycles. let initial_queue_len = queue.len(); - let mut barrels_to_resolve: ArrayHashMap = ArrayHashMap::default(); - let mut newly_scheduled: i32 = 0; let mut qi: usize = 0; while qi < queue.len() { @@ -732,24 +727,15 @@ pub(crate) fn schedule_barrel_deferred_imports( let barrel_ir = &mut this.graph.ast.items_import_records_mut()[barrel_idx as usize]; if item_is_star { - // Read flags by index, then mutate (borrowck). - let len = barrel_ir.len(); - let mut un_deferred_any = false; - for idx in 0..len { - let flags = barrel_ir.as_slice()[idx].flags; - if flags.contains(import_record::Flags::IS_UNUSED) - && !flags.contains(import_record::Flags::IS_INTERNAL) - { - if un_defer_record(barrel_ir, idx) { - barrels_to_resolve.put(barrel_idx, ())?; - un_deferred_any = true; - } + let mut un_deferred: Vec = Vec::new(); + for idx in 0..barrel_ir.len() { + if un_defer_record(barrel_ir, idx) { + un_deferred.push(idx as u32); } } // Resolve now: propagation below needs source indices. - if un_deferred_any { - newly_scheduled += - resolve_barrel_records(this, barrel_idx, &mut barrels_to_resolve); + if !un_deferred.is_empty() { + newly_scheduled += resolve_barrel_records(this, barrel_idx, &un_deferred); } // A namespace request covers every export: request each @@ -846,18 +832,12 @@ pub(crate) fn schedule_barrel_deferred_imports( continue; } if un_defer_record(barrel_ir, star_idx as usize) { - barrels_to_resolve.put(barrel_idx, ())?; - } - let mut star_rec_si = barrel_ir.as_slice()[star_idx as usize].source_index; - if !star_rec_si.is_valid() { - // Deferred record was never resolved — resolve inline now. - newly_scheduled += - resolve_barrel_records(this, barrel_idx, &mut barrels_to_resolve); - // Re-derive after resolution may have mutated slices. - star_rec_si = this.graph.ast.items_import_records_mut()[barrel_idx as usize] - .as_slice()[star_idx as usize] - .source_index; + // Resolve now: propagation below needs the source index. + newly_scheduled += resolve_barrel_records(this, barrel_idx, &[star_idx]); } + let star_rec_si = this.graph.ast.items_import_records()[barrel_idx as usize] + .as_slice()[star_idx as usize] + .source_index; if star_rec_si.is_valid() { queue.push(BarrelWorkItem { barrel_source_index: star_rec_si.get(), @@ -872,7 +852,9 @@ pub(crate) fn schedule_barrel_deferred_imports( let barrel_ir = &mut this.graph.ast.items_import_records_mut()[barrel_idx as usize]; if un_defer_record(barrel_ir, resolution.import_record_index as usize) { - barrels_to_resolve.put(barrel_idx, ())?; + // Resolve now: propagation below needs the source index. + newly_scheduled += + resolve_barrel_records(this, barrel_idx, &[resolution.import_record_index]); } // `original_alias` is an arena-backed `StoreStr` valid for the @@ -881,38 +863,23 @@ pub(crate) fn schedule_barrel_deferred_imports( Some(p) => p.slice(), None => alias, }; - if (resolution.import_record_index as usize) < barrel_ir.len() { - let mut rec_si = - barrel_ir.as_slice()[resolution.import_record_index as usize].source_index; - if !rec_si.is_valid() { - // Deferred record was never resolved — resolve inline now. - newly_scheduled += - resolve_barrel_records(this, barrel_idx, &mut barrels_to_resolve); - rec_si = this.graph.ast.items_import_records_mut()[barrel_idx as usize].as_slice() - [resolution.import_record_index as usize] - .source_index; - } - if rec_si.is_valid() { - // When the barrel re-exports a namespace import (`import * as X; export { X }`), - // propagate as a star import so the target barrel loads all exports. - queue.push(BarrelWorkItem { - barrel_source_index: rec_si.get(), - alias: propagate_alias, - is_star: resolution.alias_is_star, - }); - } + let rec_si = this.graph.ast.items_import_records()[barrel_idx as usize] + .as_slice() + .get(resolution.import_record_index as usize) + .map(|rec| rec.source_index); + if let Some(rec_si) = rec_si.filter(|si| si.is_valid()) { + // When the barrel re-exports a namespace import (`import * as X; export { X }`), + // propagate as a star import so the target barrel loads all exports. + queue.push(BarrelWorkItem { + barrel_source_index: rec_si.get(), + alias: propagate_alias, + is_star: resolution.alias_is_star, + }); } qi += 1; } - // Re-resolve any remaining un-deferred records through the normal resolution path. - while barrels_to_resolve.count() > 0 { - let barrel_source_index = barrels_to_resolve.keys()[0]; - newly_scheduled += - resolve_barrel_records(this, barrel_source_index, &mut barrels_to_resolve); - } - Ok(newly_scheduled) } diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index 7b2eb2dc85fa..06615c4f6923 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -5879,6 +5879,7 @@ pub mod bv2_impl { source: &result.source, loader: result.loader, target, + only_records: None, }); if let Some(err) = resolve_result.last_error { @@ -5937,6 +5938,8 @@ pub mod bv2_impl { pub(crate) source: &'a bun_ast::Source, pub(crate) loader: Loader, pub(crate) target: options::Target, + /// See `only_selected_record`. + pub(crate) only_records: Option<&'a [u32]>, } pub(crate) struct ResolveImportRecordResult { @@ -5944,9 +5947,17 @@ pub mod bv2_impl { pub(crate) last_error: Option, } + /// `only_records`: barrel un-deferral passes the ascending indices of the records + /// it just un-deferred. A missing `source_index` does not mean a record is still + /// unresolved (external, failed, or waiting for an onResolve plugin). + #[inline] + fn only_selected_record(only_records: Option<&[u32]>, record_index: usize) -> bool { + only_records.is_none_or(|only| only.binary_search(&(record_index as u32)).is_ok()) + } + impl<'a> BundleV2<'a> { - /// Resolve all unresolved import records for a module. Skips records that - /// are already resolved (valid source_index), unused, or internal. + /// Resolve all unresolved import records for a module, or only `ctx.only_records`. + /// Skips records that are already resolved (valid source_index), unused, or internal. /// Returns a resolve queue of new modules to schedule, plus any fatal error. /// Used by both initial parse resolution and barrel un-deferral. pub(crate) fn resolve_import_records( @@ -5956,6 +5967,8 @@ pub mod bv2_impl { let source = ctx.source; let loader = ctx.loader; let source_dir = source.path.source_dir(); + let only_records = ctx.only_records; + debug_assert!(only_records.is_none_or(<[u32]>::is_sorted)); let mut estimated_resolve_queue_count: usize = 0; for import_record in ctx.import_records.iter_mut() { if import_record @@ -5988,12 +6001,19 @@ pub mod bv2_impl { || import_record.source_index.is_valid())) as usize; } + if let Some(only) = only_records { + estimated_resolve_queue_count = estimated_resolve_queue_count.min(only.len()); + } let mut resolve_queue = ResolveQueue::default(); resolve_queue.reserve(estimated_resolve_queue_count); let mut last_error: Option = None; 'outer: for (i, import_record) in ctx.import_records.iter_mut().enumerate() { + if !only_selected_record(only_records, i) { + continue; + } + // Preserve original import specifier before resolution modifies path if import_record.original_path.is_empty() { import_record.original_path = import_record.path.text; @@ -6758,9 +6778,9 @@ pub mod bv2_impl { pub(crate) loader: Loader, pub(crate) target: options::Target, pub(crate) redirect_import_record_index: u32, - /// When true, always save source indices regardless of dev_server/loader. - /// Used for barrel un-deferral where records must always be connected. - pub(crate) force_save: bool, + /// See `only_selected_record`. `Some` also saves the source indices regardless + /// of dev_server/loader: the barrel BFS follows them. + pub(crate) only_records: Option<&'a [u32]>, } impl Default for PatchImportRecordsCtx<'_> { @@ -6771,7 +6791,7 @@ pub mod bv2_impl { loader: Loader::File, target: Target::Browser, redirect_import_record_index: u32::MAX, - force_save: false, + only_records: None, } } } @@ -6789,7 +6809,8 @@ pub mod bv2_impl { // across the `&mut self.graph.build_graphs[...]` borrow // below, so address the disjoint `self.graph.*` fields directly instead. let input_file_loaders = self.graph.input_files.items_loader(); - let save_import_record_source_index = ctx.force_save + debug_assert!(ctx.only_records.is_none_or(<[u32]>::is_sorted)); + let save_import_record_source_index = ctx.only_records.is_some() || self.dev_server.is_none() || ctx.loader == Loader::Html || ctx.loader.is_css(); @@ -6816,6 +6837,9 @@ pub mod bv2_impl { // so borrowck sees it as disjoint from `self.graph.input_files` above. let path_to_source_index_map = &mut self.graph.build_graphs[ctx.target]; for (i, record) in import_records.as_mut_slice().iter_mut().enumerate() { + if !only_selected_record(ctx.only_records, i) { + continue; + } if let Some(source_index) = path_to_source_index_map.get_path(&record.path) { if save_import_record_source_index || input_file_loaders[source_index as usize].is_css() @@ -7127,7 +7151,7 @@ pub mod bv2_impl { loader: result.loader, target: result.ast.target, redirect_import_record_index: result.ast.redirect_import_record_index, - force_save: false, + only_records: None, }, ); diff --git a/test/bundler/bundler_barrel.test.ts b/test/bundler/bundler_barrel.test.ts index 6b7177534d49..1f93ddb892e5 100644 --- a/test/bundler/bundler_barrel.test.ts +++ b/test/bundler/bundler_barrel.test.ts @@ -1395,6 +1395,170 @@ describe("bundler", () => { run: { stdout: "resolved-by-plugin" }, }); + // --- Each barrel record is resolved once --- + + // a.js is only discovered through the barrel, so its request for Broken and C + // always arrives after the barrel deferred both. Un-deferring Broken reports + // the failure. Un-deferring C must not resolve Broken (and report it) again. + itBundled("barrel/UnDeferReportsUnresolvableSiblingOnce", { + files: { + "/entry.js": /* js */ ` + import { A } from 'oncelib'; + console.log(A); + `, + "/node_modules/oncelib/package.json": JSON.stringify({ + name: "oncelib", + main: "./index.js", + sideEffects: false, + }), + "/node_modules/oncelib/index.js": /* js */ ` + export { A } from './a.js'; + export { Broken } from './missing.js'; + export { C } from './c.js'; + `, + "/node_modules/oncelib/a.js": /* js */ ` + import { Broken, C } from 'oncelib'; + export const A = Broken + C; + `, + "/node_modules/oncelib/c.js": /* js */ ` + export const C = "c"; + `, + }, + outfile: "/out.js", + bundleErrors: { + "/node_modules/oncelib/index.js": ['Could not resolve: "./missing.js"'], + }, + }); + + // A re-export that resolves as external never gets a source_index. Requests + // for it from importers must not resolve the barrel again, so the onResolve + // plugin runs once for the record. + itBundled("barrel/ExternalReExportResolvesOnce", () => { + const resolved: string[] = []; + return { + files: { + "/entry.js": /* js */ ` + import { React } from 'extlib'; + import { other } from './other.js'; + console.log(React, other); + `, + "/other.js": /* js */ ` + import { React } from 'extlib'; + export const other = React; + `, + "/node_modules/extlib/package.json": JSON.stringify({ + name: "extlib", + main: "./index.js", + sideEffects: false, + }), + "/node_modules/extlib/index.js": /* js */ ` + export { default as React } from 'react'; + export { B } from './b.js'; + `, + // Nobody imports B, so b.js must stay deferred. + "/node_modules/extlib/b.js": /* js */ ` + export const B = <<>>; + `, + }, + outfile: "/out.js", + plugins(builder) { + resolved.length = 0; + builder.onResolve({ filter: /^react$/ }, args => { + resolved.push(args.path); + return { path: args.path, external: true }; + }); + }, + onAfterBundle(api) { + expect(resolved).toEqual(["react"]); + api.expectFile("/out.js").toContain('from "react"'); + }, + }; + }); + + // Un-deferring a.js resolves only that record. The external record, which + // also has no source_index, is not dispatched to the plugin again. late.js is + // loaded only after the react answer, so its request for A arrives after the + // barrel deferred a.js. + itBundled("barrel/ExternalReExportNotResolvedAgainOnUnDefer", () => { + const resolved: string[] = []; + return { + files: { + "/entry.js": /* js */ ` + import { React } from 'latelib'; + import './late.js'; + console.log(React); + `, + "/late.js": ``, + "/node_modules/latelib/package.json": JSON.stringify({ + name: "latelib", + main: "./index.js", + sideEffects: false, + }), + "/node_modules/latelib/index.js": /* js */ ` + export { default as React } from 'react'; + export { A } from './a.js'; + export { B } from './b.js'; + `, + "/node_modules/latelib/a.js": /* js */ ` + export const A = "late-lib-a"; + `, + // Nobody imports B, so b.js must stay deferred. + "/node_modules/latelib/b.js": /* js */ ` + export const B = <<>>; + `, + }, + outfile: "/out.js", + plugins(builder) { + resolved.length = 0; + const reactResolved = Promise.withResolvers(); + builder.onResolve({ filter: /^react$/ }, args => { + resolved.push(args.path); + reactResolved.resolve(); + return { path: args.path, external: true }; + }); + builder.onLoad({ filter: /late\.js$/ }, async () => { + await reactResolved.promise; + return { contents: `import { A } from 'latelib'; console.log(A);`, loader: "js" }; + }); + }, + onAfterBundle(api) { + expect(resolved).toEqual(["react"]); + api.expectFile("/out.js").toContain('from "react"'); + api.expectFile("/out.js").toContain("late-lib-a"); + }, + }; + }); + + // When the plugin does not answer, the record falls back to the resolver and + // the failure is reported. Resolving the barrel again reported it twice. + itBundled("barrel/UnresolvableReExportReportedOnce", { + files: { + "/entry.js": /* js */ ` + import { Missing } from 'missinglib'; + console.log(Missing); + `, + "/node_modules/missinglib/package.json": JSON.stringify({ + name: "missinglib", + main: "./index.js", + sideEffects: false, + }), + "/node_modules/missinglib/index.js": /* js */ ` + export { Missing } from 'not-installed-pkg'; + export { B } from './b.js'; + `, + "/node_modules/missinglib/b.js": /* js */ ` + export const B = "b"; + `, + }, + outfile: "/out.js", + plugins(builder) { + builder.onResolve({ filter: /^not-installed-pkg$/ }, () => undefined); + }, + bundleErrors: { + "/node_modules/missinglib/index.js": ['Could not resolve: "not-installed-pkg"'], + }, + }); + // --- Load plugin + barrel optimization --- itBundled("barrel/LoadPlugin", {