mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 23:18:47 +00:00
Compare commits
1 Commits
kai/mimall
...
claude/css
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70f4fe90c5 |
@@ -511,6 +511,10 @@ pub fn NewParser_(
|
||||
p.import_records.items[import_record_index].tag = tag;
|
||||
}
|
||||
|
||||
if (state.import_loader) |loader| {
|
||||
p.import_records.items[import_record_index].loader = loader;
|
||||
}
|
||||
|
||||
p.import_records.items[import_record_index].handles_import_errors = (state.is_await_target and p.fn_or_arrow_data_visit.try_body_count != 0) or state.is_then_catch_target;
|
||||
p.import_records_for_current_part.append(p.allocator, import_record_index) catch unreachable;
|
||||
|
||||
|
||||
@@ -32,6 +32,10 @@ is_scb_bitset: BitSet = .{},
|
||||
has_client_components: bool = false,
|
||||
has_server_components: bool = false,
|
||||
|
||||
/// Tracks which CSS files are imported with `{ type: 'css' }` (CSS Module Scripts)
|
||||
/// These files need to export a CSSStyleSheet instead of the normal CSS module exports
|
||||
css_module_script_files: BitSet = .{},
|
||||
|
||||
/// This is for cross-module inlining of detected inlinable constants
|
||||
// const_values: js_ast.Ast.ConstValuesMap = .{},
|
||||
/// This is for cross-module inlining of TypeScript enum constants
|
||||
@@ -41,6 +45,7 @@ pub fn init(allocator: std.mem.Allocator, file_count: usize) !LinkerGraph {
|
||||
return LinkerGraph{
|
||||
.allocator = allocator,
|
||||
.files_live = try BitSet.initEmpty(allocator, file_count),
|
||||
.css_module_script_files = try BitSet.initEmpty(allocator, file_count),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -237,6 +242,10 @@ pub fn load(
|
||||
this.allocator,
|
||||
sources.len,
|
||||
);
|
||||
this.css_module_script_files = try BitSet.initEmpty(
|
||||
this.allocator,
|
||||
sources.len,
|
||||
);
|
||||
this.files.len = sources.len;
|
||||
var files = this.files.slice();
|
||||
|
||||
|
||||
@@ -3423,9 +3423,21 @@ pub const BundleV2 = struct {
|
||||
}
|
||||
}
|
||||
|
||||
const import_record_loader = import_record.loader orelse path.loader(&transpiler.options.loaders) orelse .file;
|
||||
// Check if the import has an explicit loader from import attributes (e.g., { type: 'css' })
|
||||
// before falling back to the path-based loader
|
||||
const explicit_loader = import_record.loader;
|
||||
const import_record_loader = explicit_loader orelse path.loader(&transpiler.options.loaders) orelse .file;
|
||||
import_record.loader = import_record_loader;
|
||||
|
||||
// Check if this is a CSS Module Script import (import ... with { type: 'css' })
|
||||
// The import should have type: 'css' assertion AND target a CSS file
|
||||
// IMPORTANT: Only set this if the loader was EXPLICITLY set via import attributes,
|
||||
// not just because the file has a .css extension
|
||||
const path_loader = path.loader(&transpiler.options.loaders) orelse .file;
|
||||
if (explicit_loader != null and explicit_loader.? == .css and path_loader == .css) {
|
||||
import_record.is_css_module_script = true;
|
||||
}
|
||||
|
||||
const is_html_entrypoint = import_record_loader == .html and target.isServerSide() and this.transpiler.options.dev_server == null;
|
||||
|
||||
if (this.pathToSourceIndexMap(target).get(path.text)) |id| {
|
||||
|
||||
@@ -17,247 +17,290 @@ pub fn generateCodeForLazyExport(this: *LinkerContext, source_index: Index.Int)
|
||||
|
||||
const module_ref = this.graph.ast.items(.module_ref)[source_index];
|
||||
|
||||
// Handle css modules
|
||||
// Handle CSS Module Scripts (import ... with { type: 'css' })
|
||||
// If this CSS file is imported with type: 'css', export a CSSStyleSheet
|
||||
if (maybe_css_ast != null and this.graph.css_module_script_files.isSet(source_index)) {
|
||||
const source = &all_sources[source_index];
|
||||
const stmt: Stmt = part.stmts[0];
|
||||
if (stmt.data != .s_lazy_export) {
|
||||
@panic("Internal error: expected top-level lazy export statement");
|
||||
}
|
||||
|
||||
// Generate: __cssModuleScript("css content")
|
||||
const css_content_str = E.String.init(source.contents);
|
||||
const css_content_expr = Expr.init(E.String, css_content_str, stmt.loc);
|
||||
|
||||
// Get the __cssModuleScript runtime function ref
|
||||
const css_module_script_ref = this.graph.runtimeFunction("__cssModuleScript");
|
||||
|
||||
// Generate the call expression: __cssModuleScript("css content")
|
||||
const call_args = try this.allocator().alloc(Expr, 1);
|
||||
call_args[0] = css_content_expr;
|
||||
|
||||
const call_expr = Expr.init(E.Call, E.Call{
|
||||
.target = Expr.initIdentifier(css_module_script_ref, stmt.loc),
|
||||
.args = BabyList(Expr).fromOwnedSlice(call_args),
|
||||
}, stmt.loc);
|
||||
|
||||
// Replace the lazy export with the call expression
|
||||
part.stmts[0].data.s_lazy_export.* = call_expr.data;
|
||||
|
||||
// Mark that we need the __cssModuleScript runtime function
|
||||
try this.graph.generateRuntimeSymbolImportAndUse(
|
||||
source_index,
|
||||
Index.part(1),
|
||||
"__cssModuleScript",
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
// Handle css modules (class name exports)
|
||||
//
|
||||
// --- original comment from esbuild ---
|
||||
// If this JavaScript file is a stub from a CSS file, populate the exports of
|
||||
// this JavaScript stub with the local names from that CSS file. This is done
|
||||
// now instead of earlier because we need the whole bundle to be present.
|
||||
// Skip this for CSS Module Scripts which already have their export set above.
|
||||
const is_css_module_script_file = maybe_css_ast != null and this.graph.css_module_script_files.isSet(source_index);
|
||||
if (maybe_css_ast) |css_ast| {
|
||||
const stmt: Stmt = part.stmts[0];
|
||||
if (stmt.data != .s_lazy_export) {
|
||||
@panic("Internal error: expected top-level lazy export statement");
|
||||
}
|
||||
if (css_ast.local_scope.count() > 0) out: {
|
||||
var exports = E.Object{};
|
||||
if (is_css_module_script_file) {
|
||||
// Skip - CSS Module Scripts already have their export set above
|
||||
} else {
|
||||
const stmt: Stmt = part.stmts[0];
|
||||
if (stmt.data != .s_lazy_export) {
|
||||
@panic("Internal error: expected top-level lazy export statement");
|
||||
}
|
||||
if (css_ast.local_scope.count() > 0) out: {
|
||||
var exports = E.Object{};
|
||||
|
||||
const symbols: *const Symbol.List = &this.graph.ast.items(.symbols)[source_index];
|
||||
const all_import_records: []const BabyList(bun.css.ImportRecord) = this.graph.ast.items(.import_records);
|
||||
const symbols: *const Symbol.List = &this.graph.ast.items(.symbols)[source_index];
|
||||
const all_import_records: []const BabyList(bun.css.ImportRecord) = this.graph.ast.items(.import_records);
|
||||
|
||||
const values = css_ast.local_scope.values();
|
||||
if (values.len == 0) break :out;
|
||||
const size = size: {
|
||||
var size: u32 = 0;
|
||||
for (values) |entry| {
|
||||
size = @max(size, entry.ref.inner_index);
|
||||
}
|
||||
break :size size + 1;
|
||||
};
|
||||
const values = css_ast.local_scope.values();
|
||||
if (values.len == 0) break :out;
|
||||
const size = size: {
|
||||
var size: u32 = 0;
|
||||
for (values) |entry| {
|
||||
size = @max(size, entry.ref.inner_index);
|
||||
}
|
||||
break :size size + 1;
|
||||
};
|
||||
|
||||
var inner_visited = try BitSet.initEmpty(this.allocator(), size);
|
||||
defer inner_visited.deinit(this.allocator());
|
||||
var composes_visited = std.AutoArrayHashMap(bun.bundle_v2.Ref, void).init(this.allocator());
|
||||
defer composes_visited.deinit();
|
||||
var inner_visited = try BitSet.initEmpty(this.allocator(), size);
|
||||
defer inner_visited.deinit(this.allocator());
|
||||
var composes_visited = std.AutoArrayHashMap(bun.bundle_v2.Ref, void).init(this.allocator());
|
||||
defer composes_visited.deinit();
|
||||
|
||||
const Visitor = struct {
|
||||
inner_visited: *BitSet,
|
||||
composes_visited: *std.AutoArrayHashMap(bun.bundle_v2.Ref, void),
|
||||
parts: *std.array_list.Managed(E.TemplatePart),
|
||||
all_import_records: []const BabyList(bun.css.ImportRecord),
|
||||
all_css_asts: []?*bun.css.BundlerStyleSheet,
|
||||
all_sources: []const Logger.Source,
|
||||
all_symbols: []const Symbol.List,
|
||||
source_index: Index.Int,
|
||||
log: *Logger.Log,
|
||||
loc: Loc,
|
||||
allocator: std.mem.Allocator,
|
||||
const Visitor = struct {
|
||||
inner_visited: *BitSet,
|
||||
composes_visited: *std.AutoArrayHashMap(bun.bundle_v2.Ref, void),
|
||||
parts: *std.array_list.Managed(E.TemplatePart),
|
||||
all_import_records: []const BabyList(bun.css.ImportRecord),
|
||||
all_css_asts: []?*bun.css.BundlerStyleSheet,
|
||||
all_sources: []const Logger.Source,
|
||||
all_symbols: []const Symbol.List,
|
||||
source_index: Index.Int,
|
||||
log: *Logger.Log,
|
||||
loc: Loc,
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
fn clearAll(visitor: *@This()) void {
|
||||
visitor.inner_visited.setAll(false);
|
||||
visitor.composes_visited.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
fn visitName(visitor: *@This(), ast: *bun.css.BundlerStyleSheet, ref: bun.css.CssRef, idx: Index.Int) void {
|
||||
bun.assert(ref.canBeComposed());
|
||||
const from_this_file = ref.sourceIndex(idx) == visitor.source_index;
|
||||
if ((from_this_file and visitor.inner_visited.isSet(ref.innerIndex())) or
|
||||
(!from_this_file and visitor.composes_visited.contains(ref.toRealRef(idx))))
|
||||
{
|
||||
return;
|
||||
fn clearAll(visitor: *@This()) void {
|
||||
visitor.inner_visited.setAll(false);
|
||||
visitor.composes_visited.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
visitor.visitComposes(ast, ref, idx);
|
||||
visitor.parts.append(E.TemplatePart{
|
||||
.value = Expr.init(
|
||||
E.NameOfSymbol,
|
||||
E.NameOfSymbol{
|
||||
.ref = ref.toRealRef(idx),
|
||||
fn visitName(visitor: *@This(), ast: *bun.css.BundlerStyleSheet, ref: bun.css.CssRef, idx: Index.Int) void {
|
||||
bun.assert(ref.canBeComposed());
|
||||
const from_this_file = ref.sourceIndex(idx) == visitor.source_index;
|
||||
if ((from_this_file and visitor.inner_visited.isSet(ref.innerIndex())) or
|
||||
(!from_this_file and visitor.composes_visited.contains(ref.toRealRef(idx))))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
visitor.visitComposes(ast, ref, idx);
|
||||
visitor.parts.append(E.TemplatePart{
|
||||
.value = Expr.init(
|
||||
E.NameOfSymbol,
|
||||
E.NameOfSymbol{
|
||||
.ref = ref.toRealRef(idx),
|
||||
},
|
||||
visitor.loc,
|
||||
),
|
||||
.tail = .{
|
||||
.cooked = E.String.init(" "),
|
||||
},
|
||||
visitor.loc,
|
||||
),
|
||||
.tail = .{
|
||||
.cooked = E.String.init(" "),
|
||||
},
|
||||
.tail_loc = visitor.loc,
|
||||
}) catch |err| bun.handleOom(err);
|
||||
.tail_loc = visitor.loc,
|
||||
}) catch |err| bun.handleOom(err);
|
||||
|
||||
if (from_this_file) {
|
||||
visitor.inner_visited.set(ref.innerIndex());
|
||||
} else {
|
||||
visitor.composes_visited.put(ref.toRealRef(idx), {}) catch unreachable;
|
||||
if (from_this_file) {
|
||||
visitor.inner_visited.set(ref.innerIndex());
|
||||
} else {
|
||||
visitor.composes_visited.put(ref.toRealRef(idx), {}) catch unreachable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn warnNonSingleClassComposes(visitor: *@This(), ast: *bun.css.BundlerStyleSheet, css_ref: bun.css.CssRef, idx: Index.Int, compose_loc: Loc) void {
|
||||
const ref = css_ref.toRealRef(idx);
|
||||
_ = ref;
|
||||
const syms: *const Symbol.List = &visitor.all_symbols[css_ref.sourceIndex(idx)];
|
||||
const name = syms.at(css_ref.innerIndex()).original_name;
|
||||
const loc = ast.local_scope.get(name).?.loc;
|
||||
fn warnNonSingleClassComposes(visitor: *@This(), ast: *bun.css.BundlerStyleSheet, css_ref: bun.css.CssRef, idx: Index.Int, compose_loc: Loc) void {
|
||||
const ref = css_ref.toRealRef(idx);
|
||||
_ = ref;
|
||||
const syms: *const Symbol.List = &visitor.all_symbols[css_ref.sourceIndex(idx)];
|
||||
const name = syms.at(css_ref.innerIndex()).original_name;
|
||||
const loc = ast.local_scope.get(name).?.loc;
|
||||
|
||||
visitor.log.addRangeErrorFmtWithNote(
|
||||
&visitor.all_sources[idx],
|
||||
.{ .loc = compose_loc },
|
||||
visitor.allocator,
|
||||
"The composes property cannot be used with {f}, because it is not a single class name.",
|
||||
.{
|
||||
bun.fmt.quote(name),
|
||||
},
|
||||
"The definition of {f} is here.",
|
||||
.{
|
||||
bun.fmt.quote(name),
|
||||
},
|
||||
visitor.log.addRangeErrorFmtWithNote(
|
||||
&visitor.all_sources[idx],
|
||||
.{ .loc = compose_loc },
|
||||
visitor.allocator,
|
||||
"The composes property cannot be used with {f}, because it is not a single class name.",
|
||||
.{
|
||||
bun.fmt.quote(name),
|
||||
},
|
||||
"The definition of {f} is here.",
|
||||
.{
|
||||
bun.fmt.quote(name),
|
||||
},
|
||||
|
||||
.{
|
||||
.loc = loc,
|
||||
},
|
||||
) catch |err| bun.handleOom(err);
|
||||
}
|
||||
.{
|
||||
.loc = loc,
|
||||
},
|
||||
) catch |err| bun.handleOom(err);
|
||||
}
|
||||
|
||||
fn visitComposes(visitor: *@This(), ast: *bun.css.BundlerStyleSheet, css_ref: bun.css.CssRef, idx: Index.Int) void {
|
||||
const ref = css_ref.toRealRef(idx);
|
||||
if (ast.composes.count() > 0) {
|
||||
const composes = ast.composes.getPtr(ref) orelse return;
|
||||
// while parsing we check that we only allow `composes` on single class selectors
|
||||
bun.assert(css_ref.tag.class);
|
||||
fn visitComposes(visitor: *@This(), ast: *bun.css.BundlerStyleSheet, css_ref: bun.css.CssRef, idx: Index.Int) void {
|
||||
const ref = css_ref.toRealRef(idx);
|
||||
if (ast.composes.count() > 0) {
|
||||
const composes = ast.composes.getPtr(ref) orelse return;
|
||||
// while parsing we check that we only allow `composes` on single class selectors
|
||||
bun.assert(css_ref.tag.class);
|
||||
|
||||
for (composes.composes.slice()) |*compose| {
|
||||
// it is imported
|
||||
if (compose.from != null) {
|
||||
if (compose.from.? == .import_record_index) {
|
||||
const import_record_idx = compose.from.?.import_record_index;
|
||||
const import_records: *const BabyList(bun.css.ImportRecord) = &visitor.all_import_records[idx];
|
||||
const import_record = import_records.at(import_record_idx);
|
||||
if (import_record.source_index.isValid()) {
|
||||
const other_file = visitor.all_css_asts[import_record.source_index.get()] orelse {
|
||||
for (composes.composes.slice()) |*compose| {
|
||||
// it is imported
|
||||
if (compose.from != null) {
|
||||
if (compose.from.? == .import_record_index) {
|
||||
const import_record_idx = compose.from.?.import_record_index;
|
||||
const import_records: *const BabyList(bun.css.ImportRecord) = &visitor.all_import_records[idx];
|
||||
const import_record = import_records.at(import_record_idx);
|
||||
if (import_record.source_index.isValid()) {
|
||||
const other_file = visitor.all_css_asts[import_record.source_index.get()] orelse {
|
||||
visitor.log.addErrorFmt(
|
||||
&visitor.all_sources[idx],
|
||||
compose.loc,
|
||||
visitor.allocator,
|
||||
"Cannot use the \"composes\" property with the {f} file (it is not a CSS file)",
|
||||
.{bun.fmt.quote(visitor.all_sources[import_record.source_index.get()].path.pretty)},
|
||||
) catch |err| bun.handleOom(err);
|
||||
continue;
|
||||
};
|
||||
for (compose.names.slice()) |name| {
|
||||
const other_name_entry = other_file.local_scope.get(name.v) orelse continue;
|
||||
const other_name_ref = other_name_entry.ref;
|
||||
if (!other_name_ref.canBeComposed()) {
|
||||
visitor.warnNonSingleClassComposes(other_file, other_name_ref, import_record.source_index.get(), compose.loc);
|
||||
} else {
|
||||
visitor.visitName(other_file, other_name_ref, import_record.source_index.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (compose.from.? == .global) {
|
||||
// E.g.: `composes: foo from global`
|
||||
//
|
||||
// In this example `foo` is global and won't be rewritten to a locally scoped
|
||||
// name, so we can just add it as a string.
|
||||
for (compose.names.slice()) |name| {
|
||||
visitor.parts.append(
|
||||
E.TemplatePart{
|
||||
.value = Expr.init(
|
||||
E.String,
|
||||
E.String.init(name.v),
|
||||
visitor.loc,
|
||||
),
|
||||
.tail = .{
|
||||
.cooked = E.String.init(" "),
|
||||
},
|
||||
.tail_loc = visitor.loc,
|
||||
},
|
||||
) catch |err| bun.handleOom(err);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// it is from the current file
|
||||
for (compose.names.slice()) |name| {
|
||||
const name_entry = ast.local_scope.get(name.v) orelse {
|
||||
visitor.log.addErrorFmt(
|
||||
&visitor.all_sources[idx],
|
||||
compose.loc,
|
||||
visitor.allocator,
|
||||
"Cannot use the \"composes\" property with the {f} file (it is not a CSS file)",
|
||||
.{bun.fmt.quote(visitor.all_sources[import_record.source_index.get()].path.pretty)},
|
||||
"The name {f} never appears in {f} as a CSS modules locally scoped class name. Note that \"composes\" only works with single class selectors.",
|
||||
.{
|
||||
bun.fmt.quote(name.v),
|
||||
bun.fmt.quote(visitor.all_sources[idx].path.pretty),
|
||||
},
|
||||
) catch |err| bun.handleOom(err);
|
||||
continue;
|
||||
};
|
||||
for (compose.names.slice()) |name| {
|
||||
const other_name_entry = other_file.local_scope.get(name.v) orelse continue;
|
||||
const other_name_ref = other_name_entry.ref;
|
||||
if (!other_name_ref.canBeComposed()) {
|
||||
visitor.warnNonSingleClassComposes(other_file, other_name_ref, import_record.source_index.get(), compose.loc);
|
||||
} else {
|
||||
visitor.visitName(other_file, other_name_ref, import_record.source_index.get());
|
||||
}
|
||||
const name_ref = name_entry.ref;
|
||||
if (!name_ref.canBeComposed()) {
|
||||
visitor.warnNonSingleClassComposes(ast, name_ref, idx, compose.loc);
|
||||
} else {
|
||||
visitor.visitName(ast, name_ref, idx);
|
||||
}
|
||||
}
|
||||
} else if (compose.from.? == .global) {
|
||||
// E.g.: `composes: foo from global`
|
||||
//
|
||||
// In this example `foo` is global and won't be rewritten to a locally scoped
|
||||
// name, so we can just add it as a string.
|
||||
for (compose.names.slice()) |name| {
|
||||
visitor.parts.append(
|
||||
E.TemplatePart{
|
||||
.value = Expr.init(
|
||||
E.String,
|
||||
E.String.init(name.v),
|
||||
visitor.loc,
|
||||
),
|
||||
.tail = .{
|
||||
.cooked = E.String.init(" "),
|
||||
},
|
||||
.tail_loc = visitor.loc,
|
||||
},
|
||||
) catch |err| bun.handleOom(err);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// it is from the current file
|
||||
for (compose.names.slice()) |name| {
|
||||
const name_entry = ast.local_scope.get(name.v) orelse {
|
||||
visitor.log.addErrorFmt(
|
||||
&visitor.all_sources[idx],
|
||||
compose.loc,
|
||||
visitor.allocator,
|
||||
"The name {f} never appears in {f} as a CSS modules locally scoped class name. Note that \"composes\" only works with single class selectors.",
|
||||
.{
|
||||
bun.fmt.quote(name.v),
|
||||
bun.fmt.quote(visitor.all_sources[idx].path.pretty),
|
||||
},
|
||||
) catch |err| bun.handleOom(err);
|
||||
continue;
|
||||
};
|
||||
const name_ref = name_entry.ref;
|
||||
if (!name_ref.canBeComposed()) {
|
||||
visitor.warnNonSingleClassComposes(ast, name_ref, idx, compose.loc);
|
||||
} else {
|
||||
visitor.visitName(ast, name_ref, idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
var visitor = Visitor{
|
||||
.inner_visited = &inner_visited,
|
||||
.composes_visited = &composes_visited,
|
||||
.source_index = source_index,
|
||||
.parts = undefined,
|
||||
.all_import_records = all_import_records,
|
||||
.all_css_asts = all_css_asts,
|
||||
.loc = stmt.loc,
|
||||
.log = this.log,
|
||||
.all_sources = all_sources,
|
||||
.allocator = this.allocator(),
|
||||
.all_symbols = this.graph.ast.items(.symbols),
|
||||
};
|
||||
var visitor = Visitor{
|
||||
.inner_visited = &inner_visited,
|
||||
.composes_visited = &composes_visited,
|
||||
.source_index = source_index,
|
||||
.parts = undefined,
|
||||
.all_import_records = all_import_records,
|
||||
.all_css_asts = all_css_asts,
|
||||
.loc = stmt.loc,
|
||||
.log = this.log,
|
||||
.all_sources = all_sources,
|
||||
.allocator = this.allocator(),
|
||||
.all_symbols = this.graph.ast.items(.symbols),
|
||||
};
|
||||
|
||||
for (values) |entry| {
|
||||
const ref = entry.ref;
|
||||
bun.assert(ref.inner_index < symbols.len);
|
||||
for (values) |entry| {
|
||||
const ref = entry.ref;
|
||||
bun.assert(ref.inner_index < symbols.len);
|
||||
|
||||
var template_parts = std.array_list.Managed(E.TemplatePart).init(this.allocator());
|
||||
var value = Expr.init(E.NameOfSymbol, E.NameOfSymbol{ .ref = ref.toRealRef(source_index) }, stmt.loc);
|
||||
var template_parts = std.array_list.Managed(E.TemplatePart).init(this.allocator());
|
||||
var value = Expr.init(E.NameOfSymbol, E.NameOfSymbol{ .ref = ref.toRealRef(source_index) }, stmt.loc);
|
||||
|
||||
visitor.parts = &template_parts;
|
||||
visitor.clearAll();
|
||||
visitor.inner_visited.set(ref.innerIndex());
|
||||
if (ref.tag.class) visitor.visitComposes(css_ast, ref, source_index);
|
||||
visitor.parts = &template_parts;
|
||||
visitor.clearAll();
|
||||
visitor.inner_visited.set(ref.innerIndex());
|
||||
if (ref.tag.class) visitor.visitComposes(css_ast, ref, source_index);
|
||||
|
||||
if (template_parts.items.len > 0) {
|
||||
template_parts.append(E.TemplatePart{
|
||||
.value = value,
|
||||
.tail_loc = stmt.loc,
|
||||
.tail = .{ .cooked = E.String.init("") },
|
||||
}) catch |err| bun.handleOom(err);
|
||||
value = Expr.init(
|
||||
E.Template,
|
||||
E.Template{
|
||||
.parts = template_parts.items,
|
||||
.head = .{
|
||||
.cooked = E.String.init(""),
|
||||
if (template_parts.items.len > 0) {
|
||||
template_parts.append(E.TemplatePart{
|
||||
.value = value,
|
||||
.tail_loc = stmt.loc,
|
||||
.tail = .{ .cooked = E.String.init("") },
|
||||
}) catch |err| bun.handleOom(err);
|
||||
value = Expr.init(
|
||||
E.Template,
|
||||
E.Template{
|
||||
.parts = template_parts.items,
|
||||
.head = .{
|
||||
.cooked = E.String.init(""),
|
||||
},
|
||||
},
|
||||
},
|
||||
stmt.loc,
|
||||
);
|
||||
stmt.loc,
|
||||
);
|
||||
}
|
||||
|
||||
const key = symbols.at(ref.innerIndex()).original_name;
|
||||
try exports.put(this.allocator(), key, value);
|
||||
}
|
||||
|
||||
const key = symbols.at(ref.innerIndex()).original_name;
|
||||
try exports.put(this.allocator(), key, value);
|
||||
part.stmts[0].data.s_lazy_export.* = Expr.init(E.Object, exports, stmt.loc).data;
|
||||
}
|
||||
|
||||
part.stmts[0].data.s_lazy_export.* = Expr.init(E.Object, exports, stmt.loc).data;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,11 @@ pub fn scanImportsAndExports(this: *LinkerContext) ScanImportsAndExportsError!vo
|
||||
|
||||
const other_file = record.source_index.get();
|
||||
const other_flags = ast_flags_list[other_file];
|
||||
|
||||
// Track CSS Module Script imports (import ... with { type: 'css' })
|
||||
if (record.is_css_module_script and css_asts[other_file] != null) {
|
||||
this.graph.css_module_script_files.set(other_file);
|
||||
}
|
||||
// other file is empty
|
||||
if (other_file >= exports_kind.len) continue;
|
||||
const other_kind = exports_kind[other_file];
|
||||
@@ -141,9 +146,14 @@ pub fn scanImportsAndExports(this: *LinkerContext) ScanImportsAndExportsError!vo
|
||||
},
|
||||
.dynamic => {
|
||||
if (!this.graph.code_splitting) {
|
||||
// If we're not splitting, then import() is just a require() that
|
||||
// returns a promise, so the imported file must be a CommonJS module
|
||||
if (exports_kind[other_file] == .esm) {
|
||||
// CSS Module Script imports should not be wrapped as CommonJS
|
||||
// They need to remain ESM to use __cssModuleScript
|
||||
if (record.is_css_module_script) {
|
||||
// Keep as ESM - will be handled by generateCodeForLazyExport
|
||||
flags[other_file].wrap = .esm;
|
||||
} else if (exports_kind[other_file] == .esm) {
|
||||
// If we're not splitting, then import() is just a require() that
|
||||
// returns a promise, so the imported file must be a CommonJS module
|
||||
flags[other_file].wrap = .esm;
|
||||
} else {
|
||||
// TODO: introduce a NamedRequire for require("./foo").Bar AST nodes to support tree-shaking those.
|
||||
|
||||
@@ -166,6 +166,9 @@ pub const ImportRecord = struct {
|
||||
wrap_with_to_esm: bool = false,
|
||||
wrap_with_to_commonjs: bool = false,
|
||||
|
||||
/// True if this import has `with { type: 'css' }` and should return a CSSStyleSheet
|
||||
is_css_module_script: bool = false,
|
||||
|
||||
pub const List = bun.BabyList(ImportRecord);
|
||||
|
||||
pub const Tag = enum {
|
||||
|
||||
@@ -177,3 +177,11 @@ export var $$typeof = /* @__PURE__ */ Symbol.for("react.element");
|
||||
export var __jsonParse = /* @__PURE__ */ a => JSON.parse(a);
|
||||
|
||||
export var __promiseAll = args => Promise.all(args);
|
||||
|
||||
// CSS Module Scripts helper - creates a CSSStyleSheet from CSS text
|
||||
// See: https://web.dev/articles/css-module-scripts
|
||||
export var __cssModuleScript = /* @__PURE__ */ cssText => {
|
||||
var sheet = new CSSStyleSheet();
|
||||
sheet.replaceSync(cssText);
|
||||
return sheet;
|
||||
};
|
||||
|
||||
@@ -318,6 +318,7 @@ pub const Runtime = struct {
|
||||
__callDispose: ?Ref = null,
|
||||
__jsonParse: ?Ref = null,
|
||||
__promiseAll: ?Ref = null,
|
||||
__cssModuleScript: ?Ref = null,
|
||||
|
||||
pub const all = [_][]const u8{
|
||||
"__name",
|
||||
@@ -335,6 +336,7 @@ pub const Runtime = struct {
|
||||
"__callDispose",
|
||||
"__jsonParse",
|
||||
"__promiseAll",
|
||||
"__cssModuleScript",
|
||||
};
|
||||
const all_sorted: [all.len]string = brk: {
|
||||
@setEvalBranchQuota(1000000);
|
||||
|
||||
147
test/bundler/css/css-module-scripts.test.ts
Normal file
147
test/bundler/css/css-module-scripts.test.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { itBundled } from "../expectBundled";
|
||||
|
||||
// Tests for CSS Module Scripts - https://web.dev/articles/css-module-scripts
|
||||
// When importing CSS with `with { type: 'css' }`, the import should return a CSSStyleSheet object
|
||||
describe("css-module-scripts", () => {
|
||||
// Mock CSSStyleSheet for testing since we're running in Bun, not a browser
|
||||
const env = {
|
||||
...process.env,
|
||||
// Inject a mock CSSStyleSheet constructor
|
||||
BUN_DEBUG_QUIET_LOGS: "1",
|
||||
};
|
||||
|
||||
itBundled("css-module-scripts/StaticImportWithTypeCSS", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
import sheet from './styles.css' with { type: 'css' };
|
||||
console.log('sheet type:', typeof sheet);
|
||||
console.log('sheet instanceof CSSStyleSheet:', sheet instanceof CSSStyleSheet);
|
||||
console.log('cssRules length:', sheet.cssRules.length);
|
||||
`,
|
||||
"/styles.css": `.foo { color: red; }`,
|
||||
},
|
||||
entryPoints: ["/entry.js"],
|
||||
outdir: "/out",
|
||||
target: "browser",
|
||||
format: "esm",
|
||||
onAfterBundle(api) {
|
||||
// Verify the output contains __cssModuleScript call
|
||||
const content = api.readFile("/out/entry.js");
|
||||
expect(content).toContain("__cssModuleScript");
|
||||
expect(content).toContain(".foo { color: red; }");
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("css-module-scripts/DynamicImportWithTypeCSS", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
const module = await import('./styles.css', { with: { type: 'css' } });
|
||||
const sheet = module.default;
|
||||
console.log('sheet type:', typeof sheet);
|
||||
console.log('sheet instanceof CSSStyleSheet:', sheet instanceof CSSStyleSheet);
|
||||
console.log('cssRules length:', sheet.cssRules.length);
|
||||
`,
|
||||
"/styles.css": `.bar { color: blue; }`,
|
||||
},
|
||||
entryPoints: ["/entry.js"],
|
||||
outdir: "/out",
|
||||
target: "browser",
|
||||
format: "esm",
|
||||
onAfterBundle(api) {
|
||||
// Verify the output contains __cssModuleScript call with CSS content
|
||||
const content = api.readFile("/out/entry.js");
|
||||
expect(content).toContain("__cssModuleScript");
|
||||
expect(content).toContain(".bar { color: blue; }");
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("css-module-scripts/DynamicImportWithAssertTypeCSS", {
|
||||
// Test the older `assert` syntax for backwards compatibility
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
const module = await import('./styles.css', { assert: { type: 'css' } });
|
||||
const sheet = module.default;
|
||||
console.log('sheet type:', typeof sheet);
|
||||
`,
|
||||
"/styles.css": `.baz { color: green; }`,
|
||||
},
|
||||
entryPoints: ["/entry.js"],
|
||||
outdir: "/out",
|
||||
target: "browser",
|
||||
format: "esm",
|
||||
onAfterBundle(api) {
|
||||
// Verify the output contains __cssModuleScript call
|
||||
const content = api.readFile("/out/entry.js");
|
||||
expect(content).toContain("__cssModuleScript");
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("css-module-scripts/CSSModuleWithTypeCSS", {
|
||||
// CSS Modules (*.module.css) should still work with type: 'css'
|
||||
// but return a CSSStyleSheet instead of the class name mapping
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
import sheet from './styles.module.css' with { type: 'css' };
|
||||
console.log('sheet instanceof CSSStyleSheet:', sheet instanceof CSSStyleSheet);
|
||||
`,
|
||||
"/styles.module.css": `.myClass { color: purple; }`,
|
||||
},
|
||||
entryPoints: ["/entry.js"],
|
||||
outdir: "/out",
|
||||
target: "browser",
|
||||
format: "esm",
|
||||
onAfterBundle(api) {
|
||||
const content = api.readFile("/out/entry.js");
|
||||
expect(content).toContain("__cssModuleScript");
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("css-module-scripts/PlainCSSImportWithoutType", {
|
||||
// Plain CSS imports without type should NOT return CSSStyleSheet
|
||||
// (existing behavior - either side-effect or object with class names)
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
import './styles.css';
|
||||
console.log('CSS imported as side effect');
|
||||
`,
|
||||
"/styles.css": `.plain { color: black; }`,
|
||||
},
|
||||
entryPoints: ["/entry.js"],
|
||||
outdir: "/out",
|
||||
target: "browser",
|
||||
format: "esm",
|
||||
onAfterBundle(api) {
|
||||
const content = api.readFile("/out/entry.js");
|
||||
// Should NOT contain CSSStyleSheet for plain imports
|
||||
expect(content).not.toContain("new CSSStyleSheet");
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("css-module-scripts/MultipleRules", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
import sheet from './styles.css' with { type: 'css' };
|
||||
console.log('rules:', sheet.cssRules.length);
|
||||
`,
|
||||
"/styles.css": /* css */ `
|
||||
.a { color: red; }
|
||||
.b { color: blue; }
|
||||
.c { color: green; }
|
||||
@media (min-width: 768px) {
|
||||
.a { color: darkred; }
|
||||
}
|
||||
`,
|
||||
},
|
||||
entryPoints: ["/entry.js"],
|
||||
outdir: "/out",
|
||||
target: "browser",
|
||||
format: "esm",
|
||||
onAfterBundle(api) {
|
||||
const content = api.readFile("/out/entry.js");
|
||||
expect(content).toContain("__cssModuleScript");
|
||||
// The CSS content should be included as a string
|
||||
expect(content).toContain(".a");
|
||||
expect(content).toContain("color");
|
||||
},
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user