Compare commits

...

4 Commits

Author SHA1 Message Date
Jarred-Sumner
9ecc903c4e bun run prettier 2025-06-30 05:33:50 +00:00
Jarred-Sumner
b1b89c0246 bun run zig-format 2025-06-30 05:33:03 +00:00
Jarred Sumner
70f9a9aa55 Delete LAYER_COLOR_SCHEME_FIX.md 2025-06-29 22:32:44 -07:00
Cursor Agent
42ddc6b893 Fix @layer color-scheme bug with layer context tracking
Co-authored-by: jarred <jarred@bun.sh>
2025-06-30 05:22:38 +00:00
5 changed files with 195 additions and 4 deletions

View File

@@ -44,6 +44,7 @@ pub const PropertyHandlerContext = struct {
dark: ArrayList(css.Property),
context: DeclarationContext,
unused_symbols: *const std.StringArrayHashMapUnmanaged(void),
layer_name: ?css.css_rules.layer.LayerName,
pub fn new(
allocator: Allocator,
@@ -60,6 +61,7 @@ pub const PropertyHandlerContext = struct {
.dark = ArrayList(css.Property){},
.context = DeclarationContext.none,
.unused_symbols = unused_symbols,
.layer_name = null,
};
}
@@ -74,6 +76,7 @@ pub const PropertyHandlerContext = struct {
.dark = .{},
.context = context,
.unused_symbols = this.unused_symbols,
.layer_name = this.layer_name,
};
}
@@ -81,6 +84,10 @@ pub const PropertyHandlerContext = struct {
this.dark.append(allocator, property) catch bun.outOfMemory();
}
pub fn setLayerContext(this: *@This(), layer_name: ?css.css_rules.layer.LayerName) void {
this.layer_name = if (layer_name) |name| name.deepClone(this.allocator) else null;
}
pub fn addLogicalRule(this: *@This(), allocator: Allocator, ltr: css.Property, rtl: css.Property) void {
this.ltr.append(allocator, ltr) catch unreachable;
this.rtl.append(allocator, rtl) catch unreachable;
@@ -155,7 +162,7 @@ pub const PropertyHandlerContext = struct {
}
if (this.dark.items.len > 0) {
dest.append(this.allocator, css.CssRule(T){
const media_rule = css.CssRule(T){
.media = MediaRule(T){
.query = MediaList{
.media_queries = brk: {
@@ -200,7 +207,26 @@ pub const PropertyHandlerContext = struct {
},
.loc = style_rule.loc,
},
}) catch bun.outOfMemory();
};
// If we have a layer context, wrap the media rule in a layer block
if (this.layer_name) |layer_name| {
dest.append(this.allocator, css.CssRule(T){
.layer_block = css.css_rules.layer.LayerBlockRule(T){
.name = layer_name.deepClone(this.allocator),
.rules = css.CssRuleList(T){
.v = brk: {
var list = ArrayList(css.CssRule(T)).initCapacity(this.allocator, 1) catch bun.outOfMemory();
list.appendAssumeCapacity(media_rule);
break :brk list;
},
},
.loc = style_rule.loc,
},
}) catch bun.outOfMemory();
} else {
dest.append(this.allocator, media_rule) catch bun.outOfMemory();
}
}
return dest;

View File

@@ -174,6 +174,22 @@ pub fn LayerBlockRule(comptime R: type) type {
try dest.writeChar('}');
}
pub fn minify(this: *This, context: *css.MinifyContext, parent_is_unused: bool) css.MinifyErr!bool {
// Save the current layer context
const saved_layer_name = context.handler_context.layer_name;
// Set the layer context for rules within this layer
context.handler_context.setLayerContext(this.name);
// Minify the rules within the layer
try this.rules.minify(context, parent_is_unused);
// Restore the previous layer context
context.handler_context.layer_name = saved_layer_name;
return this.rules.v.items.len == 0;
}
pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) This {
return css.implementDeepClone(@This(), this, allocator);
}

View File

@@ -219,8 +219,9 @@ pub fn CssRuleList(comptime AtRule: type) type {
debug("TODO: ContainerRule", .{});
},
.layer_block => |*lay| {
_ = lay; // autofix
debug("TODO: LayerBlockRule", .{});
if (try lay.minify(context, parent_is_unused)) {
continue;
}
},
.layer_statement => |*lay| {
_ = lay; // autofix

View File

@@ -7287,6 +7287,33 @@ describe("css tests", () => {
`,
{ chrome: Some(90 << 16) },
);
// Test for @layer color-scheme bug
prefix_test(
`@layer shm.colors {
body.theme-dark {
color-scheme: dark;
}
body.theme-light {
color-scheme: light;
}
}`,
`@layer shm.colors {
body.theme-dark {
--buncss-light: ;
--buncss-dark: initial;
color-scheme: dark;
}
body.theme-light {
--buncss-light: initial;
--buncss-dark: ;
color-scheme: light;
}
}`,
{ chrome: Some(90 << 16) },
);
});
describe("edge cases", () => {

View File

@@ -0,0 +1,121 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
test("@layer color-scheme should inject CSS variables within layer context", async () => {
const dir = tempDirWithFiles("layer-color-scheme-test", {
"input.css": `@layer shm.colors {
body.theme-dark {
color-scheme: dark;
}
body.theme-light {
color-scheme: light;
}
}`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "input.css", "--target=browser", "--minify"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
// The output should contain the CSS variables within the @layer block
expect(stdout).toContain("@layer shm.colors");
expect(stdout).toContain("--buncss-light:");
expect(stdout).toContain("--buncss-dark:");
// Verify that the CSS variables are within the layer context, not outside
const layerMatch = stdout.match(/@layer\s+shm\.colors\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/);
expect(layerMatch).toBeTruthy();
if (layerMatch) {
const layerContent = layerMatch[1];
expect(layerContent).toContain("--buncss-light:");
expect(layerContent).toContain("--buncss-dark:");
expect(layerContent).toContain("color-scheme:dark");
expect(layerContent).toContain("color-scheme:light");
}
});
test("@layer color-scheme should handle light dark scheme with media query in layer", async () => {
const dir = tempDirWithFiles("layer-color-scheme-media-test", {
"input.css": `@layer theme {
.element {
color-scheme: light dark;
}
}`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "input.css", "--target=browser"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
// Should contain the main layer with light theme variables
expect(stdout).toContain("@layer theme");
expect(stdout).toContain("--buncss-light:initial");
expect(stdout).toContain("--buncss-dark:");
// Should also contain a separate layer block with dark theme media query
expect(stdout).toContain("@media (prefers-color-scheme:dark)");
// The dark theme media query should also be wrapped in the same layer
const mediaQueryMatch = stdout.match(
/@layer\s+theme\s*\{[^}]*@media\s*\([^)]*prefers-color-scheme\s*:\s*dark[^)]*\)/,
);
expect(mediaQueryMatch).toBeTruthy();
});
test("color-scheme without @layer should work normally", async () => {
const dir = tempDirWithFiles("color-scheme-no-layer-test", {
"input.css": `body.theme-dark {
color-scheme: dark;
}
body.theme-light {
color-scheme: light;
}`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "input.css", "--target=browser"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
// Should contain CSS variables but no @layer wrapper
expect(stdout).toContain("--buncss-light:");
expect(stdout).toContain("--buncss-dark:");
expect(stdout).toContain("color-scheme:dark");
expect(stdout).toContain("color-scheme:light");
expect(stdout).not.toContain("@layer");
});