Compare commits

...

2 Commits

Author SHA1 Message Date
Zack Radisic
eec0b57e65 temporarily commit this for future reference 2025-07-14 00:01:34 -07:00
Zack Radisic
0094e15f80 claude code attempt 2025-07-13 23:50:12 -07:00
5 changed files with 437 additions and 237 deletions

38
instructions.md Normal file
View 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`.

View File

@@ -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;

View File

@@ -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.

View File

@@ -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];

View 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;
}
});