Skip to content

feat(bundle): add flat (unlayered) bundle option for page builders - #59

Merged
jackgranatowski merged 1 commit into
mainfrom
feat/bricks-flat-bundle
May 22, 2026
Merged

feat(bundle): add flat (unlayered) bundle option for page builders#59
jackgranatowski merged 1 commit into
mainfrom
feat/bricks-flat-bundle

Conversation

@kiro-agent

@kiro-agent kiro-agent Bot commented May 22, 2026

Copy link
Copy Markdown

This pull request was created by @kiro-agent on behalf of @jackgranatowski 👻

Comment with /kiro fix to address specific feedback or /kiro all to address everything.
Learn about Kiro autonomous agent


What

Adds a flat: true per-bundle option to bundle.config.json that strips the SLASHED @layer scaffolding before emit. First consumer is dist/slashed.bricks.css — a Bricks Builder-targeted bundle that ships unlayered CSS.

Why

Page builders that already manage the cascade via @layer (Bricks 2.0+ wraps its element defaults in @layer bricks) treat any unlayered author CSS as automatically winning over their layered rules. Shipping flat CSS to those environments is the simplest integration model — no pre-declaration file, no concat-plugin footguns, no dependency chain gymnastics.

This mirrors the deployment pattern used by Automatic.css (which moved to a "variable-first, BEM-first" unlayered model in v4.x) and Core Framework. Both ship unlayered CSS from a /wp-content/uploads/... directory and rely on Bricks' @layer bricks doing the cascade work for them.

Implementation

  • findMatchingBrace(src, openPos) — walks {} respecting CSS comments and quoted strings; returns -1 on imbalance.
  • stripLayerWrappers(content, fileLabel) — runs in two phases:
    1. Removes top-level @layer name1, name2, …; declarations (the layer-order block in core/layers.css).
    2. Unwraps each @layer slashed.X { … } block, keeping the inner CSS and dedenting two spaces.
  • Header regex anchored to start-of-line (/^@layer\b/m), so commented-out @layer text in file documentation headers cannot accidentally match.
  • Existing layered bundles are unchanged.

Verification

[bundle] → dist/slashed.optimal.css (14 files) — 140.8kB raw · 30.3kB gzip · 24.7kB br
[bundle] → dist/slashed.bricks.css  (14 files) — 135.3kB raw · 30.0kB gzip · 24.5kB br
  • grep -c '@layer' dist/slashed.bricks.min.css0 (none after minification)
  • grep -c '@layer' dist/slashed.optimal.min.css1 (the layer-order declaration, intact)
  • Brace balance walker confirms depth=0 at EOF for the flat bundle.
  • lightningcss.transform() parses the flat bundle with zero warnings.
  • Layered bundles still contain the full 15-layer @layer ...; declaration unchanged.

Out of scope

  • Documentation for the Bricks deployment recipe (enqueue snippet, bricks_is_builder_main() guard, etc.) — happy to add as docs/bricks.md in a follow-up.
  • A slashed.bricks.essential.css (smaller, no palette/forms/legacy) variant — easy to add if there's demand.

Summary by CodeRabbit

  • New Features
    • A new CSS bundle variant is now available for import as ./bricks from the package, providing an additional stylesheet distribution option.

Review Change Stack

Adds a per-bundle 'flat: true' option to bundle.config.json that strips
the SLASHED @layer scaffolding before emit. The first consumer is
dist/slashed.bricks.css, a Bricks Builder-targeted bundle.

Why: page builders that already manage the cascade via @layer (Bricks
2.0+ wraps its element defaults in @layer bricks) treat any unlayered
author CSS as automatically winning over their layered rules. Shipping
flat CSS to those environments is the simplest integration model and
matches the deployment pattern used by Automatic.css and Core Framework.

Implementation:
- findMatchingBrace() walks {} respecting comments and strings.
- stripLayerWrappers() removes top-level @layer ...; declarations and
  unwraps @layer slashed.X { ... } block wrappers, dedenting inner CSS.
- Anchored to start-of-line so commented-out @layer text in file
  headers cannot accidentally match.
- Existing layered bundles unchanged.

New artifacts:
- dist/slashed.bricks.css and .min.css (file list mirrors optimal).
- package.json exports './bricks' entry.
@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR adds flat CSS bundling support to the SLASHED build system. The bundle script gains layer-stripping utilities to remove @layer scaffolding, a new bundle configuration creates the flattened variant, and the package exports it as ./bricks for public consumption.

