Skip to content

tailwind: scope the content scan to theme markup - #53

Merged
next-devin merged 1 commit into
mainfrom
tailwind-source-guards
Sep 2, 2026
Merged

tailwind: scope the content scan to theme markup#53
next-devin merged 1 commit into
mainfrom
tailwind-source-guards

Conversation

@next-devin

Copy link
Copy Markdown
Contributor

What

Tailwind v4 scans every non-ignored file for class names. Spark never told it otherwise, so a word in a doc or a code comment that happens to spell a utility compiles that utility into assets/main.css. Twelve were riding along:

Utility Came from
collapse docs/section-roster.md — "long tables collapse on mobile"
grayscale scripts/sass-compat.py comment, and docs/section-roster.md
invert tests/test_sass_compat.py assertions
top-24, z-40, z-60, z-70 DESIGN.md — the sticky-sidebar note and the z-layer table
ease-in, ease-in-out, ease-out DESIGN.md animation table
start, table prose

None of the twelve appears in a class attribute anywhere in the theme.

This is the failure that forced the comment reword in 7e6f446 (#52) to get the CSS drift gate green. Rewording prose does not scale: the next contributor who writes "truncate" in a docstring re-breaks the gate, and the fix is to make the scan set explicit instead.

How

css/input.css now opens with @import "tailwindcss" source(none) and names the four directories that legitimately carry class names:

@source "../templates";
@source "../partials";
@source "../layouts";
@source "../assets/js";

Two things worth flagging in review:

assets/js is a source, not an exclusion. theme.js toggles hidden, border-slate-200, and border-slate-800 at runtime, and those classes appear in no template. An allowlist that omits it compiles clean and silently breaks the mobile nav and variant selection.

There are no @source not rules. With auto-detection off, nothing outside the list is scanned, so exclusions for docs//scripts//tests/ would be dead config. The allowlist is also strictly stronger: a new stray directory can't leak, whereas a blocklist only covers the directories someone remembered to name.

The tradeoff is real and worth stating: a new directory containing class attributes now generates nothing until it is listed. test_source_list_is_exactly pins the list so that omission surfaces as a failing test rather than as missing styles on the storefront, and CLAUDE.md documents the three-step path (add @source, update EXPECTED_SOURCES, rerun make release).

Proof

tests/test_tailwind_source_guard.py plants a utility token (pt-73, used nowhere) in docs/, scripts/, and tests/, compiles, and asserts it never reaches the CSS.

Those three assertions are not self-sufficient — they also pass if the compile silently produced nothing, which is exactly the failure mode an allowlist introduces. So a positive control plants the same token in templates/ and asserts it does compile. Only the pair separates "the guard holds" from "the test is blind".

Mutation-verified. Restoring the bare @import "tailwindcss":

Ran 6 tests
FAILED (failures=4)

All three negative controls fail plus the source(none) contract test; the positive control still passes, which is the correct signature.

CI change

The test compiles CSS, so it needs ./tailwindcss. It fails with a pointer to make install-tailwind rather than skipping — a skip would retire the guard invisibly the first time the binary went missing. CI ran unittest discover before installing the CLI, so that step now runs after it. No other reordering.

Local make test on a checkout without the binary will now fail; make verify-theme already required it.

Verification

make verify-theme     56 tests OK, sass-compat clean
make css-drift        passed (48219 bytes)
settings parity       119 data values / 141 schema settings
template integrity    62 files
JS module tests       12/12

assets/main.css: 49208 → 48219 bytes, rebuilt with make release and committed alongside the source change.

🤖 Generated with Claude Code

Tailwind v4 scans every non-ignored file, so prose that happens to spell a
utility name compiles that utility into assets/main.css. Twelve were riding
along: collapse, ease-in, ease-in-out, ease-out, grayscale, invert, start,
table, top-24, z-40, z-60, z-70 — from a section-roster bullet, a sass-compat
comment, DESIGN.md's z-layer table, and test_sass_compat.py assertions. None
appear in any class attribute. This is the failure that forced the comment
reword in 7e6f446 (#52) to get the drift gate green; rewording prose does not
scale.

css/input.css now imports Tailwind with source(none) and names the four
directories that legitimately carry class names. assets/js is a source, not an
exclusion: theme.js toggles `hidden` and `border-slate-*` at runtime and those
classes appear in no template. With auto-detection off, `@source not` rules for
docs/scripts/tests would be dead config, so there are none.

tests/test_tailwind_source_guard.py plants a utility token in docs/, scripts/,
and tests/ and asserts it never reaches the compiled CSS. Those assertions also
pass if the compile silently produces nothing, so a positive control plants the
same token in templates/ and asserts it does compile — only the pair separates
"the guard holds" from "the test is blind". Verified by mutation: restoring the
bare @import fails all three negative controls and the contract test while the
positive control still passes.

The test compiles CSS, so it needs ./tailwindcss and fails with a pointer to
`make install-tailwind` rather than skipping — a skip would retire the guard
invisibly. CI ran unittest before installing the CLI, so that step now runs
after it.

assets/main.css: 49208 -> 48219 bytes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@next-devin
next-devin marked this pull request as ready for review September 2, 2026 04:52
@next-devin
next-devin merged commit 257f13c into main Sep 2, 2026
2 checks passed
@next-devin
next-devin deleted the tailwind-source-guards branch September 2, 2026 05:16
next-devin added a commit that referenced this pull request Sep 3, 2026
Bump the version markers in manifest.json, README.md, and CLAUDE.md, and
promote the Unreleased changelog section to a dated 1.3.0 heading. Adds
entries for the merged-but-unlogged changes since 1.2.0: the filter-argument
escape check (#52), the scoped Tailwind content scan (#53), and the
__pycache__ ignore (#54).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
next-devin added a commit that referenced this pull request Sep 3, 2026
)

* Render {% pixels %} in the base layout so app event trackers load

The platform injects every app's storefront event tracker (GA4, GTM,
Klaviyo, Taboola) as hidden iframes through the builtin {% pixels %} tag.
Intro Bootstrap reaches it indirectly through {% core_js %}; Spark replaced
core_js with spark-platform.js and never re-added pixels, so no tracker
iframe rendered, window.customerEventManager never existed, and no app
received any storefront event on Spark-based stores.

Add a `pixels` block before the theme script stack so the tag's fetch hook
is installed before spark-cart and friends make requests. Document the
block in the load-order contract, CLAUDE.md, and the changelog.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Release 1.3.0

Bump the version markers in manifest.json, README.md, and CLAUDE.md, and
promote the Unreleased changelog section to a dated 1.3.0 heading. Adds
entries for the merged-but-unlogged changes since 1.2.0: the filter-argument
escape check (#52), the scoped Tailwind content scan (#53), and the
__pycache__ ignore (#54).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* spark-cart: recover when the stored cart was consumed by checkout

Reproduced on aptest.29next.store on 2026-09-03 right after an order
completed: every addCartLines afterwards returned HTTP 200 with

  {"addCartLines":{"success":false,"errors":{"nonFieldErrors":
    [[{"message":"Cart not found.","code":"cart_not_found"}]]},"cart":null}}

and the shopper could not add anything until browser storage was cleared.

Two things combined. The consumed cart id stays in sessionStorage and the
storefront_cart_id cookie because the order-confirmation page is rendered by
the platform, not the theme, so nothing on the theme side ever ran to forget
it. And addToCart only recovered when the request *rejected* and the message
matched isCartExpiredError; this payload resolves, so the .then branch handed
back success:false and the create-and-retry path never ran.

- isCartNotFoundResult() recognises the resolved shape (code cart_not_found,
  or a message matching the existing expired-cart heuristic, at any depth of
  the errors object).
- addToCart settles the request into an outcome first, then recovers exactly
  once for either a rejection or a resolved cart_not_found: clear the stored
  id, createCart, retry with recover=false. A failure inside the retry cannot
  re-enter the recovery branch.
- getCart clears the stored id when the platform returns cart:null or a
  not-found error. Badge hydration calls this on the first storefront page
  after confirmation, so the stale id is gone before the shopper's next add.
- updateCartLines, removeCartLines, addVoucher, removeVoucher clear the id on
  cart_not_found without recreating (the caller is editing a cart it believes
  exists), so a stale id cannot leak into the next add.
- clearCartId() removes the sessionStorage key and expires the cookie.

Tests: the exact aptest payload on the first add asserts createCart and a
second addCartLines follow with the new id stored; a second miss returns the
platform answer without looping; getCart null/reject and each explicit-id
mutation clear the stored id and dispatch nothing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* spark-cart: only a cart_not_found code or "cart … not found" marks a resolved result

Review finding on #55: isCartNotFoundResult fell back to isCartExpiredError,
whose substring match on "invalid" would have flagged ordinary resolved
validation errors ("Invalid quantity", "Invalid voucher code") as a missing
cart and wiped a live cart id. Resolved results now count only the
cart_not_found code or a message naming the cart as not found; the broad
matcher stays on the rejection path where it started. Regression test covers
an add, a voucher, and a line update that each fail validation with "invalid"
in the message and assert no recreate and the id kept.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
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.

1 participant