Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions .changeset/9569-components-stylesheet-source-none.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
'@object-ui/components': minor
'@object-ui/fields': minor
'@object-ui/plugin-grid': minor
'@object-ui/plugin-kanban': minor
---

fix(components): Tailwind no longer compiles this package's prose into the published stylesheet

`@object-ui/components`' Tailwind entry opened with a bare `@import 'tailwindcss'`
— the only one of this repository's four stylesheet entries without a
`source(none)` pin. Tailwind v4's automatic source detection was therefore ON,
and it roots at the PROCESS working directory, so it scanned every non-ignored
file in the package: `CHANGELOG.md`, `README.md`, the markdown beside the
renderers, `shadcn-components.json`, even an English sentence in a comment inside
`tsconfig.test.json`. Every class-shaped token in that prose compiled into a real
rule in the published `dist/index.css`.

The entry now reads `@import 'tailwindcss' source(none);`. It keeps the FULL
Tailwind import, because this is the root sheet: preflight, the `@theme` block
and the base layer are emitted exactly as before. Only automatic detection is
off, so the entry's own `@source` lines are the whole input set — and the
compiled sheet is now byte-identical whatever directory the build is launched
from, which it previously was not.

## Breaking: 23 prose-derived utilities leave `@object-ui/components/style.css`

None of these is emitted by any component under this package's `src`; each one
existed solely because some prose in the package mentioned it. Grouped by the
file that was creating it:

- from `CHANGELOG.md` — `cursor-not-allowed`, `flex-shrink-0`, `lg:p-8`,
`md:text-2xl`, `origin-[--radix-...]`, `pb-20`, `scale-0`, `sm:px-6`,
`sm:py-4`, `space-y-8`, `w-[--sidebar-width]`, `xl:flex`
- from `README.md` — `bg-blue-500`, `hover:bg-blue-700`, `text-white`
- from `README_SHADCN_SYNC.md` — `dark:backdrop-blur-sm`, `dark:bg-background/95`
- from the JSON examples in the renderer docs — `bg-blue-50`, `bg-gray-50`,
`h-[600px]`, `h-[800px]`
- from `shadcn-components.json` — a variant of `origin-[--radix-` ending in a
typographic ellipsis
- from a prose comment in `tsconfig.test.json` — `invert`, which is an ordinary
English word there

Two of them are worth calling out. `flex-shrink-0` is a deprecated Tailwind v3
alias this repository deliberately migrated away from, and it was shipping only
because the changelog entry announcing that migration names it.
`w-[--sidebar-width]` and the `origin-[--radix-` pair are the v3 bracket syntax,
shipping only because the changelog entry recording the move to v4 syntax quotes
the old spelling. A consumer who had come to rely on any of the 23 should add it
to their own Tailwind sources; they were never this package's to publish.

## Fixed: three sibling packages get back classes their own components emit

The plugin stylesheets subtract every rule `@object-ui/components` already
ships, so any class the prose invented was subtracted out of THEIR sheets while
their components still emitted it — under-styled published packages with a green
build. Restored by this change:

- `@object-ui/plugin-kanban` — `h-[600px]`, `sm:px-6`, `sm:py-4`
- `@object-ui/plugin-grid` — `bg-blue-50`, `cursor-not-allowed`
- `@object-ui/fields` — `bg-blue-50`, `bg-blue-500`, `bg-gray-50`, `text-white`

The same mechanism was also a loaded gun for the release lane: `changeset:version`
writes changeset bodies into `packages/components/CHANGELOG.md`, so a changeset
that merely QUOTED a class name promoted it to a real utility in this sheet and
stripped it out of the plugin sheets at the next release. That is no longer
possible for these four packages — which is why this body can safely quote 23
class names.

## Why `minor` rather than `patch`

Removing utilities from a published stylesheet is a contraction of a published
artifact, so it carries a breaking semantic. This repository does not ship
`major` outside the `@objectstack` major-sync release, and marks its own breaking
changes `minor` with the break spelled out — which is what the section above is.
6 changes: 6 additions & 0 deletions .github/workflows/changeset-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1078,6 +1078,12 @@ jobs:
# runner time daily for a PR nobody reads until release day. That is the
# cost objectstack#10850 was closed to REMOVE, walking back in.
#
# ⚠️ "SOURCE" ABOVE MEANS `.ts`/`.tsx`/config AND NOT EVERY BUILD INPUT: a
# generated `CHANGELOG.md` IS a Tailwind source input for any package whose
# stylesheet entry scans it, which is how a changeset body that merely
# QUOTED a class name used to compile that class into a published sheet and
# subtract it back out of the plugin sheets (objectui#9569).
#
# So the validation is scoped to what the diff can actually move, and the
# union — `ci.yml` on the source, these steps on the version outputs — is
# the whole post-version tree.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
*/

