Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 6 additions & 0 deletions src/ast/import_record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ bitflags::bitflags! {

const WAS_ORIGINALLY_REQUIRE = 1 << 9;

/// Code splitting repointed this `import()` from the file it named at
/// the JavaScript chunk built for that file. The options object written
/// on the `import()` described the original file, so the printer
/// leaves it out.
const POINTS_TO_CHUNK = 1 << 10;

/// If true, this import can be removed if it's unused
const IS_EXTERNAL_WITHOUT_SIDE_EFFECTS = 1 << 11;

Expand Down
9 changes: 7 additions & 2 deletions src/bundler/linker_context/computeCrossChunkDependencies.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::bun_renamer as renamer;
use crate::mal_prelude::*;
use bun_alloc::ArenaVecExt as _;
use bun_ast::ImportRecordFlags;
use bun_collections::{ArrayHashMap, VecExt};

use crate::LinkerContext;
Expand Down Expand Up @@ -117,8 +118,8 @@ struct CrossChunkDependencies<'a, 'bump> {
impl<'a, 'bump> CrossChunkDependencies<'a, 'bump> {
// Called once per chunk from the sequential loop above. Writes:
// `self.chunk_meta[chunk_index]` (per-chunk disjoint),
// `self.import_records[source_index][rec].{path,source_index}` (per-chunk
// disjoint via `chunk.files_with_parts_in_chunk`),
// `self.import_records[source_index][rec].{path,source_index,loader,flags}`
// (per-chunk disjoint via `chunk.files_with_parts_in_chunk`),
Comment thread
robobun marked this conversation as resolved.
// `symbols.assign_chunk_index(ref)` (Relaxed atomic store to
// `Symbol.chunk_index: AtomicU32`; per-symbol-ref disjoint by chunk
// membership — debug-asserted in `assign_chunk_index`).
Expand Down Expand Up @@ -176,6 +177,10 @@ impl<'a, 'bump> CrossChunkDependencies<'a, 'bump> {
// which outlives the link pass.
import_record.path.text = _chunks[other_chunk_index as usize].unique_key;
import_record.source_index = Index::INVALID;
import_record.loader = None;
import_record
.flags
.insert(ImportRecordFlags::POINTS_TO_CHUNK);

// Track this cross-chunk dynamic import so we make sure to
// include its hash when we're calculating the hashes of all
Expand Down
4 changes: 3 additions & 1 deletion src/js_printer/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2711,7 +2711,9 @@ pub(crate) mod __gated_printer {
self.print_string_literal_utf8(path.pretty, false);
}

if !import_options.is_missing() {
if !import_options.is_missing()
&& !record.flags.contains(ImportRecordFlags::POINTS_TO_CHUNK)
{
self.print_whitespacer(ws!(b", "));
self.print_expr(import_options, Level::Comma, ExprFlagSet::empty());
}
Expand Down
95 changes: 95 additions & 0 deletions test/bundler/bundler_splitting.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,101 @@ describe("bundler", () => {
},
});

// A dynamic import the linker points at a chunk must not keep the import
// attributes the user wrote for the original file: the chunk is JavaScript,
// and the runtime would otherwise try to load it with the attribute's loader.
itBundled("splitting/DynamicImportWithAttributeToChunk", {
files: {
"/entry.ts": `
const { default: data } = await import("./data.json", { with: { type: "json" } });
console.log(data.answer);
`,
"/data.json": `{ "answer": 42 }`,
},
splitting: true,
outdir: "/out",
target: "bun",
onAfterBundle(api) {
expect(api.readFile("/out/entry.js")).toMatch(/import\("\.\/data-[a-z0-9]+\.js"\)/);
},
run: {
file: "/out/entry.js",
stdout: "42",
},
});

itBundled("splitting/DynamicImportAttributesToChunkAllLoaders", {
files: {
"/entry.ts": `
const json = await import("./data.json", { assert: { type: "json" } });
const text = await import("./note.txt", { with: { type: "text" } });
const toml = await import("./config.toml", { with: { type: "toml" } });
console.log(json.default.answer, JSON.stringify(text.default), toml.default.name);
`,
"/data.json": `{ "answer": 42 }`,
"/note.txt": `hello`,
"/config.toml": `name = "from toml"`,
},
splitting: true,
outdir: "/out",
onAfterBundle(api) {
const entry = api.readFile("/out/entry.js");
expect(entry).toMatch(/import\("\.\/data-[a-z0-9]+\.js"\)/);
expect(entry).toMatch(/import\("\.\/note-[a-z0-9]+\.js"\)/);
expect(entry).toMatch(/import\("\.\/config-[a-z0-9]+\.js"\)/);
expect(entry).not.toContain("type:");
},
run: {
file: "/out/entry.js",
stdout: '42 "hello" from toml',
},
});

itBundled("splitting/DynamicImportAttributesToChunkMinified", {
files: {
"/entry.ts": `
const { default: data } = await import("./data.json", { with: { type: "json" } });
console.log(data.answer);
`,
"/data.json": `{ "answer": 42 }`,
},
splitting: true,
outdir: "/out",
minifyWhitespace: true,
minifySyntax: true,
onAfterBundle(api) {
expect(api.readFile("/out/entry.js")).toMatch(/import\("\.\/data-[a-z0-9]+\.js"\)/);
},
run: {
file: "/out/entry.js",
stdout: "42",
},
});

// The attributes still belong on an import() that stays external: it loads
// the file the user named, not a chunk.
itBundled("splitting/ExternalDynamicImportKeepsAttributes", {
files: {
"/entry.ts": `
const { default: data } = await import("./data.json", { with: { type: "json" } });
console.log(data.answer);
`,
},
external: ["*.json"],
splitting: true,
outdir: "/out",
runtimeFiles: {
"/out/data.json": `{ "answer": 42 }`,
},
onAfterBundle(api) {
api.expectFile("/out/entry.js").toContain('import("./data.json", { with: { type: "json" } })');
},
run: {
file: "/out/entry.js",
stdout: "42",
},
});

// N same-named cross-chunk exports must get unique aliases in O(N) total
// (ExportRenamer::next_renamed_name). Debug/ASAN builds blow past the 15s
// cap with far fewer files than release, hence the scaled N.
Expand Down