Skip to content

fix(publisher): keep classes a runtime script toggles - #485

Open
borskyj-symph wants to merge 1 commit into
CoreBunch:mainfrom
borskyj-symph:fix/publish-keeps-runtime-toggled-classes
Open

fix(publisher): keep classes a runtime script toggles#485
borskyj-symph wants to merge 1 commit into
CoreBunch:mainfrom
borskyj-symph:fix/publish-keeps-runtime-toggled-classes

Conversation

@borskyj-symph

Copy link
Copy Markdown
Contributor

What

Fixes #465.

Publish tree-shakes the class registry against node class ids: collectUsedStyleRuleIds walks page and Visual Component nodes, and treeShakeStyleRules keeps a class rule only when some authored node carries it. A modifier that only exists at runtime, toggled by a script, is carried by no node, so publish treated it as dead and dropped it.

.mobilenav { display: none; }
.mobilenav--open { display: flex; }   /* no node carries this — pruned at publish */

The rule is present and correct at every point before publish. site_read_styles returns it, the canvas renders it, the stored document round-trips it. It is absent only from the published stylesheet, so the menu opens in the editor and does nothing on the live site. That shape is expensive to debug: you trust what reads back, so you start on the script, then event binding, and reach the stylesheet last.

The workaround is to apply runtime state as inline styles from script, which gives up the cascade entirely — no media queries, no pseudo-states, no theming on that state.

How

Collect the identifier-shaped runs in each script site file and treat a class whose name appears among them as used.

Splitting the source on characters a CSS class name cannot contain (/[^A-Za-z0-9_-]+/) covers classList.add, className assignment, template literals and lookup tables of state names without modelling any of them. It over-collects: add, length and every other identifier in the file land in the set, so a class named after one of them is kept even when no script really references it.

Over-collecting is the safe direction here. A false positive costs a few bytes of CSS. A false negative costs a rule that is correct everywhere until publish drops it and the feature dies in production with nothing to point at.

This can only keep more CSS than before, never less. Script-referenced ids are unioned into the used set and never subtracted, so no rule that survives today can start being pruned by this change.

Of the three options the issue proposes, this is the one that needs no author opt-in and adds no config surface. Happy to switch to a safelist or a per-rule keep flag if you would rather have an explicit mechanism.

Scope notes

  • All script files, not only runtime-enabled ones. Filtering on runtime.scripts[id].enabled would be more precise but couples this module to runtime config normalization, and a disabled script's classes surviving is harmless under the over-collect argument above.
  • usedStyleRuleIdSignature widened too, so the canvas and the publisher agree on which rules are live rather than diverging on this one point. Both callers already pass a whole site document, so the wider Pick needs no caller changes.
  • Run set cached on files-array identity, because the signature helper runs inside a canvas store selector and is hit on every store change. The store snapshot is immutable, so identity is a sufficient key.
  • Only type: 'script' files count. A class name mentioned in a doc or config file does not keep a rule alive; there is a test pinning that.

Tests

Three tests in src/__tests__/publisher/classStyleInjector.test.ts:

  • a class no node carries survives publish when a runtime script names it — fails on main
  • a class neither carried nor named by any script is still pruned, so tree-shaking is not simply disabled — fails on main
  • only script files count, not other site file types — scope guard, passes either way by construction

Checks

bun run lint and bun run build (tsc -b) clean. bun test src/__tests__/publisher/ src/__tests__/canvas/ — 782 pass, 0 fail.

The full bun test run has ~226 failures on this Windows checkout on main as well, all in collab/socket/SQLite-temp-file areas (EBUSY: resource busy or locked on temp DB cleanup). That set is unchanged by this PR.

Publish tree-shakes the class registry against node class ids, so a rule
survives only when some authored node carries its class. A modifier that only
exists at runtime, toggled by a script, is carried by no node, so publish
dropped it.

The rule is present and correct at every point before publish. site_read_styles
returns it, the canvas renders it, the stored document round-trips it. It is
absent only from the published stylesheet, so a nav that opens in the editor
does nothing on the live site and the search starts on the script and reaches
the stylesheet last.

Collect the identifier-shaped runs in each script file and treat a class whose
name appears among them as used. Splitting the source on characters a CSS class
name cannot contain covers classList.add, className assignment, template
literals and lookup tables without modelling any of them, at the cost of
over-collecting ordinary identifiers. Over-collecting is the safe direction: a
false positive costs a few bytes of CSS, a false negative costs a feature that
works everywhere except in production. Script-referenced ids are unioned into
the used set and never subtracted, so this can only keep more CSS than before,
never less.

Widen collectUsedStyleRuleIds and usedStyleRuleIdSignature to take files and
styleRules. Both callers already pass a whole site document, and routing the
canvas through the same collection keeps the editor and the publisher agreeing
on which rules are live. The run set is cached on files-array identity because
the signature helper runs inside a canvas store selector.

Fixes CoreBunch#465

Co-Authored-By: Claude Opus 5 <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.

[Bug]: Publish prunes CSS classes that are only applied at runtime by JS

2 participants