/**
* `src/index.css` must not scan this package's own TEST files — objectui#8446.
* `src/index.css` must not scan this package's own TEST files (objectui#8446),
* nor its own PROSE (objectui#9569).
*
* ## What was wrong
*
Expand Down Expand Up @@ -38,15 +39,29 @@
* production-sourced utility and a floor on the rule count, so an empty or
* gutted sheet fails.
*
* ## Why `base` is passed explicitly
* ## Why `base` is still passed explicitly
*
* `src/index.css` opens with a bare `@import 'tailwindcss'`, so Tailwind's
* automatic source detection is ON and resolves against the PROCESS CWD. Vitest
* runs from the repo root while `pnpm build` runs from this package directory,
* and the two produce different stylesheets from the same bytes (measured:
* 3430 rules vs 1385). Pinning `base` to the package root makes this reading
* reproduce the artifact the BUILD produces, from either working directory.
* The CWD dependence itself is out of scope here and reported separately.
* It no longer has to be. `src/index.css` used to open with a bare
* `@import 'tailwindcss'`, leaving automatic source detection ON and rooted at
* the PROCESS CWD: vitest runs from the repo root while `pnpm build` runs from
* this package directory, and the two compiled different stylesheets from the
* same bytes. That entry now carries `source(none)` (objectui#9569), so nothing
* is detected automatically and this reading is CWD-independent by construction
* -- `base` governs only the automatic root, never the `@source` lines, which
* resolve against the entry stylesheet's own directory. It is kept because it
* costs nothing and still pins this reading to the directory the build runs in
* if that entry ever loses the `source(none)`.
*
* ## The prose half (objectui#9569)
*
* Automatic detection scanned every non-ignored file in the package, so class
* names quoted in `CHANGELOG.md`, `README.md` and the docs beside the renderers
* compiled into the published sheet. `changeset:version` writes changeset bodies
* into `CHANGELOG.md`, which made that a live channel from a release note into
* `dist/index.css` -- and, through the plugin builders' subtraction, out of the
* sibling packages' sheets. The second test below pins the door shut with its
* own lit control, because a negative assertion about a token nothing mentions
* any more would pass for the wrong reason.
*/
import { readFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
Expand All @@ -73,6 +88,12 @@ const SENTINEL = 'mt-[3.7331px]';
*/
const SENTINEL_VALUE = '3.7331px';

/**
* Named by this package's `CHANGELOG.md` and by no shipped source in it. Under
* the pre-objectui#9569 entry this compiled a real rule into `dist/index.css`.
*/
const PROSE_ONLY_CLASS = 'flex-shrink-0';

async function compilePublishedStylesheet(): Promise<{
css: string;
selectors: Set<string>;
Expand Down Expand Up @@ -100,4 +121,22 @@ describe('packages/components/src/index.css @source scan', () => {
expect(SENTINEL).toContain(SENTINEL_VALUE);
expect(css).not.toContain(SENTINEL_VALUE);
}, 60_000);

it("does not compile a class that only this package's CHANGELOG prose names", async () => {
const changelog = await readFile(resolve(packageRoot, 'CHANGELOG.md'), 'utf8');

// CONTROL — the door this asserts is shut must still exist. `CHANGELOG.md`
// is append-only, so an entry that once named this class keeps naming it; if
// this ever goes red the probe needs re-pointing at a token the file does
// carry, NOT deleting.
expect(changelog).toContain(PROSE_ONLY_CLASS);

// `flex-shrink-0` is the deprecated Tailwind v3 alias this repo migrated
// away from, so no shipped source emits it — compiling it would not merely
// be waste, it would be wrong. The only reason it was ever in the published
// sheet is the changelog entry announcing that migration.
const { css, selectors } = await compilePublishedStylesheet();
expect(selectors.has(`.${PROSE_ONLY_CLASS}`)).toBe(false);
expect(css).not.toContain(`.${PROSE_ONLY_CLASS}`);
}, 60_000);
});
51 changes: 50 additions & 1 deletion packages/components/src/index.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,40 @@
@import 'tailwindcss';
/*
* `source(none)` is load-bearing, not tidiness.
*
* This entry is the ROOT sheet, so it keeps the FULL `@import 'tailwindcss'`:
* preflight, the `@theme` block and the base layer are this package's to emit.
* The narrow siblings (`fields`, `plugin-grid`, `plugin-kanban`) deliberately
* emit none of those and therefore spell their entry differently -- `@reference`
* on this file plus `@import 'tailwindcss/utilities.css' layer(utilities)
* source(none)`. Read `packages/fields/src/index.css`'s own header before
* copying anything from it: it states "This file has no `@import 'tailwindcss'`
* on purpose". Mirroring the siblings literally here would drop preflight and
* the shared theme from the only sheet that ships them.
*
* What `source(none)` switches off is Tailwind's AUTOMATIC source detection,
* which roots at the PROCESS CWD. Two defects followed from leaving it on:
*
* 1. The published artifact depended on where the build was launched from --
* same commit, same command, two different stylesheets. The ratio measured
* on objectui#9569: compiling with the cwd at this package emitted roughly a
* third of the classes that compiling from the repo root emitted.
* 2. It scanned this package's own `CHANGELOG.md` -- 473 kB of published prose
* -- and compiled every class-shaped token in it into `dist/index.css`.
* That is worse than dead weight: `pnpm changeset:version` WRITES changeset
* bodies into that CHANGELOG, so a changeset that merely quotes a class name
* turned it into a real utility here, and the plugin builders subtract every
* rule this sheet already ships -- silently stripping that class out of
* THEIR published sheets while their components still emit it (objectui#9569).
* objectui#8446 already tried the narrow route on this same file, enumerating
* bad doors with `@source not` while leaving detection ON; `CHANGELOG.md` is
* the door it did not enumerate. Enumerating doors is what failed. The door
* list is unbounded -- any published file in this package is a door -- so
* detection is off and the inputs are declared instead.
*
* With it, the ONLY inputs are the explicit `@source` lines below, and the
* output is byte-identical from any working directory.
*/
@import 'tailwindcss' source(none);

