diff --git a/docs/bundler/index.mdx b/docs/bundler/index.mdx index 5134a3d13719..911273c406f6 100644 --- a/docs/bundler/index.mdx +++ b/docs/bundler/index.mdx @@ -562,28 +562,6 @@ Each `import()` of a bundled JavaScript module also becomes its own chunk. Tree With `target: "bun"`, a `require()` of a bundled ES module becomes a chunk of its own as well. The call is emitted as `import.meta.require("./chunk-.js")` and stays synchronous — Bun evaluates the chunk when the call runs — so a `require()` inside a function that never runs keeps its module out of the startup working set entirely. Tree shaking treats these chunks like `import()` chunks. `require()` of a CommonJS module is unaffected and keeps returning `module.exports`. A `require()` cycle between such chunks follows Bun's runtime `require()` of an ES module: a module required again while its chunk is still being loaded sees the partially initialized CommonJS placeholder (`{}`) rather than the live bindings the in-chunk wrapper provides. For other targets — and with `splitRequire: false` (`--no-split-require`) — the required module is inlined into the calling chunk behind a lazy wrapper instead, because the call must return synchronously. -#### Evaluation order of shared modules - -Modules that the same entrypoints load go into one shared chunk, and a chunk runs its modules in one order. Each entrypoint is a program of its own, so two entrypoints can import the same modules in different orders: - - - -```ts index.ts icon="/icons/typescript.svg" -import "./a.ts"; -import "./b.ts"; -``` - -```ts worker.ts icon="/icons/typescript.svg" -import "./b.ts"; -import "./a.ts"; -``` - - - -When `a.ts` and `b.ts` have side effects, Bun writes them to separate chunks, and each entrypoint imports the chunks in its own order. Modules that every entrypoint runs one after the other, in the same order, still share a chunk. So do modules that only declare, whatever the order of the imports, modules in an import cycle, modules that reach a top-level `await`, and modules that import a CommonJS module. - -The same applies to two `import()` targets: the target that loads first decides the order, as it does when the source runs unbundled. When both calls are in flight at once, the host decides which target evaluates first. If the order matters, `await` the first `import()` before you start the second. - #### Tree-shaking `import()` and `require()` results When the result of a string-literal `import()` or `require()` of an ES module is only ever used to read specific exports, the exports nobody reads are dropped from that module — and whatever only they depended on tree-shakes with them. This applies with and without `splitting`; the module is still loaded lazily, exactly where the call is written. diff --git a/src/bundler/Chunk.rs b/src/bundler/Chunk.rs index ff44a441c158..5d73ed9f2ce9 100644 --- a/src/bundler/Chunk.rs +++ b/src/bundler/Chunk.rs @@ -105,10 +105,7 @@ bitflags::bitflags! { const IS_EXECUTABLE = 1 << 0; const HAS_HTML_CHUNK = 1 << 1; const IS_BROWSER_CHUNK_FROM_SERVER_BUILD = 1 << 2; - /// The file of this entry point is in another chunk, and the entry point loads a chunk that - /// `split_chunks_by_evaluation_order` made. - const RANKS_IMPORTS_FROM_ENTRY_POINT_FILE = 1 << 3; - // _padding: u4 = 0 + // _padding: u5 = 0 } } diff --git a/src/bundler/lib.rs b/src/bundler/lib.rs index 3d1477db8e5c..40d6557f436e 100644 --- a/src/bundler/lib.rs +++ b/src/bundler/lib.rs @@ -124,8 +124,6 @@ pub mod linker_context { #[path = "mergeSmallChunks.rs"] pub mod merge_small_chunks; - #[path = "splitChunksByEvaluationOrder.rs"] - pub(crate) mod split_chunks_by_evaluation_order; #[path = "crossChunkNames.rs"] pub mod cross_chunk_names; diff --git a/src/bundler/linker_context/README.md b/src/bundler/linker_context/README.md index fb0e6e633276..5c93841fd926 100644 --- a/src/bundler/linker_context/README.md +++ b/src/bundler/linker_context/README.md @@ -745,19 +745,6 @@ The renamed symbols are then used during final code generation to produce output - Keeps a chunk out of the fold when it can be in the middle of being evaluated while an entry of its class loads (it, or a file that statically imports its way to it, `require()`s a split ES module): the entry's chunk reads the other members then - With `--min-chunk-size`, additionally folds small chunks with no top-level side effects into a chunk loaded by a superset of their entries when every dependency is already loaded wherever the target is, no static import cycle between chunks results, and every CommonJS/ESM wrapper the moved code initializes at the top level is already initialized by a chunk the target imports -#### `splitChunksByEvaluationOrder.rs` - -**Purpose**: Keeps files out of one chunk when the entry points that load them run them in different orders. A chunk prints its files in one order, and each chunk imports the other chunks in its own order. Runs after `mergeSmallChunks.rs`, on the entry bits that it leaves. - -**Key functions**: - -- Groups the files by chunk key (`File.entry_bits`). Only the entry points that can be the first to load a group count (`EntryLoadGraph::load_class`) -- Only a file that runs something when it loads has an order that can be observed (`loading_file_has_no_side_effects`), and a file of the group that imports such a file -- A group with such a file in an import cycle stays as it is: chunks import each other for the bindings that they use, which in a cycle is not the order in which the files run. So does a group with such a file that reaches a top-level await: a chunk does not wait for a chunk that it does not import. And a group with such a file that imports a wrapped file: the call of the wrapper prints where the `import` is, between the files that the other imports run -- A group that a split `require()` loads stays as it is: the imports of a chunk are not ranked by what the call runs -- Walks the imports of each such entry point (`for_each_edge`), in parallel, for the order in which it evaluates those files -- Files that every order has back to back, in the same order, are a run. `computeChunks()` adds the number of the run to the chunk key when a group has several. The other files of the group keep the key as it is - #### `computeCrossChunkDependencies.rs` **Purpose**: Resolves dependencies between different chunks. diff --git a/src/bundler/linker_context/computeChunks.rs b/src/bundler/linker_context/computeChunks.rs index 8ebff0d1f3be..bde0bdb1e091 100644 --- a/src/bundler/linker_context/computeChunks.rs +++ b/src/bundler/linker_context/computeChunks.rs @@ -18,7 +18,6 @@ use super::find_all_imported_parts_in_js_order::find_all_imported_parts_in_js_or use super::find_imported_css_files_in_js_order::find_imported_css_files_in_js_order; use super::find_imported_files_in_css_order::find_imported_files_in_css_order; use super::merge_small_chunks::merge_small_chunks; -use super::split_chunks_by_evaluation_order::split_chunks_by_evaluation_order; #[inline(always)] fn make_flags(has_html_chunk: bool, is_browser_chunk_from_server_build: bool) -> chunk::Flags { @@ -282,30 +281,6 @@ pub(crate) fn compute_chunks( let min_chunk_size = this.options.min_chunk_size; merge_small_chunks(this, temp, min_chunk_size)?; } - let run_of_file: Vec = if code_splitting { - split_chunks_by_evaluation_order(this)? - } else { - Vec::new() - }; - if !run_of_file.is_empty() { - let file_entry_bits = this.graph.files.items_entry_bits(); - let mut loads_split_off_chunk = AutoBitSet::init_empty(this.graph.entry_points.len())?; - for (source_index, _) in run_of_file.iter().enumerate().filter(|(_, run)| **run != 0) { - loads_split_off_chunk.set_union(&file_entry_bits[source_index]); - } - let entry_source_indices = this.graph.entry_points.items_source_index(); - for (entry_id, &source_index) in entry_source_indices.iter().enumerate() { - let chunk_index = entry_point_to_js_chunk_idx[entry_id]; - if loads_split_off_chunk.is_set(entry_id) - && file_entry_bits[source_index as usize].count() >= 2 - && chunk_index != u32::MAX - { - js_chunks.values_mut()[chunk_index as usize] - .flags - .insert(chunk::Flags::RANKS_IMPORTS_FROM_ENTRY_POINT_FILE); - } - } - } let css_asts = this.graph.ast.items_css(); let ast_targets = this.graph.ast.items_target(); @@ -352,13 +327,8 @@ pub(crate) fn compute_chunks( if !contributes_code.is_set(source_index.get() as usize) { continue; } - let entry_bits_key = entry_bits.bytes(this.graph.entry_points.len()); - let js_chunk_key: &[u8] = match run_of_file.get(source_index.get() as usize) - { - None | Some(0) => temp.alloc_slice_copy(entry_bits_key), - Some(run) => temp - .alloc_slice_copy(&[entry_bits_key, &run.to_le_bytes()].concat()), - }; + let js_chunk_key = + temp.alloc_slice_copy(entry_bits.bytes(this.graph.entry_points.len())); let js_chunk_entry = js_chunks.get_or_put(js_chunk_key)?; if !js_chunk_entry.found_existing { diff --git a/src/bundler/linker_context/findAllImportedPartsInJSOrder.rs b/src/bundler/linker_context/findAllImportedPartsInJSOrder.rs index 7695d5d93780..78710de09c93 100644 --- a/src/bundler/linker_context/findAllImportedPartsInJSOrder.rs +++ b/src/bundler/linker_context/findAllImportedPartsInJSOrder.rs @@ -266,14 +266,6 @@ impl WalkPlan { plan.chunk_of_file[file] = chunk_index; } } - // Several chunks can have one key (`split_chunks_by_evaluation_order`). - for (chunk_index, chunk) in chunks.iter().enumerate() { - if matches!(chunk.content, chunk::Content::Javascript(_)) { - for &file in chunk.files_with_parts_in_chunk.keys() { - plan.chunk_of_file[file as usize] = chunk_index as u32; - } - } - } } // The entry point that loads first among the chunk's entry points owns the chunk. @@ -331,7 +323,7 @@ impl WalkPlan { } #[derive(Clone, Copy)] -pub(crate) enum Edge { +enum Edge { /// `file` runs here, under the same load. Import(IndexInt), /// A split `require()` in a part that runs at load: the chunk of `file` runs here. @@ -342,7 +334,7 @@ pub(crate) enum Edge { /// The files that a file leads to, in evaluation order, with the part that leads there. `runs`: the load evaluates the file. /// A file runs where it is imported, not where its bindings are used: `part.dependencies` does not order what runs. -pub(crate) fn for_each_edge( +fn for_each_edge( c: &LinkerContext, source_index: IndexInt, runs: bool, @@ -856,13 +848,6 @@ fn reached_chunks_in_order( // Start where the load enters this chunk. let mut roots: Vec = chunk.files_with_parts_in_chunk.keys().to_vec(); roots.sort_unstable_by_key(|&source_index| order.entered[source_index as usize]); - // The load enters at the file of the entry point, which is in another chunk. - if chunk - .flags - .contains(chunk::Flags::RANKS_IMPORTS_FROM_ENTRY_POINT_FILE) - { - roots.insert(0, chunk.entry_point.source_index()); - } let mut reached: Vec = Vec::new(); let mut reached_set = AutoBitSet::init_empty(chunks_len)?; @@ -896,9 +881,7 @@ fn reached_chunks_in_order( visited.set(source_index as usize); let is_file_in_chunk = if css[source_index as usize].is_none() { - // Several chunks can have one key (`split_chunks_by_evaluation_order`). entry_bits.eql(&file_entry_bits[source_index as usize]) - && matches!(chunk_of_file[source_index as usize], other if other == chunk_index || other == u32::MAX) } else { entry_bits.has_intersection(&file_entry_bits[source_index as usize]) }; diff --git a/src/bundler/linker_context/mergeSmallChunks.rs b/src/bundler/linker_context/mergeSmallChunks.rs index 6483d0c06e63..86f752290986 100644 --- a/src/bundler/linker_context/mergeSmallChunks.rs +++ b/src/bundler/linker_context/mergeSmallChunks.rs @@ -704,258 +704,6 @@ fn entries_loaded_mid_evaluation( Ok(loads) } -/// Which entry points can load before which. Reads `File.entry_bits`. -pub(crate) struct EntryLoadGraph { - /// Which entries statically contain a live `import()` of each dynamic entry. - importer_bits: Vec, - /// Dynamic entries some live split `require()` loads. The call - /// runs while its importer is still evaluating, so no importer is - /// guaranteed to precede the target; folding shared code into the - /// importer's chunk could place it after the call site. - required_sync: AutoBitSet, - /// ... and, per file making such calls, the entries it loads that way. - sync_calls: ArrayHashMap, - /// A dynamic entry can stand in for its importers when some live code - /// `import()`s it, nothing `require()`s it, and no importer is mid-evaluation - /// at a top-level await while it loads. Every other entry is a root: it may - /// be the first thing loaded (a user entry), or nothing says what precedes it. - guaranteed: AutoBitSet, - /// Over the `import()` graph of the entries. The virtual root is `idom.len() - 1`. - idom: Vec, - seen: Vec, - epoch: u32, - worklist: Vec, -} - -impl EntryLoadGraph { - pub(crate) fn new(this: &LinkerContext) -> crate::Result { - let entry_points_len = this.graph.entry_points.len(); - let entry_source_indices = this.graph.entry_points.items_source_index(); - let kinds = this.graph.files.items_entry_point_kind(); - let css_asts = this.graph.ast.items_css(); - let import_records = this.graph.ast.items_import_records(); - let parts = this.graph.ast.items_parts(); - let flags = this.graph.meta.items_flags(); - let file_entry_bits = this.graph.files.items_entry_bits(); - - let mut entry_id_by_source: Vec = vec![u32::MAX; this.graph.files.len()]; - for (entry_id, &source_index) in entry_source_indices.iter().enumerate() { - let slot = &mut entry_id_by_source[source_index as usize]; - if *slot == u32::MAX { - *slot = entry_id as u32; - } - } - let is_live_js = |source_index: u32| { - this.graph.files_live.is_set(source_index as usize) - && css_asts[source_index as usize].is_none() - }; - - let mut importer_bits: Vec = Vec::with_capacity(entry_points_len); - for _ in 0..entry_points_len { - importer_bits.push(AutoBitSet::init_empty(entry_points_len)?); - } - let mut required_sync = AutoBitSet::init_empty(entry_points_len)?; - let mut sync_calls: ArrayHashMap = ArrayHashMap::new(); - for source_index in this.graph.reachable_files.iter() { - let source_index = source_index.get(); - if !is_live_js(source_index) { - continue; - } - let records = &import_records[source_index as usize]; - let parts_live = &this.graph.parts_live[source_index as usize]; - for (part_index, part) in parts[source_index as usize].as_slice().iter().enumerate() { - if !parts_live.is_set(part_index) { - continue; - } - for &record_index in part.import_record_indices.iter() { - let record = &records[record_index as usize]; - if !record.source_index.is_valid() - || !this.is_external_dynamic_import(record, source_index) - { - continue; - } - let target_entry = entry_id_by_source[record.source_index.get() as usize]; - if target_entry == u32::MAX { - continue; - } - if record - .flags - .contains(ImportRecordFlags::CROSS_CHUNK_REQUIRE) - { - required_sync.set(target_entry as usize); - match sync_calls.entry(source_index) { - MapEntry::Occupied(calls) => { - calls.into_mut().set(target_entry as usize) - } - MapEntry::Vacant(calls) => { - let mut called = AutoBitSet::init_empty(entry_points_len)?; - called.set(target_entry as usize); - calls.insert(called); - } - } - } else { - importer_bits[target_entry as usize] - .set_union(&file_entry_bits[source_index as usize]); - } - } - } - } - // A self-`import()` cannot be the load that comes first. - for (entry_id, importers) in importer_bits.iter_mut().enumerate() { - importers.unset(entry_id); - } - - // An entry whose chunk (or a chunk it imports) uses top-level await can - // still be mid-evaluation when an `import()` it started links, so it - // guarantees nothing: a chunk that `import()` target then imported from - // it would wait on it forever. - let mut awaits = AutoBitSet::init_empty(entry_points_len)?; - for source_index in this.graph.reachable_files.iter() { - let source_index = source_index.get(); - if is_live_js(source_index) - && flags[source_index as usize].is_async_or_has_async_dependency - { - awaits.set_union(&file_entry_bits[source_index as usize]); - } - } - - let mut guaranteed = AutoBitSet::init_empty(entry_points_len)?; - for entry_id in 0..entry_points_len { - if kinds[entry_source_indices[entry_id] as usize] == EntryPoint::Kind::DynamicImport - && !required_sync.is_set(entry_id) - && importer_bits[entry_id].find_first_set().is_some() - && !importer_bits[entry_id].has_intersection(&awaits) - { - guaranteed.set(entry_id); - } - } - // The `import()` graph over entries as CSR (`successors_of(n)` = - // `edges[offsets[n]..offsets[n + 1]]`): a virtual root precedes every - // root, and each importer precedes the guaranteed entries it `import()`s. - let vroot = entry_points_len; - let mut offsets: Vec = vec![0; entry_points_len + 3]; - for entry_id in 0..entry_points_len { - if !guaranteed.is_set(entry_id) { - offsets[vroot + 2] += 1; - continue; - } - let mut iter = importer_bits[entry_id].iterator::(); - while let Some(importer) = iter.next() { - offsets[importer + 2] += 1; - } - } - for i in 2..offsets.len() { - offsets[i] += offsets[i - 1]; - } - let mut edges: Vec = vec![0; offsets[offsets.len() - 1] as usize]; - for entry_id in 0..entry_points_len { - if !guaranteed.is_set(entry_id) { - edges[offsets[vroot + 1] as usize] = entry_id as u32; - offsets[vroot + 1] += 1; - continue; - } - let mut iter = importer_bits[entry_id].iterator::(); - while let Some(importer) = iter.next() { - edges[offsets[importer + 1] as usize] = entry_id as u32; - offsets[importer + 1] += 1; - } - } - let successors = |node: usize| &edges[offsets[node] as usize..offsets[node + 1] as usize]; - let idom = immediate_dominators( - entry_points_len + 1, - successors, - vroot, - |entry_id, each: &mut dyn FnMut(usize)| { - if guaranteed.is_set(entry_id) { - let mut iter = importer_bits[entry_id].iterator::(); - while let Some(importer) = iter.next() { - each(importer); - } - } else { - each(vroot); - } - }, - ); - - Ok(EntryLoadGraph { - importer_bits, - required_sync, - sync_calls, - guaranteed, - idom, - seen: vec![0; entry_points_len], - epoch: 0, - worklist: Vec::new(), - }) - } - - /// Whether a live split `require()` loads an entry of `key`. - pub(crate) fn is_required(&self, key: &AutoBitSet) -> bool { - self.required_sync.has_intersection(key) - } - - /// `key` minus each guaranteed entry none of whose - /// importers a root reaches through `import()`s without passing through the - /// key; such an importer ran after some entry of the key, so that entry's - /// chunk was loaded first. (An importer cycle no root reaches never loads, - /// so it does not count either.) A key whose every entry is redundant is - /// never loaded and left alone. Each entry that is left can be the first of `key` to load. - pub(crate) fn load_class(&mut self, key: &AutoBitSet) -> crate::Result { - let vroot = self.idom.len() - 1; - let mut class = key.clone()?; - let mut dropped = false; - let mut candidates = key.iterator::(); - while let Some(entry_id) = candidates.next() { - if !self.guaranteed.is_set(entry_id) { - continue; - } - // Fast path: a key entry dominates `entry_id`, or nothing loads it. - let mut up = self.idom[entry_id]; - let mut preceded = up == UNREACHED; - while !preceded && up as usize != vroot { - if key.is_set(up as usize) { - preceded = true; - } else { - up = self.idom[up as usize]; - } - } - if !preceded { - // Walk importers backward, stopping at the key; a root reached - // this way loads `entry_id` before anything in the key. - preceded = true; - self.epoch += 1; - self.worklist.clear(); - self.worklist.push(entry_id); - 'walk: while let Some(below) = self.worklist.pop() { - let mut iter = self.importer_bits[below].iterator::(); - while let Some(importer) = iter.next() { - if key.is_set(importer) - || self.seen[importer] == self.epoch - || self.idom[importer] == UNREACHED - { - continue; - } - if !self.guaranteed.is_set(importer) { - preceded = false; - break 'walk; - } - self.seen[importer] = self.epoch; - self.worklist.push(importer); - } - } - } - if preceded { - class.unset(entry_id); - dropped = true; - } - } - if dropped && class.find_first_set().is_none() { - return Ok(key.clone()?); - } - Ok(class) - } -} - /// Folds code-splitting chunks into other chunks where that is unobservable, /// so fewer modules are loaded at runtime. /// @@ -966,7 +714,7 @@ impl EntryLoadGraph { /// /// 1. An `import()` entry point `D` is redundant in a key when, whichever way /// `D` gets loaded, some other entry in that key has already been loaded -/// (`EntryLoadGraph::load_class`: no importer of `D` can be reached from a process +/// (`load_class` below: no importer of `D` can be reached from a process /// root without passing through the key). Keys that are equal /// after dropping their redundant entries describe chunks that are always /// loaded together: with one user entry `main` and a lazy `import("./x")` @@ -1018,10 +766,19 @@ pub(crate) fn merge_small_chunks( } let css_asts = this.graph.ast.items_css(); let ast_targets = this.graph.ast.items_target(); + let import_records = this.graph.ast.items_import_records(); + let parts = this.graph.ast.items_parts(); let flags = this.graph.meta.items_flags(); let file_entry_bits = this.graph.files.items_entry_bits(); let files_len = this.graph.files.len(); + let entry_id_by_source: &mut [u32] = temp.alloc_slice_fill_copy(files_len, u32::MAX); + for (entry_id, &source_index) in entry_source_indices.iter().enumerate() { + let slot = &mut entry_id_by_source[source_index as usize]; + if *slot == u32::MAX { + *slot = entry_id as u32; + } + } let is_dynamic_entry = |entry_id: usize| { kinds[entry_source_indices[entry_id] as usize] == EntryPoint::Kind::DynamicImport }; @@ -1029,7 +786,203 @@ pub(crate) fn merge_small_chunks( this.graph.files_live.is_set(source_index as usize) && css_asts[source_index as usize].is_none() }; - let mut load_graph = EntryLoadGraph::new(this)?; + + // Which entries statically contain a live `import()` of each dynamic entry. + let mut importer_bits: Vec = Vec::with_capacity(entry_points_len); + for _ in 0..entry_points_len { + importer_bits.push(AutoBitSet::init_empty(entry_points_len)?); + } + // Dynamic entries some live split `require()` loads. The call + // runs while its importer is still evaluating, so no importer is + // guaranteed to precede the target; folding shared code into the + // importer's chunk could place it after the call site. + let mut required_sync = AutoBitSet::init_empty(entry_points_len)?; + // ... and, per file making such calls, the entries it loads that way. + let mut sync_calls: ArrayHashMap = ArrayHashMap::new(); + for source_index in this.graph.reachable_files.iter() { + let source_index = source_index.get(); + if !is_live_js(source_index) { + continue; + } + let records = &import_records[source_index as usize]; + let parts_live = &this.graph.parts_live[source_index as usize]; + for (part_index, part) in parts[source_index as usize].as_slice().iter().enumerate() { + if !parts_live.is_set(part_index) { + continue; + } + for &record_index in part.import_record_indices.iter() { + let record = &records[record_index as usize]; + if !record.source_index.is_valid() + || !this.is_external_dynamic_import(record, source_index) + { + continue; + } + let target_entry = entry_id_by_source[record.source_index.get() as usize]; + if target_entry == u32::MAX { + continue; + } + if record + .flags + .contains(ImportRecordFlags::CROSS_CHUNK_REQUIRE) + { + required_sync.set(target_entry as usize); + match sync_calls.entry(source_index) { + MapEntry::Occupied(calls) => calls.into_mut().set(target_entry as usize), + MapEntry::Vacant(calls) => { + let mut called = AutoBitSet::init_empty(entry_points_len)?; + called.set(target_entry as usize); + calls.insert(called); + } + } + } else { + importer_bits[target_entry as usize] + .set_union(&file_entry_bits[source_index as usize]); + } + } + } + } + // A self-`import()` cannot be the load that comes first. + for (entry_id, importers) in importer_bits.iter_mut().enumerate() { + importers.unset(entry_id); + } + + // An entry whose chunk (or a chunk it imports) uses top-level await can + // still be mid-evaluation when an `import()` it started links, so it + // guarantees nothing: a chunk that `import()` target then imported from + // it would wait on it forever. + let mut awaits = AutoBitSet::init_empty(entry_points_len)?; + for source_index in this.graph.reachable_files.iter() { + let source_index = source_index.get(); + if is_live_js(source_index) && flags[source_index as usize].is_async_or_has_async_dependency + { + awaits.set_union(&file_entry_bits[source_index as usize]); + } + } + + // A dynamic entry can stand in for its importers when some live code + // `import()`s it, nothing `require()`s it, and no importer is mid-evaluation + // at a top-level await while it loads. Every other entry is a root: it may + // be the first thing loaded (a user entry), or nothing says what precedes it. + let mut guaranteed = AutoBitSet::init_empty(entry_points_len)?; + for entry_id in 0..entry_points_len { + if is_dynamic_entry(entry_id) + && !required_sync.is_set(entry_id) + && importer_bits[entry_id].find_first_set().is_some() + && !importer_bits[entry_id].has_intersection(&awaits) + { + guaranteed.set(entry_id); + } + } + // The `import()` graph over entries as CSR (`successors_of(n)` = + // `edges[offsets[n]..offsets[n + 1]]`): a virtual root precedes every + // root, and each importer precedes the guaranteed entries it `import()`s. + let vroot = entry_points_len; + let offsets: &mut [u32] = temp.alloc_slice_fill_copy(entry_points_len + 3, 0u32); + for entry_id in 0..entry_points_len { + if !guaranteed.is_set(entry_id) { + offsets[vroot + 2] += 1; + continue; + } + let mut iter = importer_bits[entry_id].iterator::(); + while let Some(importer) = iter.next() { + offsets[importer + 2] += 1; + } + } + for i in 2..offsets.len() { + offsets[i] += offsets[i - 1]; + } + let edges: &mut [u32] = temp.alloc_slice_fill_copy(offsets[offsets.len() - 1] as usize, 0u32); + for entry_id in 0..entry_points_len { + if !guaranteed.is_set(entry_id) { + edges[offsets[vroot + 1] as usize] = entry_id as u32; + offsets[vroot + 1] += 1; + continue; + } + let mut iter = importer_bits[entry_id].iterator::(); + while let Some(importer) = iter.next() { + edges[offsets[importer + 1] as usize] = entry_id as u32; + offsets[importer + 1] += 1; + } + } + let successors = |node: usize| &edges[offsets[node] as usize..offsets[node + 1] as usize]; + let idom = immediate_dominators( + entry_points_len + 1, + successors, + vroot, + |entry_id, each: &mut dyn FnMut(usize)| { + if guaranteed.is_set(entry_id) { + let mut iter = importer_bits[entry_id].iterator::(); + while let Some(importer) = iter.next() { + each(importer); + } + } else { + each(vroot); + } + }, + ); + + // `load_class(key)`: `key` minus each guaranteed entry none of whose + // importers a root reaches through `import()`s without passing through the + // key; such an importer ran after some entry of the key, so that entry's + // chunk was loaded first. (An importer cycle no root reaches never loads, + // so it does not count either.) A key whose every entry is redundant is + // never loaded and left alone. + let mut seen: Vec = vec![0; entry_points_len]; + let mut epoch: u32 = 0; + let mut worklist: Vec = Vec::new(); + let mut load_class = |key: &AutoBitSet| -> crate::Result { + let mut class = key.clone()?; + let mut dropped = false; + let mut candidates = key.iterator::(); + while let Some(entry_id) = candidates.next() { + if !guaranteed.is_set(entry_id) { + continue; + } + // Fast path: a key entry dominates `entry_id`, or nothing loads it. + let mut up = idom[entry_id]; + let mut preceded = up == UNREACHED; + while !preceded && up as usize != vroot { + if key.is_set(up as usize) { + preceded = true; + } else { + up = idom[up as usize]; + } + } + if !preceded { + // Walk importers backward, stopping at the key; a root reached + // this way loads `entry_id` before anything in the key. + preceded = true; + epoch += 1; + worklist.clear(); + worklist.push(entry_id); + 'walk: while let Some(below) = worklist.pop() { + let mut iter = importer_bits[below].iterator::(); + while let Some(importer) = iter.next() { + if key.is_set(importer) + || seen[importer] == epoch + || idom[importer] == UNREACHED + { + continue; + } + if !guaranteed.is_set(importer) { + preceded = false; + break 'walk; + } + seen[importer] = epoch; + worklist.push(importer); + } + } + } + if preceded { + class.unset(entry_id); + dropped = true; + } + } + if dropped && class.find_first_set().is_none() { + return Ok(key.clone()?); + } + Ok(class) + }; // Group the live JS files by their chunk key, and the groups by their // load-condition class (the key with redundant dynamic entries removed). @@ -1101,7 +1054,7 @@ pub(crate) fn merge_small_chunks( let group = match entry { MapEntry::Occupied(entry) => entry.into_mut(), MapEntry::Vacant(entry) => { - let class = load_graph.load_class(bits)?; + let class = load_class(bits)?; match classes.entry(temp.alloc_slice_copy(class.bytes(entry_points_len))) { MapEntry::Occupied(e) => e.into_mut().1.push(group_index), MapEntry::Vacant(e) => { @@ -1154,8 +1107,8 @@ pub(crate) fn merge_small_chunks( for (group, loads) in entries_loaded_mid_evaluation( this, - &load_graph.sync_calls, - &load_graph.required_sync, + &sync_calls, + &required_sync, file_entry_bits, group_of_file, groups.count(), @@ -1269,11 +1222,10 @@ pub(crate) fn merge_small_chunks( dominated.push(AutoBitSet::init_empty(entry_points_len)?); } for entry_id in 0..entry_points_len { - let mut up = load_graph.idom[entry_id]; - // The root of the dominator tree stands for the process. It comes after the entry points. - while up != UNREACHED && up as usize != entry_points_len { + let mut up = idom[entry_id]; + while up != UNREACHED && up as usize != vroot { dominated[up as usize].set(entry_id); - up = load_graph.idom[up as usize]; + up = idom[up as usize]; } } for group in groups.values_mut() { diff --git a/src/bundler/linker_context/splitChunksByEvaluationOrder.rs b/src/bundler/linker_context/splitChunksByEvaluationOrder.rs deleted file mode 100644 index d58946ea22fd..000000000000 --- a/src/bundler/linker_context/splitChunksByEvaluationOrder.rs +++ /dev/null @@ -1,376 +0,0 @@ -use crate::mal_prelude::*; -use bun_collections::{AutoBitSet, StringHashMap}; - -use crate::linker_context::find_all_imported_parts_in_js_order::{Edge, for_each_edge}; -use crate::linker_context::merge_small_chunks::EntryLoadGraph; -use crate::options::Loader; -use crate::{Index, IndexInt, LinkerContext}; - -const NONE: u32 = u32::MAX; - -/// The files with one `File.entry_bits` that several entry points load. -struct Group { - /// One of the files. - source_index: IndexInt, - /// The entry points that can be the first to load the files (`EntryLoadGraph::load_class`). - load_class: AutoBitSet, - /// The lowest id in `load_class`. The other orders are compared with its order. - reference_entry_id: u32, - /// The last file of the group in the order that is being read, and the entry point of that order. - last_file: IndexInt, - last_file_entry_id: u32, - /// How many runs of files every order has back to back, in the same order. - runs_len: u32, - stays_whole: bool, -} - -/// Files share a chunk when the same entry points load them, and run them back to back in the same order: -/// a chunk prints its files in one order. Per file: what the key of its chunk has next to `File.entry_bits`. -/// Empty: 0 for every file. -pub(crate) fn split_chunks_by_evaluation_order(c: &LinkerContext) -> crate::Result> { - let _trace = bun_core::perf::trace("Bundler.splitChunksByEvaluationOrder"); - let entry_points_len = c.graph.entry_points.len(); - if entry_points_len < 2 { - return Ok(Vec::new()); - } - let files_len = c.graph.files.len(); - let file_entry_bits = c.graph.files.items_entry_bits(); - let css = c.graph.ast.items_css(); - let loaders = c.parse_graph().input_files.items_loader(); - - // Only the order of files that run something when they load can be observed. - let mut key_of_file: Vec = vec![NONE; files_len]; - let mut id_of_key: StringHashMap = StringHashMap::default(); - // Per key: one of its files, and how many of them run something. - let mut keys: Vec<(IndexInt, u32)> = Vec::new(); - let mut is_tracked: Vec = vec![false; files_len]; - for source_index in c.graph.reachable_files.iter() { - let source_index = source_index.get(); - let id = source_index as usize; - if source_index == Index::RUNTIME.value() - || !c.graph.files_live.is_set(id) - || css[id].is_some() - || loaders[id] == Loader::Html - || file_entry_bits[id].count() < 2 - { - continue; - } - let key = *id_of_key.get_or_put_value( - file_entry_bits[id].bytes(entry_points_len), - keys.len() as u32, - )?; - if key as usize == keys.len() { - keys.push((source_index, 0)); - } - key_of_file[id] = key; - if !c.loading_file_has_no_side_effects(source_index) { - is_tracked[id] = true; - keys[key as usize].1 += 1; - } - } - - // One such file has one order, and so has one loader. - let mut load_graph: Option = None; - let mut groups: Vec = Vec::new(); - let mut group_of_key: Vec = vec![NONE; keys.len()]; - for (key, &(source_index, effects_len)) in keys.iter().enumerate() { - if effects_len < 2 { - continue; - } - let load_graph = match &mut load_graph { - Some(load_graph) => load_graph, - None => load_graph.insert(EntryLoadGraph::new(c)?), - }; - // The imports of a chunk are not ranked by what a split `require()` runs (`reached_chunks_in_order`). - if load_graph.is_required(&file_entry_bits[source_index as usize]) { - continue; - } - let load_class = load_graph.load_class(&file_entry_bits[source_index as usize])?; - let Some(reference_entry_id) = load_class.find_first_set() else { - continue; - }; - if load_class.count() < 2 { - continue; - } - group_of_key[key] = groups.len() as u32; - groups.push(Group { - source_index, - load_class, - reference_entry_id: reference_entry_id as u32, - last_file: NONE, - last_file_entry_id: NONE, - runs_len: 0, - stays_whole: false, - }); - } - if groups.is_empty() { - return Ok(Vec::new()); - } - let group_of_file: Vec = key_of_file - .iter() - .map(|&key| { - if key == NONE { - NONE - } else { - group_of_key[key as usize] - } - }) - .collect(); - - // A file that imports one of those files runs it, so it has a place in the order too. - let mut importers_of_file: Vec> = vec![Vec::new(); files_len]; - let mut worklist: Vec = Vec::new(); - for (id, &group) in group_of_file.iter().enumerate() { - if group == NONE { - is_tracked[id] = false; - continue; - } - if is_tracked[id] { - worklist.push(id as IndexInt); - } - for_each_edge(c, id as IndexInt, true, |_, edge| { - if let Edge::Import(other) = edge - && group_of_file[other as usize] == group - { - importers_of_file[other as usize].push(id as IndexInt); - } - }); - } - while let Some(source_index) = worklist.pop() { - for &importer in &importers_of_file[source_index as usize] { - if !core::mem::replace(&mut is_tracked[importer as usize], true) { - worklist.push(importer); - } - } - } - - // A chunk imports another for the bindings that it uses, not for every `import` of its files. In an import - // cycle that is not the order in which the files run. And a chunk does not wait for the top-level await of - // a chunk that it does not import. - let is_cyclic = find_cyclic_files(c); - let flags = c.graph.meta.items_flags(); - let mut entries_to_compare = AutoBitSet::init_empty(entry_points_len)?; - let mut inits: Vec = Vec::new(); - for (id, &group) in group_of_file.iter().enumerate() { - if !is_tracked[id] { - continue; - } - // The `import` of a wrapped file is a call where the `import` is, ahead of the files that the next - // `import` runs. The `import` of a chunk runs ahead of all the code of a chunk. - inits.clear(); - c.top_level_inits(id as IndexInt, &mut inits); - if is_cyclic[id] || flags[id].is_async_or_has_async_dependency || !inits.is_empty() { - groups[group as usize].stays_whole = true; - } - } - // The chunk of an entry point whose file is in another chunk imports its chunks in the order of that file. - let entry_points = c.graph.entry_points.items_source_index(); - for group in &mut groups { - let mut entry_ids = file_entry_bits[group.source_index as usize].iterator::(); - while let Some(entry_id) = entry_ids.next() { - let id = entry_points[entry_id] as usize; - group.stays_whole |= is_cyclic[id] && file_entry_bits[id].count() >= 2; - } - } - for group in groups.iter().filter(|group| !group.stays_whole) { - entries_to_compare.set_union(&group.load_class); - } - - let mut orders: Vec = Vec::new(); - let mut entry_ids = entries_to_compare.iterator::(); - while let Some(entry_id) = entry_ids.next() { - orders.push(EvaluationOrder { - entry_id: entry_id as u32, - files: Vec::new(), - }); - } - c.worker_pool().each_ptr( - (c, is_tracked.as_slice()), - |&(c, is_tracked): &(&LinkerContext, &[bool]), order: *mut EvaluationOrder, _: usize| { - // SAFETY: `each_ptr` hands each task a distinct `*mut EvaluationOrder`. - unsafe { &mut *order }.compute(c, is_tracked); - }, - &mut orders, - ); - - // Per file: the file of its group that the reference order has right before it. - let mut reference_previous: Vec = vec![NONE; files_len]; - // Whether every order has that file right before it. - let mut follows_previous: Vec = vec![false; files_len]; - // By ascending entry point id, so the reference of a group comes first. - for order in &orders { - for &source_index in &order.files { - let id = source_index as usize; - let group = &mut groups[group_of_file[id] as usize]; - if group.stays_whole || !group.load_class.is_set(order.entry_id as usize) { - continue; - } - if group.last_file_entry_id != order.entry_id { - group.last_file_entry_id = order.entry_id; - group.last_file = NONE; - } - let previous = core::mem::replace(&mut group.last_file, source_index); - if group.reference_entry_id == order.entry_id { - reference_previous[id] = previous; - follows_previous[id] = previous != NONE; - } else if reference_previous[id] != previous { - follows_previous[id] = false; - } - } - } - - for order in &orders { - for &source_index in &order.files { - let group = &mut groups[group_of_file[source_index as usize] as usize]; - if group.reference_entry_id == order.entry_id && !group.stays_whole { - group.runs_len += !follows_previous[source_index as usize] as u32; - } - } - } - if groups.iter().all(|group| group.runs_len < 2) { - return Ok(Vec::new()); - } - let mut run_of_file: Vec = vec![0; files_len]; - let mut runs_len: u32 = 0; - for order in &orders { - for &source_index in &order.files { - let group = &groups[group_of_file[source_index as usize] as usize]; - if group.reference_entry_id == order.entry_id && group.runs_len >= 2 { - runs_len += !follows_previous[source_index as usize] as u32; - run_of_file[source_index as usize] = runs_len; - } - } - } - // `merge_small_chunks` can move a file into a group that not every entry point of the group imports it from. - // It imports a file that is split off, so it does not stay with the files that the split-off files import. - for (id, &group) in group_of_file.iter().enumerate() { - if is_tracked[id] && run_of_file[id] == 0 && groups[group as usize].runs_len >= 2 { - runs_len += 1; - run_of_file[id] = runs_len; - } - } - Ok(run_of_file) -} - -/// Per file: whether it is in a strongly connected component of several files (Tarjan's algorithm). -fn find_cyclic_files(c: &LinkerContext) -> Vec { - const UNVISITED: u32 = u32::MAX; - struct Frame { - source_index: IndexInt, - /// `edges[first_edge..]` are the imports of the file, `edges[next_edge..]` the ones still to follow. - first_edge: usize, - next_edge: usize, - } - - let files_len = c.graph.files.len(); - let mut is_cyclic: Vec = vec![false; files_len]; - // The position in `scc_stack` when the file was pushed. - let mut index: Vec = vec![UNVISITED; files_len]; - let mut lowlink: Vec = vec![0; files_len]; - let mut on_scc_stack: Vec = vec![false; files_len]; - let mut scc_stack: Vec = Vec::new(); - let mut edges: Vec = Vec::new(); - let mut stack: Vec = Vec::new(); - - for &root in c.graph.entry_points.items_source_index() { - if index[root as usize] != UNVISITED { - continue; - } - let mut next_file = Some(root); - loop { - if let Some(source_index) = next_file.take() { - index[source_index as usize] = scc_stack.len() as u32; - lowlink[source_index as usize] = scc_stack.len() as u32; - scc_stack.push(source_index); - on_scc_stack[source_index as usize] = true; - let first_edge = edges.len(); - let is_live = c.graph.files_live.is_set(source_index as usize); - for_each_edge(c, source_index, is_live, |_, edge| match edge { - Edge::Import(other) | Edge::LoadNow(other) => edges.push(other), - Edge::LoadLater(_) => {} - }); - stack.push(Frame { - source_index, - first_edge, - next_edge: first_edge, - }); - } - let Some(frame) = stack.last_mut() else { - break; - }; - let id = frame.source_index as usize; - if let Some(&other) = edges.get(frame.next_edge) { - frame.next_edge += 1; - if index[other as usize] == UNVISITED { - next_file = Some(other); - } else if on_scc_stack[other as usize] { - lowlink[id] = lowlink[id].min(index[other as usize]); - } - continue; - } - edges.truncate(frame.first_edge); - stack.pop(); - if let Some(parent) = stack.last() { - let parent = parent.source_index as usize; - lowlink[parent] = lowlink[parent].min(lowlink[id]); - } - if lowlink[id] == index[id] { - let component = scc_stack.drain(index[id] as usize..); - let is_cycle = component.len() > 1; - for member in component { - on_scc_stack[member as usize] = false; - is_cyclic[member as usize] = is_cycle; - } - } - } - } - is_cyclic -} - -/// The tracked files that loading an entry point evaluates, in evaluation order. -struct EvaluationOrder { - entry_id: u32, - files: Vec, -} - -impl EvaluationOrder { - /// Does not follow a split `require()`: no tracked file is loaded by one. - fn compute(&mut self, c: &LinkerContext, is_tracked: &[bool]) { - #[derive(Clone, Copy)] - enum Frame { - Enter(IndexInt), - Leave(IndexInt), - } - let entry_bits = c.graph.files.items_entry_bits(); - let mut seen = bun_core::handle_oom(AutoBitSet::init_empty(c.graph.files.len())); - let mut stack: Vec = vec![Frame::Enter( - c.graph.entry_points.items_source_index()[self.entry_id as usize], - )]; - while let Some(frame) = stack.pop() { - let source_index = match frame { - Frame::Leave(source_index) => { - self.files.push(source_index); - continue; - } - Frame::Enter(source_index) => source_index, - }; - let id = source_index as usize; - if seen.is_set(id) { - continue; - } - seen.set(id); - let runs = - c.graph.files_live.is_set(id) && entry_bits[id].is_set(self.entry_id as usize); - let mark = stack.len(); - for_each_edge(c, source_index, runs, |_, edge| { - if let Edge::Import(other) = edge { - stack.push(Frame::Enter(other)); - } - }); - if runs && is_tracked[id] { - stack.push(Frame::Leave(source_index)); - } - stack[mark..].reverse(); - } - } -} diff --git a/test/bundler/bundler_splitting.test.ts b/test/bundler/bundler_splitting.test.ts index 83429801df29..dc380bd534ce 100644 --- a/test/bundler/bundler_splitting.test.ts +++ b/test/bundler/bundler_splitting.test.ts @@ -2346,331 +2346,6 @@ describe("bundler", () => { run: { file: "/out/entry.js", stdout: "late2 b late1 b" }, }); - // index.js evaluates p, q (through its import()) and worker.js evaluates q, p. They are two programs, - // and either can be the one that loads the two files. A chunk prints its files in one order, so they - // do not share one, and each program imports the two chunks in its own order. - const evaluationOrderFiles = { - "/index.js": `import("./a.js").then(m => console.log("ok", m.a));`, - "/worker.js": `import { q } from "./q.js"; import { a } from "./a.js"; console.log("worker", q, a);`, - "/a.js": `import "./p.js"; import { q } from "./q.js"; export const a = "a" + q;`, - "/p.js": `globalThis.P = { v: "P" };`, - "/q.js": `export const q = "q" + (globalThis.P?.v ?? "-noP");`, - }; - const chunkWith = (api: BundlerTestBundleAPI, text: string) => - jsFilesIn(api).filter(file => api.readFile("/out/" + file).includes(text)); - for (const [name, options] of [ - ["", {}], - ["/ReversedEntryPoints", { entryPoints: ["/worker.js", "/index.js"] }], - ["/HashedEntry", { entryNaming: "[name].entry-[hash].[ext]" }], - ["/Browser", { target: "browser" }], - ["/Minify", { minifyIdentifiers: true, minifySyntax: true, minifyWhitespace: true }], - ["/MinChunkSize", { minChunkSize: 1024 * 1024 }], - ] as const) { - itBundled("splitting/SharedFilesInTwoOrders" + name, { - files: evaluationOrderFiles, - entryPoints: ["/index.js", "/worker.js"], - splitting: true, - target: "bun", - outdir: "/out", - format: "esm", - ...options, - onAfterBundle(api) { - launchHashedEntry(api, "index"); - launchHashedEntry(api, "worker"); - }, - run: [ - { file: "/out/index.js", stdout: "ok aqP" }, - { file: "/out/worker.js", stdout: "worker q-noP aq-noP" }, - ], - }); - } - - // Whichever of x.js and y.js the program imports first loads p.js and q.js. - itBundled("splitting/SharedFilesInTwoOrdersOfImportCalls", { - files: { - "/entry.js": /* js */ ` - const load = { x: () => import("./x.js"), y: () => import("./y.js") }; - const [first, second] = process.argv.slice(2); - load[first]().then(() => load[second]()); - `, - "/x.js": `import "./p.js"; import "./q.js"; console.log("x");`, - "/y.js": `import "./q.js"; import "./p.js"; console.log("y");`, - "/p.js": `console.log("p");`, - "/q.js": `console.log("q");`, - }, - entryPoints: ["/entry.js"], - splitting: true, - outdir: "/out", - format: "esm", - run: [ - { file: "/out/entry.js", args: ["x", "y"], stdout: "p\nq\nx\ny" }, - { file: "/out/entry.js", args: ["y", "x"], stdout: "q\np\ny\nx" }, - ], - }); - - // one.js and two.js run back to back in both programs, so they still share a chunk. - itBundled("splitting/SharedFilesInTwoOrdersKeepRunsTogether", { - files: { - "/e1.js": `import "./one.js"; import "./two.js"; import "./three.js"; console.log("e1");`, - "/e2.js": `import "./three.js"; import "./one.js"; import "./two.js"; console.log("e2");`, - "/one.js": `console.log("one");`, - "/two.js": `console.log("two");`, - "/three.js": `console.log("three");`, - }, - entryPoints: ["/e1.js", "/e2.js"], - splitting: true, - outdir: "/out", - format: "esm", - onAfterBundle(api) { - expect(chunkWith(api, `"one"`)).toEqual(chunkWith(api, `"two"`)); - expect(chunkWith(api, `"one"`)).not.toEqual(chunkWith(api, `"three"`)); - expect(jsFilesIn(api)).toHaveLength(4); - }, - run: [ - { file: "/out/e1.js", stdout: "one\ntwo\nthree\ne1" }, - { file: "/out/e2.js", stdout: "three\none\ntwo\ne2" }, - ], - }); - - // Both evaluate p, q: one chunk. - itBundled("splitting/SharedFilesInOneOrder", { - files: { - ...evaluationOrderFiles, - "/worker.js": /* js */ ` - import "./p.js"; - import { q } from "./q.js"; - import { a } from "./a.js"; - console.log("worker", q, a); - `, - }, - entryPoints: ["/index.js", "/worker.js"], - splitting: true, - target: "bun", - outdir: "/out", - format: "esm", - onAfterBundle(api) { - expect(chunkWith(api, "globalThis.P =")).toEqual(chunkWith(api, "globalThis.P?.v")); - }, - run: [ - { file: "/out/index.js", stdout: "ok aqP" }, - { file: "/out/worker.js", stdout: "worker qP aqP" }, - ], - }); - - // The two orders differ only in files that declare. Nothing can tell: one chunk. - itBundled("splitting/SharedFilesInTwoOrdersDeclarationsOnly", { - files: { - "/e1.js": `import { f } from "./f.js"; import { g } from "./g.js"; console.log("e1", f(), g());`, - "/e2.js": `import { g } from "./g.js"; import { f } from "./f.js"; console.log("e2", f(), g());`, - "/f.js": `export function f() { return "f"; }`, - "/g.js": `export function g() { return "g"; }`, - }, - entryPoints: ["/e1.js", "/e2.js"], - splitting: true, - outdir: "/out", - format: "esm", - onAfterBundle(api) { - expect(jsFilesIn(api)).toHaveLength(3); - }, - run: [ - { file: "/out/e1.js", stdout: "e1 f g" }, - { file: "/out/e2.js", stdout: "e2 f g" }, - ], - }); - - // a.js enters the cycle at root.js, so member.js runs first. The chunk of member.js would import the chunk of - // root.js for cfg, and run it first. The files stay in one chunk. - itBundled("splitting/SharedFilesInTwoOrdersImportCycle", { - files: { - "/a.js": `import "./root.js"; import "./user.js"; console.log("a");`, - "/b.js": `import "./user.js"; import "./root.js"; console.log("b");`, - "/root.js": `import { helper } from "./member.js"; export const cfg = { ok: 1 }; console.log("root");`, - "/member.js": `import { cfg } from "./root.js"; export function helper() { return cfg.ok; } console.log("member");`, - "/user.js": `import { helper } from "./member.js"; console.log("user", helper());`, - }, - entryPoints: ["/a.js", "/b.js"], - splitting: true, - outdir: "/out", - format: "esm", - onAfterBundle(api) { - expect(jsFilesIn(api)).toHaveLength(3); - }, - run: { file: "/out/a.js", stdout: "member\nroot\nuser 1\na" }, - }); - - // m0.js and m1.js import each other, so they are in the chunk of five.js and six.js, with the runtime. They are - // files of that chunk like the others: a cycle, so the chunk stays whole. - itBundled("splitting/SharedFilesInTwoOrdersEntryPointsImportEachOther", { - files: { - "/m0.js": `import * as five from "./five.js"; import * as one from "./m1.js"; console.log("m0", Object.keys(five), Object.keys(one));`, - "/m1.js": `import * as six from "./six.js"; import "./m0.js"; export const v1 = 1; console.log("m1", Object.keys(six));`, - "/five.js": `console.log("five"); export const v5 = 5;`, - "/six.js": `console.log("six"); export const v6 = 6;`, - }, - entryPoints: ["/m0.js", "/m1.js"], - splitting: true, - outdir: "/out", - format: "esm", - run: { file: "/out/m0.js", stdout: `five\nsix\nm1 [ "v6" ]\nm0 [ "v5" ] [ "v1" ]` }, - }); - - // No two entry points disagree here, so the chunk of main.js imports its chunks as before: the one with main.js - // first. four.js requires its way back to main.js at load, and main.js has to be the file that is entered first. - itBundled("splitting/SharedFilesInOneOrderKeepImportsOfEntryPointChunk", { - files: { - "/main.js": `import { six } from "./six.js"; import "./three.js"; export const main = "main"; console.log("main", six);`, - "/other.js": `export const later = () => require("./three.js");`, - "/three.js": `import { four } from "./four.js"; export { four };`, - "/four.js": `import { six } from "./six.js"; const seven = require("./seven.js"); export const four = typeof seven; console.log("four", six);`, - "/six.js": `export const six = ["six"].join();`, - "/seven.js": `export * as main from "./main.js";`, - }, - entryPoints: ["/main.js", "/other.js"], - splitting: true, - target: "bun", - outdir: "/out", - format: "esm", - run: { file: "/out/main.js", stdout: "four six\nmain six" }, - }); - - // m2.js imports m0.js, so m0.js is in a chunk of its own, and the chunk of the entry point only imports chunks. - // It imports them in the order of m0.js: four.js and six.js come before the chunk of m0.js. - itBundled("splitting/SharedFilesInTwoOrdersEntryPointInAnotherChunk", { - files: { - "/m0.js": `import "./four.js"; import "./six.js"; export const v0 = "v0"; console.log("m0");`, - "/m1.js": `import "./six.js"; import "./four.js"; console.log("m1");`, - "/m2.js": `import * as m0 from "./m0.js"; console.log("m2", Object.keys(m0));`, - "/four.js": `console.log("four");`, - "/six.js": `console.log("six");`, - }, - entryPoints: ["/m0.js", "/m1.js", "/m2.js"], - splitting: true, - outdir: "/out", - format: "esm", - run: [ - { file: "/out/m0.js", stdout: "four\nsix\nm0" }, - { file: "/out/m1.js", stdout: "six\nfour\nm1" }, - { file: "/out/m2.js", stdout: `four\nsix\nm0\nm2 [ "v0" ]` }, - ], - }); - - // five.js imports seven.js and uses nothing from it, so a chunk of five.js would not import a chunk of seven.js, - // and would not wait for its await. The files stay in one chunk. - itBundled("splitting/SharedFilesInTwoOrdersTopLevelAwait", { - files: { - "/a.js": `import "./six.js"; import "./five.js"; console.log("a");`, - "/b.js": `import "./five.js"; import "./six.js"; console.log("b");`, - "/five.js": `import "./seven.js"; console.log("five");`, - "/six.js": `console.log("six");`, - "/seven.js": `await null; console.log("seven");`, - }, - entryPoints: ["/a.js", "/b.js"], - splitting: true, - outdir: "/out", - format: "esm", - onAfterBundle(api) { - expect(jsFilesIn(api)).toHaveLength(3); - }, - run: { file: "/out/a.js", stdout: "six\nseven\nfive\na" }, - }); - - // m1.js runs five.cjs where it imports it, ahead of six.js. A chunk of m1.js would import a chunk of six.js - // ahead of all its code. The files stay in one chunk. - itBundled("splitting/SharedFilesInTwoOrdersImportOfWrappedFile", { - files: { - "/m0.js": `import "./four.js"; import "./m1.js"; console.log("m0");`, - "/m1.js": `import five from "./five.cjs"; import "./six.js"; import "./four.js"; console.log("m1", five);`, - "/four.js": `console.log("four");`, - "/five.cjs": `console.log("five"); module.exports = 5;`, - "/six.js": `console.log("six");`, - }, - entryPoints: ["/m0.js", "/m1.js"], - splitting: true, - outdir: "/out", - format: "esm", - run: { file: "/out/m0.js", stdout: "four\nfive\nsix\nm1 5\nm0" }, - }); - - // f.js requires d.js at load, which runs t2.js and t1.js ahead of the imports of a.js. The imports of a chunk - // are not ranked by that, so the files stay in one chunk. - itBundled("splitting/SharedFilesInTwoOrdersRequireAtLoad", { - files: { - "/a.js": `import "./f.js"; import "./t1.js"; import "./t2.js"; console.log("a");`, - "/b.js": `import "./t1.js"; import "./t2.js"; console.log("b");`, - "/f.js": `const { d } = require("./d.js"); export const f = d;`, - "/d.js": `import "./t2.js"; import "./t1.js"; export const d = 1;`, - "/t1.js": `console.log("t1", globalThis.P);`, - "/t2.js": `globalThis.P = "P"; console.log("t2");`, - }, - entryPoints: ["/a.js", "/b.js"], - splitting: true, - target: "bun", - outdir: "/out", - format: "esm", - run: { file: "/out/a.js", stdout: "t2\nt1 P\na" }, - }); - - // lazy.js loads after x.js or y.js, so q.js folds into the chunk of p.js. The rule applies to the chunk - // that the fold leaves. - itBundled("splitting/SharedFilesInTwoOrdersAfterFold", { - files: { - "/x.js": `import "./p.js"; import "./q.js"; console.log("x"); export const later = () => import("./lazy.js");`, - "/y.js": `import "./q.js"; import "./p.js"; console.log("y"); export const later = () => import("./lazy.js");`, - "/lazy.js": `import "./q.js"; console.log("lazy");`, - "/p.js": `console.log("p");`, - "/q.js": `console.log("q");`, - }, - entryPoints: ["/x.js", "/y.js"], - splitting: true, - outdir: "/out", - format: "esm", - run: [ - { file: "/out/x.js", stdout: "p\nq\nx" }, - { file: "/out/y.js", stdout: "q\np\ny" }, - ], - }); - - // u.js folds into the chunk of the other files, and e1.js does not import it. It uses t.js, which is split off, so - // it does not stay with k.js, which t.js and r.js use: the two chunks would import each other. - itBundled("splitting/SharedFilesInTwoOrdersFoldedFile", { - files: { - "/e1.js": `import "./r.js"; import "./t.js";`, - "/e2.js": `import "./t.js"; import { u } from "./u.js"; import "./r.js"; console.log(u().v);`, - "/e3.js": `import "./t.js"; import { u } from "./u.js"; import "./r.js"; console.log(u().v);`, - "/t.js": `import { k } from "./k.js"; console.log("t", k?.v); export const t = { v: 1 };`, - "/k.js": `export const k = { v: 1, pad: "${Buffer.alloc(20000, "x").toString()}" };`, - "/r.js": `import { k } from "./k.js"; console.log("r", k?.v);`, - "/u.js": `import { t } from "./t.js"; export const u = () => t;`, - }, - entryPoints: ["/e1.js", "/e2.js", "/e3.js"], - splitting: true, - minChunkSize: 1024, - outdir: "/out", - format: "esm", - run: [ - { file: "/out/e1.js", stdout: "r 1\nt 1" }, - { file: "/out/e2.js", stdout: "t 1\nr 1\n1" }, - ], - }); - - // main.js has run p.js and q.js by the time page.js loads, so the order of page.js does not count: one chunk. - itBundled("splitting/SharedFilesInTwoOrdersAlreadyLoaded", { - files: { - "/main.js": `import "./p.js"; import "./q.js"; import("./page.js");`, - "/page.js": `import "./q.js"; import "./p.js"; console.log("page");`, - "/p.js": `console.log("p");`, - "/q.js": `console.log("q");`, - }, - entryPoints: ["/main.js"], - splitting: true, - outdir: "/out", - format: "esm", - onAfterBundle(api) { - expect(chunkWith(api, `"p"`)).toEqual(chunkWith(api, `"q"`)); - }, - run: { file: "/out/main.js", stdout: "p\nq\npage" }, - }); - // The namespace object of barrel.js names a before b, but b.js runs first: the shared chunk follows late_b.js. itBundled("splitting/SharedChunkOwnerIgnoresNamespaceExportOrder", { files: {