Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
14 changes: 8 additions & 6 deletions docs/bundler/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1050,15 +1050,15 @@ With `.` as `root`, the generated file structure looks like this:

### publicPath

A prefix added to any import paths in bundled code.
A prefix added to the file paths the bundler emits into your code.

In many cases, generated bundles contain no import statements; the goal of bundling is to combine all of the code into a single file. In a few cases, though, the generated bundles contain import statements:
In many cases, generated bundles contain no references to other files; the goal of bundling is to combine all of the code into a single file. In a few cases, though, the bundler writes paths to other output files into the bundle:

- **Asset imports** — When importing an unrecognized file type like `*.svg`, the bundler defers to the file loader, which copies the file into `outdir` as is. The import is converted into a variable.
- **External modules** — Files and modules marked as external are not included in the bundle. Instead, the import statement is left in the final bundle.
- **Chunking.** When `splitting` is enabled, the bundler may generate separate "chunk" files that represent code that is shared among multiple entrypoints.
- **Asset imports** — When importing an unrecognized file type like `*.svg`, the bundler defers to the file loader, which copies the file into `outdir` as is. The import is converted into a variable holding the file's path.
- **Chunking** — When `splitting` is enabled, the bundler may generate separate "chunk" files that represent code shared among multiple entrypoints, and emits `import` statements that reference those chunks.
- **Linked source maps** — With `sourcemap: "linked"`, the bundler emits a `//# sourceMappingURL=` comment pointing at the generated `.map` file.

In any of these cases, the final bundles may contain paths to other files. By default these imports are relative. Here is an example of an asset import:
By default these paths are relative. Here is an example of an asset import:

<CodeGroup>

Expand Down Expand Up @@ -1099,6 +1099,8 @@ The output file would now look something like this.
var logo = "https://cdn.example.com/logo-a7305bdef.svg";
```

`publicPath` does not rewrite specifiers for [external](#external) modules. An import marked as external is emitted unchanged so it can be resolved at runtime. To rewrite an external specifier to a CDN URL, use an [`onResolve`](/bundler/plugins#onresolve) plugin that returns `{ path, external: true }`.
Comment thread
robobun marked this conversation as resolved.
Outdated

### define

A map of global identifiers to be replaced at build time. Keys of this object are identifier names, and values are JSON strings that are inlined.
Expand Down
59 changes: 54 additions & 5 deletions src/bundler/bundle_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,14 @@
/// `jsc::api::JSBundler::FileMap` — re-exported from the canonical def below.
pub use api::JSBundler::FileMap;

#[derive(Clone, Copy)]
#[derive(Clone)]
pub struct PendingImport {
pub to_source_index: Index,
pub import_record_index: u32,
/// Set when an onResolve plugin returned `{ path, external: true }` before
/// the importer's ast was installed. Applied to `import_record.path` in
/// `patch_import_record_source_indices`; `to_source_index` is unused.
Comment thread
robobun marked this conversation as resolved.
Outdated
pub external_path: Option<Box<[u8]>>,
}

pub struct BundleV2<'a> {
Expand Down Expand Up @@ -4712,7 +4716,44 @@
}
} else {
drop(result.namespace);
drop(result.path);
// Plugin returned `{ path, external: true }`. Keep the
// import external but rewrite its printed specifier to the
// plugin-provided path (esbuild does the same).
Comment thread
robobun marked this conversation as resolved.
Outdated
if resolve.import_record.kind != ImportKind::EntryPointBuild
&& !strings::eql(&result.path, &resolve.import_record.specifier)
{
let source_import_records =
&mut this.graph.ast.items_import_records_mut()
[resolve.import_record.importer_source_index as usize];
if (source_import_records.len() as u32)
> resolve.import_record.import_record_index
{
// SAFETY: `result.path` is moved into `free_list`
// below and thus outlives `BundleV2`; erase to
// `'static` so the import record can borrow it.
let result_path_static: &'static [u8] =
unsafe { &*std::ptr::from_ref::<[u8]>(result.path.as_ref()) };
source_import_records.as_mut_slice()
[resolve.import_record.import_record_index as usize]
.path = bun_paths::fs::Path::init(result_path_static);
this.free_list.push(result.path);
} else {
let entry = this
.resolve_tasks_waiting_for_import_source_index
.get_or_put(resolve.import_record.importer_source_index)
.expect("oom");
if !entry.found_existing {
*entry.value_ptr = Vec::new();
}
let _ = entry.value_ptr.push(PendingImport {
to_source_index: Index::INVALID,
import_record_index: resolve.import_record.import_record_index,
external_path: Some(result.path),
});
}
} else {
drop(result.path);
}
}