/* Class-based dark variant: follow `.dark` on <html>/ancestor instead of
the OS `prefers-color-scheme`, so the in-app theme toggle controls UI.
Expand All @@ -10,6 +46,19 @@
@custom-variant dark (&:where(.dark, .dark *));

/* Scan sources for Tailwind classes.
*
* Since the entry above carries `source(none)`, these three lines are now the
* ONLY thing that keeps this sheet non-empty -- there is no automatic detection
* left to cover for a path that names nothing. Every path is resolved relative
* to THIS FILE's directory: `@tailwindcss/postcss` compiles with its base set to
* the entry stylesheet's own directory, so `../src/**` and `./**` below both
* land on `packages/components/src`. `packages/runner/src/index.css` records the
* failure mode in detail -- five `@source` lines there were each wrong by one
* `src` segment, named directories that do not exist, and contributed nothing,
* while automatic detection quietly kept that sheet populated. A near-empty
* sheet and a correct one both build green, so the lines below were re-measured
* by ablation on objectui#9569 rather than assumed: deleting the first one
* collapses this compilation to its base layer.
*
* Only SHIPPED source. Tailwind v4 scans source TEXT, so any class-shaped
* token in a file that is scanned compiles a real rule into the published
Expand Down
11 changes: 11 additions & 0 deletions scripts/markdown-test-inputs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,17 @@ export const ADJUDICATED = new Map([
reads: ['content/docs/guide/schema-rendering.md', 'packages/react/README.md'],
},
],
// objectui#9569. This one reads a CHANGELOG as a CONTROL, not as a spec: the
// test asserts a class named only by that prose is absent from the compiled
// stylesheet, and reads the file to prove the prose still names it -- so the
// negative assertion cannot pass because the token quietly went away.
[
'packages/components/src/__tests__/index-css-scan-excludes-tests.test.ts',
{
reads: ['packages/components/CHANGELOG.md'],
notRead: ['CHANGELOG.md'],
},
],
[
'packages/components/src/__tests__/page-body-single-node-8310.test.tsx',
{
Expand Down
Loading