Skip to content
14 changes: 8 additions & 6 deletions src/bundler/LinkerContext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2650,12 +2650,14 @@ impl<'a> LinkerContext<'a> {
}
}

// CSS files only follow their import records.
if ctx.css_reprs[source_index as usize].is_some() {
continue;
}

for part in ctx.parts[source_index as usize].as_slice() {
// Of the JS parts a stylesheet also carries, only the live ones get printed.
let live_parts_only = ctx.css_reprs[source_index as usize].is_some();
let parts_live = &self.graph.parts_live[source_index as usize];
let parts = ctx.parts[source_index as usize].as_slice();
for (part_index, part) in parts.iter().enumerate() {
if live_parts_only && !parts_live.is_set(part_index) {
continue;
}
for dependency in part.dependencies.iter() {
let dep = dependency.source_index.get();
if dep != source_index
Expand Down
139 changes: 76 additions & 63 deletions src/bundler/linker_context/computeCrossChunkDependencies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ pub(crate) fn compute_cross_chunk_dependencies(
chunks: bun_ptr::BackRef::new(&*chunks),
chunk_meta: &mut chunk_metas,
parts: ast.parts,
css_asts: ast.css,
import_records: ast.import_records,
flags: meta.flags,
entry_point_chunk_indices: files.entry_point_chunk_index,
Expand Down Expand Up @@ -91,6 +92,7 @@ struct CrossChunkDependencies<'a, 'bump> {
// (caller stack frame).
chunks: bun_ptr::BackRef<[Chunk]>,
parts: &'a [bun_ast::PartList<'bump>],
css_asts: &'a [crate::bundled_ast::CssCol],
import_records: &'a mut [bun_ast::import_record::List<'bump>],
flags: &'a [js_meta::Flags],
entry_point_chunk_indices: &'a [IndexInt],
Expand Down Expand Up @@ -139,15 +141,27 @@ impl<'a, 'bump> CrossChunkDependencies<'a, 'bump> {
// Go through `chunk_meta.imports` / `chunk_meta.dynamic_imports`.
let entry_point_chunk_indices = deps.entry_point_chunk_indices;

let chunk::Content::Javascript(js) = &chunk.content else {
return;
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

// CSS files get no chunk; their live JS parts are copied into each chunk that imports them.
let css_asts = deps.css_asts;
let copied_css_files = js
.files_in_chunk_order
.iter()
.copied()
.filter(|&source_index| css_asts[source_index as usize].is_some());

// Go over each file in this chunk
for &source_index in chunk.files_with_parts_in_chunk.keys() {
// TODO: make this switch
if matches!(chunk.content, chunk::Content::Css(_)) {
continue;
}
if !matches!(chunk.content, chunk::Content::Javascript(_)) {
continue;
}
for source_index in chunk
.files_with_parts_in_chunk
.keys()
.iter()
.copied()
.chain(copied_css_files)
{
let is_css_copy = css_asts[source_index as usize].is_some();

// Go over each part in this file that's marked for inclusion in this chunk
let parts = deps.parts[source_index as usize].as_slice();
Expand Down Expand Up @@ -191,7 +205,9 @@ impl<'a, 'bump> CrossChunkDependencies<'a, 'bump> {
// the same name should already be marked as all being in a single
// chunk. In that case this will overwrite the same value below which
// is fine.
symbols.assign_chunk_index(&part.declared_symbols, chunk_index as u32);
if !is_css_copy {
symbols.assign_chunk_index(&part.declared_symbols, chunk_index as u32);
}

let used_refs = part.symbol_uses.keys();

Expand Down Expand Up @@ -255,66 +271,63 @@ impl<'a, 'bump> CrossChunkDependencies<'a, 'bump> {
}

// Include the exports if this is an entry point chunk
if matches!(chunk.content, chunk::Content::Javascript(_)) {
if chunk.entry_point.is_entry_point() {
let flags = deps.flags[chunk.entry_point.source_index() as usize];
if flags.wrap != WrapKind::Cjs {
let resolved_exports =
&deps.resolved_exports[chunk.entry_point.source_index() as usize];
let sorted_and_filtered_export_aliases = &deps
.sorted_and_filtered_export_aliases
[chunk.entry_point.source_index() as usize];
for alias in sorted_and_filtered_export_aliases.iter() {
let export_ = resolved_exports.get(alias).unwrap();
let mut target_ref = export_.data.import_ref;

// If this is an import, then target what the import points to
if let Some(import_data) = deps.imports_to_bind
[export_.data.source_index.get() as usize]
.get(&target_ref)
{
target_ref = import_data.data.import_ref;
}

// If this is an ES6 import from a CommonJS file, it will become a
// property access off the namespace symbol instead of a bare
// identifier. In that case we want to pull in the namespace symbol
// instead. The namespace symbol stores the result of "require()".
if let Some(namespace_alias) =
&symbols.get_const(target_ref).unwrap().namespace_alias
{
target_ref = namespace_alias.namespace_ref;
}
if chunk.entry_point.is_entry_point() {
let flags = deps.flags[chunk.entry_point.source_index() as usize];
if flags.wrap != WrapKind::Cjs {
let resolved_exports =
&deps.resolved_exports[chunk.entry_point.source_index() as usize];
let sorted_and_filtered_export_aliases = &deps.sorted_and_filtered_export_aliases
[chunk.entry_point.source_index() as usize];
for alias in sorted_and_filtered_export_aliases.iter() {
let export_ = resolved_exports.get(alias).unwrap();
let mut target_ref = export_.data.import_ref;

// If this is an import, then target what the import points to
if let Some(import_data) = deps.imports_to_bind
[export_.data.source_index.get() as usize]
.get(&target_ref)
{
target_ref = import_data.data.import_ref;
}

if cfg!(debug_assertions) {
// SAFETY: arena slice valid for the link pass.
let name = symbols.get_const(target_ref).unwrap().original_name.slice();
debug!("Cross-chunk export: {}", bstr::BStr::new(name),);
}
// If this is an ES6 import from a CommonJS file, it will become a
// property access off the namespace symbol instead of a bare
// identifier. In that case we want to pull in the namespace symbol
// instead. The namespace symbol stores the result of "require()".
Comment thread
robobun marked this conversation as resolved.
Resolved
if let Some(namespace_alias) =
&symbols.get_const(target_ref).unwrap().namespace_alias
{
target_ref = namespace_alias.namespace_ref;
}

let _ = chunk_meta.imports.put(target_ref, ()); // OOM-only Result
if cfg!(debug_assertions) {
// SAFETY: arena slice valid for the link pass.
let name = symbols.get_const(target_ref).unwrap().original_name.slice();
debug!("Cross-chunk export: {}", bstr::BStr::new(name),);
}
}

// Ensure "exports" is included if the current output format needs it
// https://github.com/evanw/esbuild/blob/v0.27.2/internal/linker/linker.go#L1049-L1051
if flags.force_include_exports_for_entry_point {
// result intentionally discarded
let _ = chunk_meta.imports.put(
deps.exports_refs[chunk.entry_point.source_index() as usize],
(),
);
let _ = chunk_meta.imports.put(target_ref, ()); // OOM-only Result
}
}

// Include the wrapper if present
// https://github.com/evanw/esbuild/blob/v0.27.2/internal/linker/linker.go#L1053-L1056
if flags.wrap != WrapKind::None {
// result intentionally discarded
let _ = chunk_meta.imports.put(
deps.wrapper_refs[chunk.entry_point.source_index() as usize],
(),
);
}
// Ensure "exports" is included if the current output format needs it
// https://github.com/evanw/esbuild/blob/v0.27.2/internal/linker/linker.go#L1049-L1051
Comment thread
robobun marked this conversation as resolved.
Resolved
if flags.force_include_exports_for_entry_point {
// result intentionally discarded
let _ = chunk_meta.imports.put(
deps.exports_refs[chunk.entry_point.source_index() as usize],
(),
);
}

// Include the wrapper if present
// https://github.com/evanw/esbuild/blob/v0.27.2/internal/linker/linker.go#L1053-L1056
Comment thread
robobun marked this conversation as resolved.
Resolved
if flags.wrap != WrapKind::None {
// result intentionally discarded
let _ = chunk_meta.imports.put(
deps.wrapper_refs[chunk.entry_point.source_index() as usize],
(),
);
}
}
}
Expand Down
105 changes: 105 additions & 0 deletions test/bundler/bundler_loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,111 @@ describe("bundler", async () => {
run: { stdout: "[true,true,true]" },
});

// The namespace object of a CSS file is built with the runtime's `__export`
// helper. No other module in this bundle uses the runtime, so the CSS file's
// JS side is the only thing pulling it into the output.
for (const target of ["bun", "browser"] as const) {
itBundled(`bun/loader-css-namespace-import-${target}`, {
target,
outdir: "/out",
files: {
"/entry.ts": /* js */ `
import * as plain from "./plain.css";
import * as styles from "./styles.module.css";
console.log(
JSON.stringify([
Object.keys(plain),
plain.default,
Object.keys(styles).sort(),
styles.foo === styles.default.foo && typeof styles.foo === "string",
]),
);
`,
"/plain.css": `.plain { color: red; }`,
"/styles.module.css": `.foo { color: blue; }`,
},
run: { stdout: '[["default"],{},["default","foo"],true]' },
});
}

// Every CSS file has a namespace export part that uses `__export`, but it is
// only printed when something namespace-imports the file. A stylesheet that
// is merely imported must not pull the (here live, because of util.js)
// runtime into its entry point.
itBundled("bun/loader-css-plain-import-does-not-pull-in-runtime", {
outdir: "/out",
entryPoints: ["/a.js", "/b.js"],
files: {
"/a.js": /* js */ `
import * as util from "./util.js";
console.log(JSON.stringify(Object.keys(util)));
`,
"/util.js": `export const util = 1;`,
"/b.js": /* js */ `
import "./side-effect.css";
import styles from "./styles.module.css";
console.log(JSON.stringify(Object.keys(styles)));
`,
"/side-effect.css": `.side { color: red; }`,
"/styles.module.css": `.foo { color: blue; }`,
},
onAfterBundle(api) {
expect(api.readFile("/out/a.js")).toContain("__export(");
api.expectFile("/out/b.js").toMatchInlineSnapshot(`
"// styles.module.css
var styles_module_default = {
foo: "foo_-MSaAA"
};

// b.js
console.log(JSON.stringify(Object.keys(styles_module_default)));
"
`);
},
run: [
{ file: "/out/a.js", stdout: '["util"]' },
{ file: "/out/b.js", stdout: '["foo"]' },
],
});

// Which entry points print a stylesheet's namespace object is decided by the
// entry bits of the stylesheet, however it was reached: here b.js reaches
// x.css through its own stylesheet's @import, and the import record of a
// tree-shaken package is what makes the printer visit x.css, so b.js prints
// the namespace object that a.js made live and needs the runtime as well.
itBundled("bun/loader-css-namespace-object-printed-for-another-entry", {
outdir: "/out",
entryPoints: ["/a.js", "/b.js"],
files: {
"/a.js": /* js */ `
import * as ns from "./x.css";
console.log("a", JSON.stringify(Object.keys(ns)));
`,
"/b.js": /* js */ `
import { unused } from "pkg";
import "./b.css";
console.log("b");
`,
"/x.css": `.x { color: red; }`,
"/b.css": /* css */ `
@import "./x.css";
.b { color: blue; }
`,
"/node_modules/pkg/package.json": `{ "name": "pkg", "main": "index.js", "sideEffects": false }`,
"/node_modules/pkg/index.js": /* js */ `
import "../../x.css";
export const unused = 1;
`,
},
onAfterBundle(api) {
expect(api.readFile("/out/b.js")).toContain("__export(exports_x,");
},
run: [
{ file: "/out/a.js", stdout: 'a ["default"]' },
{ file: "/out/b.js", stdout: "b" },
],
});

itBundled("bun/wasm-is-copied-to-outdir", {
target: "bun",
outdir: "/out",
Expand Down
Loading
Loading