if let Some(source_index) = out_source_index {
Expand Down Expand Up @@ -4744,6 +4785,7 @@
let _ = entry.value_ptr.push(PendingImport {
to_source_index: source_index,
import_record_index: resolve.import_record.import_record_index,
external_path: None,
});
} else {
let import_record: &mut ImportRecord = &mut source_import_records
Expand Down Expand Up @@ -6652,15 +6694,22 @@
let (_, value) = self
.resolve_tasks_waiting_for_import_source_index
.swap_remove_at(idx);
for to_assign in value.slice() {
if save_import_record_source_index
for to_assign in value {
if let Some(external_path) = to_assign.external_path {
// SAFETY: box moved into `free_list` (outlives BundleV2),
// so the `'static` erasure is valid for the import record.
let path_static: &'static [u8] =
unsafe { &*std::ptr::from_ref::<[u8]>(external_path.as_ref()) };
import_records.as_mut_slice()[to_assign.import_record_index as usize]
.path = bun_paths::fs::Path::init(path_static);
self.free_list.push(external_path);

Check warning on line 6705 in src/bundler/bundle_v2.rs

View check run for this annotation

Claude / Claude Code Review

Deferred external-path rewrite can be re-bundled by path_to_source_index_map lookup

The deferred `external_path` rewrite is applied to `record.path` (loop at 6697–6705) *before* the second loop at 6718 does `path_to_source_index_map.get_path(&record.path)` on the rewritten text — so if the plugin-returned path happens to match an already-bundled source's path, `record.source_index` gets set and the plugin-external import is silently bundled. The immediate branch at line 4736 doesn't have this problem (it writes into `graph.ast` after `patch_import_record_source_indices` has alr
Comment thread
claude[bot] marked this conversation as resolved.
Outdated
} else if save_import_record_source_index
|| input_file_loaders[to_assign.to_source_index.get() as usize].is_css()
{
import_records.as_mut_slice()[to_assign.import_record_index as usize]
.source_index = to_assign.to_source_index;
}
}
drop(value);
}

// Inlined `self.path_to_source_index_map(ctx.target)` (== `&mut self.graph.build_graphs[target]`)
Expand Down
78 changes: 78 additions & 0 deletions test/bundler/bundler_plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,84 @@ describe("bundler", () => {
},
};
});
// https://github.com/oven-sh/bun/issues/11652
itBundled("plugin/ResolveExternalRewritesPathESM", ({ root }) => {
return {
files: {
"index.ts": /* ts */ `
import React from "react";
import { createRoot } from "react-dom/client";
export { preact } from "preact";
export * from "mobx";
const lazy = await import("lodash");
console.log(React, createRoot, lazy);
`,
},
format: "esm",
plugins(builder) {
builder.onResolve({ filter: /^(react|react-dom\/client|preact|mobx|lodash)$/ }, args => {
return { path: "https://esm.sh/" + args.path, external: true };
});
},
onAfterBundle(api) {
const out = api.readFile("/out.js");
expect(out).toContain(`from "https://esm.sh/react"`);
expect(out).toContain(`from "https://esm.sh/react-dom/client"`);
expect(out).toContain(`from "https://esm.sh/preact"`);
expect(out).toContain(`from "https://esm.sh/mobx"`);
expect(out).toContain(`import("https://esm.sh/lodash")`);
expect(out).not.toContain(`"react"`);
expect(out).not.toContain(`"react-dom/client"`);
expect(out).not.toContain(`"preact"`);
expect(out).not.toContain(`"mobx"`);
expect(out).not.toContain(`"lodash"`);
},
};
});
itBundled("plugin/ResolveExternalRewritesPathCJS", ({ root }) => {
return {
files: {
"index.ts": /* ts */ `
const React = require("react");
console.log(React);
`,
},
format: "cjs",
plugins(builder) {
builder.onResolve({ filter: /^react$/ }, args => {
return { path: "https://esm.sh/react", external: true };
});
},
onAfterBundle(api) {
const out = api.readFile("/out.js");
expect(out).toContain(`require("https://esm.sh/react")`);
expect(out).not.toContain(`"react"`);
},
};
});
itBundled("plugin/ResolveExternalSamePathUnchanged", ({ root }) => {
let called = 0;
return {
files: {
"index.ts": /* ts */ `
import React from "react";
console.log(React);
`,
},
format: "esm",
plugins(builder) {
builder.onResolve({ filter: /^react$/ }, args => {
called++;
return { path: args.path, external: true };
});
},
onAfterBundle(api) {
expect(called).toBe(1);
const out = api.readFile("/out.js");
expect(out).toContain(`from "react"`);
},
};
});
itBundled("plugin/ResolveManySegfault", ({ root }) => {
let resolveCount = 0;
let loadCount = 0;
Expand Down
Loading