fix(plugin-grid,plugin-kanban): publish the stylesheet their themed utilities need - #6403
Merged
os-support-ai merged 4 commits intoAug 25, 2026
Merged
Conversation
…tilities need Only @object-ui/components and @object-ui/fields shipped CSS, and each scans its own `src` only, so a class used exclusively by a plugin could not appear in either sheet by construction. A published-state app that installed one of these two plugins and followed the quick-start rendered the grid or the board with 25 themed utilities that had no source anywhere — `bg-muted/10`, `bg-card/60`, `text-muted-foreground/60` and friends — plus ~103 plain ones. Each package now gains `src/index.css` and `scripts/build-css.mjs`, plus a `"./style.css": "./dist/index.css"` export, built in the subtraction shape @object-ui/fields established: `@reference` the components entry so the compilation gets its theme tokens, class-based `dark` variant and animate plugin without emitting a byte of them, then subtract every rule that package's published sheet already ships. 16.30 kB and 11.41 kB rather than ~170 kB each. The build step is shared (scripts/build-plugin-stylesheet.mjs) so the next plugin inherits the pattern instead of copying it, and it refuses to write a sheet that fails any of its four assertions. objectui#4929, maintainer ruling 2026-08-17 (Direction 1).
…tion run disproved The entry headers said a plain `@import 'tailwindcss'` entry "would re-emit preflight, the theme and the ~1350 utilities the consumer already got". Measured on the sibling plugin, it does not: the subtraction removes those too, and the published artifact comes out the same size (11.26 kB vs 11.41 kB). The narrow entry is still the shape we use, for the reason that survives the measurement — it makes a `:root` theme block impossible to emit at all, and such a block loaded after the base sheet would override a consumer's own token overrides. The comments now say that instead of the claim that is not true.
…gin-published-stylesheets
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…and hash it Three gates on the first push, all of them about the new build step and all of them pointing the same way — a build step nobody hashes, or one that silently decides not to run, is the stale-or-absent stylesheet this card is about. 1. `check:entry-guard` — `scripts/build-plugin-stylesheet.mjs` hand-typed its own `process.argv[1]` entry guard. Node leaves `argv[1]` as the caller typed it, so a script reached through a symlink compares two different paths, answers false and does nothing with exit 0. Both wrappers now call `isEntrypoint(import.meta.url)` from `scripts/invoked-as.mjs`, the one predicate in this tree, and the shared module no longer carries a guard. `KNOWN_HAND_TYPED_GUARDS` untouched — it is shrink-only. 2. `scripts/__tests__/turbo-build-inputs.test.ts` — the two plugins' build programs read `scripts/build-plugin-stylesheet.mjs` and, after fix 1, `scripts/invoked-as.mjs` from outside their package directories, which turbo was not hashing. Both are added to `turbo.json`'s `build` inputs, which tightens the cache key. Verified derived rather than guessed: dropping either line reds the gate with that exact path named. 3. `scripts/__tests__/package-files-exist.test.ts` — the exports-surface discovery pin listed the two packages that ship a generated `style.css`. It now lists four. That list records reality and grew because the capability grew (maintainer ruling 2026-08-17 on objectui#4929, Direction 1, scoped to plugin-grid + plugin-kanban); the comment beside it carries the old value, the clause that moved it, and the direction that still means a defect. Behaviour of the guard checked in both directions rather than inferred: with both `dist/index.css` deleted, importing the wrappers (the test suite) leaves them absent, and `turbo run build` recreates them at 16696 and 11684 bytes.
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
os-support-ai
marked this pull request as ready for review
August 25, 2026 20:57
os-support-ai
deleted the
claude/issue-4929-plugin-published-stylesheets
branch
August 25, 2026 21:12
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4929
Maintainer ruling 2026-08-17, Direction 1: each of the two plugins with measured classes gains the
scripts/build-css.mjsstep and a"./style.css": "./dist/index.css"export, built in the subtraction shape@object-ui/fieldsestablished (objectui#4059).What was broken
@object-ui/componentsand@object-ui/fieldswere the only packages shipping CSS, and each scans its ownsrconly — so a class used exclusively by a plugin could not appear in either sheet by construction. A published-state Vite app that installed@object-ui/plugin-grid/plugin-kanbanand followed the quick-start rendered the grid or the board with themed utilities that had no producer anywhere in the world: they resolve@themetokens declared inpackages/components/src/index.css, which that package does not publish.Re-derived measurement — the card's numbers vs this tree
The card measured at
40d3a3318by literal grep over source. I re-derived on the merged tree by compiling each package's entry and diffing the emitted classes against both published sheets, which is the same question asked of the artifact rather than of string literals.40d3a3318, grep)All 21 of the card's themed utilities still hold — each is used by one of the two plugins and is in neither published sheet. Four more turned up that a literal grep cannot see:
[&>h3]:text-foreground/80(arbitrary variant),border-l-primary/40,border-primary/30,hover:text-primary. The plain count is lower than the card's because the two methods count different things: a compiled diff drops literals that never resolve to a rule and dedupes variants, and it credits the base sheets with every class they actually emit rather than with the strings that happen to appear in their source. The premise is intact — the themed count is 25, not zero.The shape, and the division of labour between its two halves
Each package gains
src/index.css, which@references the components entry — theme tokens, the class-baseddarkvariant and thetailwindcss-animateplugin become available for resolution while emitting nothing — andscripts/build-css.mjs, which subtracts every rule components' built sheet already ships.plugin-gridplugin-kanbanThe build step is shared —
scripts/build-plugin-stylesheet.mjsat the repo root, with a thin per-package wrapper holding only that package's sentinels and ceiling — because the ruling says the step is "the pattern any future plugin inherits", and a pattern that has to be copy-pasted is not inherited. Repo-root modules imported by a package's build are existing practice here (scripts/vite-dts-*.ts).postcss/@tailwindcss/postcssare injected by the wrapper rather than imported at the root, so each package declares exactly what it imports.packages/fields/scripts/build-css.mjsis deliberately left alone: rewriting a published package's build belongs in its own change with its own byte-for-byte verification. Filed as #6405.Tests, and the ghost-assertion guard
scripts/__tests__/plugin-published-stylesheet.test.ts— 14 assertions, node project, ~2.6 s. It compiles rather than readingdist/, because CI runs the suite on an unbuilt worktree; a test that read the artifact would pass vacuously or skip.Against unmodified
origin/main(faa863dce), the test file copied into a clean worktree:That is the defect itself — on
mainthere is no build step and no sheet to assert about. Because an import-level failure proves only that the subject is absent, every assertion group was then driven red individually by mutating the fix on this branch. Baseline first, then one leg per group; each mutation was proved on disk by grep count and blob hash, and each restore bygit checkout HEAD --on the path plus a byte-identical hash and a cleangit diff HEAD:@reference+ empty the builder's ownMUST_SURVIVEflex"./style.css"fromplugin-grid/package.jsonsource(none)from the grid entry:root { --color-ghost-4929 }block into the grid entry@referenceonlyDegenerate control.
flex,text-sm,rounded-md,bg-background,sr-onlyare all carried by components' sheet. The test asserts each is present in the plugin's pre-subtraction compile and absent from the published sheet — so the absence cannot be explained by the plugin never using the class, and "the sheet has the class" cannot pass for the subtraction working. Under thesubtractionmutation this is exactly what goes red:expected [ 'flex' ] to deeply equal [].One honest negative result. A sixth leg — swapping the narrow entry for a plain
@import 'tailwindcss'— left the suite green. That is not a ghost: the subtraction removes the duplicated preflight and theme too, and the published artifact comes out the same size (11.26 kB vs 11.41 kB). The narrow entry stays because it makes a:roottheme block impossible to emit at all, and such a block loaded after the base sheet would override a consumer's own token overrides. The in-source comments originally claimed the wide entry would ship ~170 kB of duplication; that claim was measured, found false, and corrected in commit 2 rather than left standing.An earlier run of this same matrix was thrown away: every leg exited 1 through
--reporter=basic, which vitest 4 does not have, so the whole matrix had measured nothing while reading as five confirmations. The table above is from the re-run, and the baseline row exists so a non-zero exit means an assertion.CI came back red three times on the first push (
372bb5d61) — what changedAll three were this PR's own, all three in code this PR adds, and all three point the same way as the card itself: a build step nobody hashes, or one that silently decides not to run, is the stale-or-absent stylesheet #4929 is about. Each was reproduced locally, fixed, and shown green before this second push.
1.
Lint→check:entry-guard.scripts/build-plugin-stylesheet.mjshand-typed its own entry guard (process.argv[1], twice in one expression — hence the gate's two reports at one line). Node leavesargv[1]as the caller typed it, so a script reached through a symlink compares two different paths, answers false and does nothing, with exit 0 and no output — a silent no-op build step. Both wrappers now callisEntrypoint(import.meta.url)fromscripts/invoked-as.mjs, the one predicate in this tree, and the shared module carries no guard at all.KNOWN_HAND_TYPED_GUARDSis untouched; it is shrink-only.The guard was then checked in both directions rather than inferred, because its failure mode is invisible. With both
dist/index.cssdeleted: importing the wrappers (the whole test suite does) leaves them absent, andturbo run buildrecreates them at 16696 and 11684 bytes.2.
Test (shard 1/4)→turbo-build-inputs. The two build programs readscripts/build-plugin-stylesheet.mjs— and, after fix 1,scripts/invoked-as.mjs— from outside their package directories, which turbo was not hashing; it would replay a stale verdict and hand a staledist/to everything downstream. Both paths are now inturbo.json'sbuildinputs, which tightens the cache key. Derived, not guessed: dropping either line reds the gate naming that exact path.3.
Test (shard 4/4)→package-files-exist, the exports-surface discovery pin. This one moves a pin, so it carries its three parts.Old value, verbatim:
New value, verbatim:
The clause that moves it — the maintainer ruling of 2026-08-17 on #4929:
The list grew because the ruling grew the capability, not because it was in the way: it is a discovery assertion ("guard cannot pass by finding nothing"), so it records which packages ship a generated CSS export, and leaving it at two would make the guard assert a world that no longer exists. The comment beside it now carries all three parts plus the direction that still means a defect — shrinking this list means a package stopped producing a sheet it still promises, which is objectui#4059 returning.
Verification, all at
820631805(the pushed HEAD)turbo run build --filter=plugin-grid --filter=plugin-kanban, both sheets deleted first✓ built dist/index.css (16.30 kB)/(11.41 kB), files back on diskvitest run packages/plugin-grid packages/plugin-kanban scripts/__tests__pnpm lint(full turbo lint, 47 tasks) pluslint:rootand both packages'linttype-check:scripts,plugin-grid+plugin-kanbantype-check--listFilesconfirms all four new files are in the scripts programcheck:entry-guard(--self-testand the real run) ·check:control-bytes·check:doc-fences·check:doc-types·check:phantom-deps·docs:check-links·changeset:check·lint:coverage·type-check:coverage·check-changeset-presence·check-cross-repo-closer-outcomeNot measured locally, named rather than glossed:
check:doc-snippetsexits 2,PRECONDITION NOT MET— it needs most packages built, which this worktree is not; it says nothing either way and CI runs it.check:readme-exportsfails on an unbuilt worktree for ~20 other packages ("type entry not on disk — runpnpm buildfirst") with zero findings against the two packages touched here.Docs
The one-line import per plugin is added to the quick-start, theming, troubleshooting and plugins guides, to both plugin pages and to both package READMEs. The objectui#4858 correction — do not scan
node_modules, do not hand-declare@theme— is untouched and stays true: it now holds for plugins too, because the sheets exist. Every doc addition says explicitly that only these two plugin packages publish a sheet, so nobody imports a subpath that is not there.Reported, not fixed here
packages/cli/src/utils/app-generator.ts:335still restates the@themeblock token-for-token and scansnode_modules/@object-ui/*/dist. These sheets make part of that removable, but not all of it: the temporary app supports every plugin, and the other@object-ui/plugin-*packages still publish no stylesheet. Filed as objectui run's temporary app restates the components @theme token-for-token; the new plugin sheets make it only partly removable #6404, not touched here.packages/fields/scripts/build-css.mjsnow duplicates logic that lives in the shared builder. Filed as packages/fields/scripts/build-css.mjs duplicates the shared plugin stylesheet builder added for #4929 #6405.plugin-kanban's sheet keeps a 2.7 kB@layer propertiesblock: 74 of its 75--tw-*declarations are byte-identical to components' and the 75th (--tw-scroll-snap-strictness, fromsnap-mandatory) is not, so the rule is not identical and rule-level subtraction cannot drop it. Idempotent, and left as-is rather than moving the subtraction to declaration level.⛔ Draft on purpose — the PM lands it.
Generated by Claude Code
Generated by Claude Code