Changes

Flat CSS Bundle Support

Layer / File(s) Summary
Flat bundling implementation
scripts/bundle.js
Adds findMatchingBrace helper to parse CSS braces while skipping comments and quoted strings, stripLayerWrappers transformer to remove top-level @layer declarations and unwrap @layer slashed.X { ... } blocks with dedenting, and updates buildOne to conditionally apply layer stripping when flat: true option is enabled.
Bundle configuration and package export
bundle.config.json, package.json
Defines new dist/slashed.bricks.css bundle entry with flat: true selecting core stylesheets plus optional/tokens.palette.css, optional/forms.css, and optional/legacy.css; exports the bundle as ./bricks subpath in package exports map.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • codeslash-dev/SLASHED#4: Introduces the initial bundler and watch system that this PR extends with the new flat option for stripping @layer scaffolding during CSS concatenation.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding a flat (unlayered) bundle option for page builders, which is the core feature implemented across all modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/bricks-flat-bundle

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/bundle.js`:
- Around line 96-98: The current loop that strips `@layer` wrappers uses a fixed
"for (let guard = 0; guard < 32; guard++)" and may silently stop after 32
iterations leaving remaining headers; change the logic to loop until no header
match is found (e.g., while ((header = /^`@layer`[ \t]+[\w.\s,-]+\{/m.exec(out))
!== null) { ... }) or keep a safety ceiling but throw an error if the ceiling is
reached and a header still matches; update references to the guard variable, the
header regex (/^`@layer`[ \t]+[\w.\s,-]+\{/m), and the out string processing to
ensure all `@layer` wrappers are either removed or a clear exception is raised
instead of silently stopping.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0312f0ce-5b9d-467c-91fe-1df413146f61

📥 Commits

Reviewing files that changed from the base of the PR and between 69497dc and 3a04bfd.

⛔ Files ignored due to path filters (3)
  • dist/slashed.bricks.css is excluded by !**/dist/**
  • dist/slashed.bricks.min.css is excluded by !**/dist/**
  • dist/slashed.bricks.min.css.map is excluded by !**/dist/**, !**/*.map
📒 Files selected for processing (3)
  • bundle.config.json
  • package.json
  • scripts/bundle.js

Comment thread scripts/bundle.js
Comment on lines +96 to +98
for (let guard = 0; guard < 32; guard++) {
const header = /^@layer[ \t]+[\w.\s,-]+\{/m.exec(out);
if (!header) break;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid silent partial flattening after 32 @layer blocks.

The fixed guard < 32 cap can leave remaining wrappers unprocessed without error, producing a partially layered “flat” bundle. Consider iterating until no match remains (or throw when a safety ceiling is hit and matches still exist).

Proposed fix
-  for (let guard = 0; guard < 32; guard++) {
-    const header = /^`@layer`[ \t]+[\w.\s,-]+\{/m.exec(out);
-    if (!header) break;
+  for (;;) {
+    const header = /^`@layer`[ \t]+[\w.\s,-]+\{/m.exec(out);
+    if (!header) break;
     const start = header.index;
     const openBrace = start + header[0].length - 1;
     const closeBrace = findMatchingBrace(out, openBrace);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (let guard = 0; guard < 32; guard++) {
const header = /^@layer[ \t]+[\w.\s,-]+\{/m.exec(out);
if (!header) break;
for (;;) {
const header = /^`@layer`[ \t]+[\w.\s,-]+\{/m.exec(out);
if (!header) break;
const start = header.index;
const openBrace = start + header[0].length - 1;
const closeBrace = findMatchingBrace(out, openBrace);
🧰 Tools
🪛 OpenGrep (1.21.0)

[ERROR] 97-97: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/bundle.js` around lines 96 - 98, The current loop that strips `@layer`
wrappers uses a fixed "for (let guard = 0; guard < 32; guard++)" and may
silently stop after 32 iterations leaving remaining headers; change the logic to
loop until no header match is found (e.g., while ((header = /^`@layer`[
\t]+[\w.\s,-]+\{/m.exec(out)) !== null) { ... }) or keep a safety ceiling but
throw an error if the ceiling is reached and a header still matches; update
references to the guard variable, the header regex (/^`@layer`[
\t]+[\w.\s,-]+\{/m), and the out string processing to ensure all `@layer` wrappers
are either removed or a clear exception is raised instead of silently stopping.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants