mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
Compare commits
2 Commits
claude/css
...
zack/fix-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eec0b57e65 | ||
|
|
0094e15f80 |
38
instructions.md
Normal file
38
instructions.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Fixing CSS modules in Bun's dev server
|
||||||
|
|
||||||
|
Look inside the reproduction folder: /Users/zackradisic/Code/bun-repro-18258/
|
||||||
|
|
||||||
|
When importing a CSS module, it is not being resolved correctly and the following error is thrown:
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend ReferenceError: import_Ooga_module is not defined
|
||||||
|
at App (/Users/zackradisic/Code/bun-repro-18258/src/App.tsx:5:21)
|
||||||
|
at react-stack-bottom-frame (/Users/zackradisic/Code/bun-repro-18258/node_modules/react-dom/cjs/react-dom-client.development.js:23863:20)
|
||||||
|
at renderWithHooks (/Users/zackradisic/Code/bun-repro-18258/node_modules/react-dom/cjs/react-dom-client.development.js:5529:22)
|
||||||
|
at updateFunctionComponent (/Users/zackradisic/Code/bun-repro-18258/node_modules/react-dom/cjs/react-dom-client.development.js:8897:19)
|
||||||
|
at beginWork (/Users/zackradisic/Code/bun-repro-18258/node_modules/react-dom/cjs/react-dom-client.development.js:10522:18)
|
||||||
|
at runWithFiberInDEV (/Users/zackradisic/Code/bun-repro-18258/node_modules/react-dom/cjs/react-dom-client.development.js:1519:30)
|
||||||
|
at performUnitOfWork (/Users/zackradisic/Code/bun-repro-18258/node_modules/react-dom/cjs/react-dom-client.development.js:15132:22)
|
||||||
|
at workLoopSync (/Users/zackradisic/Code/bun-repro-18258/node_modules/react-dom/cjs/react-dom-client.development.js:14956:41)
|
||||||
|
at renderRootSync (/Users/zackradisic/Code/bun-repro-18258/node_modules/react-dom/cjs/react-dom-client.development.js:14936:11)
|
||||||
|
at performWorkOnRoot (/Users/zackradisic/Code/bun-repro-18258/node_modules/react-dom/cjs/react-dom-client.development.js:14462:44)
|
||||||
|
```
|
||||||
|
|
||||||
|
Some notes on how CSS modules work:
|
||||||
|
|
||||||
|
- We generate a JS object where the keys are the class names and the values are the class names made unique. This starts on line 226 in `generateCodeForLazyExport.zig`. We do this using `E.NameOfSymbol` which uses a `Ref` because we don't know the final generated name until after all CSS modules are parsed.
|
||||||
|
- The `mangleLocalCss` (inside `LinkerContext.zig`) is called after all CSS modules are parsed. It is used to mangle the class names in the CSS modules so that they are unique.
|
||||||
|
|
||||||
|
Some notes about how Bun's dev server is different and is what likely causing the issue:
|
||||||
|
|
||||||
|
- The dev server skips stuff and doesn't call all the same linker functions
|
||||||
|
- This is causing `generateCodeForLazyExport` to _not_ be called and thus the JS object is not generated for importing CSS modules
|
||||||
|
- We will have to create our own routine which will do this
|
||||||
|
- One key place to look at is `finalizeBundle(...)` in `DevServer.zig`, that's when the bundle is finalized.
|
||||||
|
|
||||||
|
You instructions:
|
||||||
|
|
||||||
|
1. Split out the existing code in `generateCodeForLazyExport` which handles CSS into a new function which can be called separately
|
||||||
|
2. Call this new function in `finalizeBundle(...)` in `DevServer.zig`
|
||||||
|
|
||||||
|
**IMPORTANT**: If you need ANY help from me (for example running bun or something), please ask me by executing the command `say bun2 help`.
|
||||||
@@ -436,6 +436,7 @@ pub const LinkerContext = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub const generateCodeForLazyExport = @import("linker_context/generateCodeForLazyExport.zig").generateCodeForLazyExport;
|
pub const generateCodeForLazyExport = @import("linker_context/generateCodeForLazyExport.zig").generateCodeForLazyExport;
|
||||||
|
pub const generateCodeForCSSModuleIfNeeded = @import("linker_context/generateCodeForLazyExport.zig").generateCodeForCSSModuleIfNeeded;
|
||||||
pub const scanImportsAndExports = @import("linker_context/scanImportsAndExports.zig").scanImportsAndExports;
|
pub const scanImportsAndExports = @import("linker_context/scanImportsAndExports.zig").scanImportsAndExports;
|
||||||
pub const doStep5 = @import("linker_context/doStep5.zig").doStep5;
|
pub const doStep5 = @import("linker_context/doStep5.zig").doStep5;
|
||||||
pub const createExportsForFile = @import("linker_context/doStep5.zig").createExportsForFile;
|
pub const createExportsForFile = @import("linker_context/doStep5.zig").createExportsForFile;
|
||||||
|
|||||||
@@ -2331,7 +2331,7 @@ pub const BundleV2 = struct {
|
|||||||
var html_files: std.AutoArrayHashMapUnmanaged(Index, void) = .{};
|
var html_files: std.AutoArrayHashMapUnmanaged(Index, void) = .{};
|
||||||
|
|
||||||
// Separate non-failing files into two lists: JS and CSS
|
// Separate non-failing files into two lists: JS and CSS
|
||||||
const js_reachable_files = reachable_files: {
|
const js_reachable_files, const css_reachable_files = reachable_files: {
|
||||||
var css_total_files = try std.ArrayListUnmanaged(Index).initCapacity(this.graph.allocator, this.graph.css_file_count);
|
var css_total_files = try std.ArrayListUnmanaged(Index).initCapacity(this.graph.allocator, this.graph.css_file_count);
|
||||||
try start.css_entry_points.ensureUnusedCapacity(this.graph.allocator, this.graph.css_file_count);
|
try start.css_entry_points.ensureUnusedCapacity(this.graph.allocator, this.graph.css_file_count);
|
||||||
var js_files = try std.ArrayListUnmanaged(Index).initCapacity(this.graph.allocator, this.graph.ast.len - this.graph.css_file_count - 1);
|
var js_files = try std.ArrayListUnmanaged(Index).initCapacity(this.graph.allocator, this.graph.ast.len - this.graph.css_file_count - 1);
|
||||||
@@ -2434,7 +2434,7 @@ pub const BundleV2 = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break :reachable_files js_files.items;
|
break :reachable_files .{ js_files.items, css_total_files.items };
|
||||||
};
|
};
|
||||||
|
|
||||||
this.graph.heap.helpCatchMemoryIssues();
|
this.graph.heap.helpCatchMemoryIssues();
|
||||||
@@ -2458,6 +2458,11 @@ pub const BundleV2 = struct {
|
|||||||
js_reachable_files,
|
js_reachable_files,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Generate code for CSS modules
|
||||||
|
for (css_reachable_files) |source_index| {
|
||||||
|
try this.linker.generateCodeForCSSModuleIfNeeded(source_index.get());
|
||||||
|
}
|
||||||
|
|
||||||
this.graph.heap.helpCatchMemoryIssues();
|
this.graph.heap.helpCatchMemoryIssues();
|
||||||
|
|
||||||
// Compute line offset tables and quoted contents, used in source maps.
|
// Compute line offset tables and quoted contents, used in source maps.
|
||||||
|
|||||||
@@ -1,6 +1,271 @@
|
|||||||
|
fn generateCodeForCSSModule(this: *LinkerContext, source_index: Index.Int, css_ast: *bun.css.BundlerStyleSheet, part: *Part, stmt_loc: Loc) !void {
|
||||||
|
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 all_sources = this.parse_graph.input_files.items(.source);
|
||||||
|
const all_css_asts = this.graph.ast.items(.css);
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
const Visitor = struct {
|
||||||
|
inner_visited: *BitSet,
|
||||||
|
composes_visited: *std.AutoArrayHashMap(bun.bundle_v2.Ref, void),
|
||||||
|
parts: *std.ArrayList(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(" "),
|
||||||
|
},
|
||||||
|
.tail_loc = visitor.loc,
|
||||||
|
}) catch bun.outOfMemory();
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
visitor.log.addRangeErrorFmtWithNote(
|
||||||
|
&visitor.all_sources[idx],
|
||||||
|
.{ .loc = compose_loc },
|
||||||
|
visitor.allocator,
|
||||||
|
"The composes property cannot be used with {}, because it is not a single class name.",
|
||||||
|
.{
|
||||||
|
bun.fmt.quote(name),
|
||||||
|
},
|
||||||
|
"The definition of {} is here.",
|
||||||
|
.{
|
||||||
|
bun.fmt.quote(name),
|
||||||
|
},
|
||||||
|
|
||||||
|
.{
|
||||||
|
.loc = loc,
|
||||||
|
},
|
||||||
|
) catch bun.outOfMemory();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
visitor.log.addErrorFmt(
|
||||||
|
&visitor.all_sources[idx],
|
||||||
|
compose.loc,
|
||||||
|
visitor.allocator,
|
||||||
|
"Cannot use the \"composes\" property with the {} file (it is not a CSS file)",
|
||||||
|
.{bun.fmt.quote(visitor.all_sources[import_record.source_index.get()].path.pretty)},
|
||||||
|
) catch bun.outOfMemory();
|
||||||
|
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 bun.outOfMemory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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 {} never appears in {} 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 bun.outOfMemory();
|
||||||
|
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),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (values) |entry| {
|
||||||
|
const ref = entry.ref;
|
||||||
|
bun.assert(ref.inner_index < symbols.len);
|
||||||
|
|
||||||
|
var template_parts = std.ArrayList(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);
|
||||||
|
|
||||||
|
if (template_parts.items.len > 0) {
|
||||||
|
template_parts.append(E.TemplatePart{
|
||||||
|
.value = value,
|
||||||
|
.tail_loc = stmt_loc,
|
||||||
|
.tail = .{ .cooked = E.String.init("") },
|
||||||
|
}) catch bun.outOfMemory();
|
||||||
|
value = Expr.init(
|
||||||
|
E.Template,
|
||||||
|
E.Template{
|
||||||
|
.parts = template_parts.items,
|
||||||
|
.head = .{
|
||||||
|
.cooked = E.String.init(""),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stmt_loc,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generateCodeForCSSModuleIfNeeded(this: *LinkerContext, source_index: Index.Int) !void {
|
||||||
|
const all_css_asts = this.graph.ast.items(.css);
|
||||||
|
const maybe_css_ast: ?*bun.css.BundlerStyleSheet = all_css_asts[source_index];
|
||||||
|
|
||||||
|
if (maybe_css_ast) |css_ast| {
|
||||||
|
const parts = &this.graph.ast.items(.parts)[source_index];
|
||||||
|
|
||||||
|
if (parts.len < 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const part: *Part = &parts.ptr[1];
|
||||||
|
|
||||||
|
if (part.stmts.len == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stmt: Stmt = part.stmts[0];
|
||||||
|
if (stmt.data != .s_lazy_export) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try generateCodeForCSSModule(this, source_index, css_ast, part, stmt.loc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn generateCodeForLazyExport(this: *LinkerContext, source_index: Index.Int) !void {
|
pub fn generateCodeForLazyExport(this: *LinkerContext, source_index: Index.Int) !void {
|
||||||
const exports_kind = this.graph.ast.items(.exports_kind)[source_index];
|
const exports_kind = this.graph.ast.items(.exports_kind)[source_index];
|
||||||
const all_sources = this.parse_graph.input_files.items(.source);
|
|
||||||
const all_css_asts = this.graph.ast.items(.css);
|
const all_css_asts = this.graph.ast.items(.css);
|
||||||
const maybe_css_ast: ?*bun.css.BundlerStyleSheet = all_css_asts[source_index];
|
const maybe_css_ast: ?*bun.css.BundlerStyleSheet = all_css_asts[source_index];
|
||||||
var parts = &this.graph.ast.items(.parts)[source_index];
|
var parts = &this.graph.ast.items(.parts)[source_index];
|
||||||
@@ -25,240 +290,7 @@ pub fn generateCodeForLazyExport(this: *LinkerContext, source_index: Index.Int)
|
|||||||
// now instead of earlier because we need the whole bundle to be present.
|
// now instead of earlier because we need the whole bundle to be present.
|
||||||
if (maybe_css_ast) |css_ast| {
|
if (maybe_css_ast) |css_ast| {
|
||||||
const stmt: Stmt = part.stmts[0];
|
const stmt: Stmt = part.stmts[0];
|
||||||
if (stmt.data != .s_lazy_export) {
|
try generateCodeForCSSModule(this, source_index, css_ast, part, stmt.loc);
|
||||||
@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 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();
|
|
||||||
|
|
||||||
const Visitor = struct {
|
|
||||||
inner_visited: *BitSet,
|
|
||||||
composes_visited: *std.AutoArrayHashMap(bun.bundle_v2.Ref, void),
|
|
||||||
parts: *std.ArrayList(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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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(" "),
|
|
||||||
},
|
|
||||||
.tail_loc = visitor.loc,
|
|
||||||
}) catch bun.outOfMemory();
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
visitor.log.addRangeErrorFmtWithNote(
|
|
||||||
&visitor.all_sources[idx],
|
|
||||||
.{ .loc = compose_loc },
|
|
||||||
visitor.allocator,
|
|
||||||
"The composes property cannot be used with {}, because it is not a single class name.",
|
|
||||||
.{
|
|
||||||
bun.fmt.quote(name),
|
|
||||||
},
|
|
||||||
"The definition of {} is here.",
|
|
||||||
.{
|
|
||||||
bun.fmt.quote(name),
|
|
||||||
},
|
|
||||||
|
|
||||||
.{
|
|
||||||
.loc = loc,
|
|
||||||
},
|
|
||||||
) catch bun.outOfMemory();
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
visitor.log.addErrorFmt(
|
|
||||||
&visitor.all_sources[idx],
|
|
||||||
compose.loc,
|
|
||||||
visitor.allocator,
|
|
||||||
"Cannot use the \"composes\" property with the {} file (it is not a CSS file)",
|
|
||||||
.{bun.fmt.quote(visitor.all_sources[import_record.source_index.get()].path.pretty)},
|
|
||||||
) catch bun.outOfMemory();
|
|
||||||
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 bun.outOfMemory();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} 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 {} never appears in {} 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 bun.outOfMemory();
|
|
||||||
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),
|
|
||||||
};
|
|
||||||
|
|
||||||
for (values) |entry| {
|
|
||||||
const ref = entry.ref;
|
|
||||||
bun.assert(ref.inner_index < symbols.len);
|
|
||||||
|
|
||||||
var template_parts = std.ArrayList(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);
|
|
||||||
|
|
||||||
if (template_parts.items.len > 0) {
|
|
||||||
template_parts.append(E.TemplatePart{
|
|
||||||
.value = value,
|
|
||||||
.tail_loc = stmt.loc,
|
|
||||||
.tail = .{ .cooked = E.String.init("") },
|
|
||||||
}) catch bun.outOfMemory();
|
|
||||||
value = Expr.init(
|
|
||||||
E.Template,
|
|
||||||
E.Template{
|
|
||||||
.parts = template_parts.items,
|
|
||||||
.head = .{
|
|
||||||
.cooked = E.String.init(""),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
stmt.loc,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const stmt: Stmt = part.stmts[0];
|
const stmt: Stmt = part.stmts[0];
|
||||||
|
|||||||
124
test/regression/issue/18258-css-modules-dev-server.test.ts
Normal file
124
test/regression/issue/18258-css-modules-dev-server.test.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import { test, expect } from "bun:test";
|
||||||
|
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
|
||||||
|
|
||||||
|
test("CSS modules work in dev server", async () => {
|
||||||
|
// Increase timeout
|
||||||
|
await Bun.sleep(0);
|
||||||
|
const timeoutController = new AbortController();
|
||||||
|
const timeout = setTimeout(() => timeoutController.abort(), 30000);
|
||||||
|
const dir = tempDirWithFiles("css-modules-dev", {
|
||||||
|
"package.json": JSON.stringify({
|
||||||
|
name: "css-modules-test",
|
||||||
|
scripts: {
|
||||||
|
dev: "bun dev"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
"src/App.tsx": `
|
||||||
|
import classes from "./styles.module.css";
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
return (
|
||||||
|
<div className={classes.container}>
|
||||||
|
<h1 className={classes.title}>Hello CSS Modules</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
"src/styles.module.css": `
|
||||||
|
.container {
|
||||||
|
background: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
"src/index.tsx": `
|
||||||
|
import { serve } from "bun";
|
||||||
|
|
||||||
|
const server = serve({
|
||||||
|
port: 0, // Random port
|
||||||
|
async fetch(req) {
|
||||||
|
const url = new URL(req.url);
|
||||||
|
if (url.pathname === "/test") {
|
||||||
|
// Import and render the component
|
||||||
|
const { App } = await import("./App.tsx");
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
component: App.toString(),
|
||||||
|
hasClasses: typeof App === 'function'
|
||||||
|
}),
|
||||||
|
{ headers: { "Content-Type": "application/json" } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new Response("Not found", { status: 404 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("PORT:" + server.port);
|
||||||
|
`,
|
||||||
|
"bunfig.toml": `
|
||||||
|
[dev]
|
||||||
|
framework = "react"
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
const proc = Bun.spawn({
|
||||||
|
cmd: [bunExe(), "dev"],
|
||||||
|
env: bunEnv,
|
||||||
|
cwd: dir,
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe"
|
||||||
|
});
|
||||||
|
|
||||||
|
let port = 0;
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
|
||||||
|
// Wait for server to start and get port
|
||||||
|
const reader = proc.stdout!.getReader();
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
const text = new TextDecoder().decode(value);
|
||||||
|
stdout += text;
|
||||||
|
|
||||||
|
const portMatch = text.match(/PORT:(\d+)/);
|
||||||
|
if (portMatch) {
|
||||||
|
port = parseInt(portMatch[1]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also capture stderr
|
||||||
|
(async () => {
|
||||||
|
const reader = proc.stderr!.getReader();
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
stderr += new TextDecoder().decode(value);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
expect(port).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Test that CSS modules don't throw errors
|
||||||
|
const response = await fetch(`http://localhost:${port}/test`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(data.hasClasses).toBe(true);
|
||||||
|
|
||||||
|
// The component should render without errors
|
||||||
|
expect(data.component).toContain("function App()");
|
||||||
|
|
||||||
|
// Check stderr for CSS module errors
|
||||||
|
expect(stderr).not.toContain("import_styles_module is not defined");
|
||||||
|
expect(stderr).not.toContain("ReferenceError");
|
||||||
|
} finally {
|
||||||
|
proc.kill();
|
||||||
|
await proc.exited;
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user