mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 23:18:47 +00:00
Compare commits
2 Commits
ciro/fix-a
...
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 generateCodeForCSSModuleIfNeeded = @import("linker_context/generateCodeForLazyExport.zig").generateCodeForCSSModuleIfNeeded;
|
||||
pub const scanImportsAndExports = @import("linker_context/scanImportsAndExports.zig").scanImportsAndExports;
|
||||
pub const doStep5 = @import("linker_context/doStep5.zig").doStep5;
|
||||
pub const createExportsForFile = @import("linker_context/doStep5.zig").createExportsForFile;
|
||||
|
||||
@@ -2331,7 +2331,7 @@ pub const BundleV2 = struct {
|
||||
var html_files: std.AutoArrayHashMapUnmanaged(Index, void) = .{};
|
||||
|
||||
// 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);
|
||||
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);
|
||||
@@ -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();
|
||||
@@ -2458,6 +2458,11 @@ pub const BundleV2 = struct {
|
||||
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();
|
||||
|
||||
// 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 {
|
||||
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 maybe_css_ast: ?*bun.css.BundlerStyleSheet = all_css_asts[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.
|
||||
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{};
|
||||
|
||||
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;
|
||||
}
|
||||
try generateCodeForCSSModule(this, source_index, css_ast, part, stmt.loc);
|
||||
}
|
||||
|
||||
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