Skip to content

CORE-2736: Extract the CSS colour audit engine into a publishable module - #148

Merged
RoyEJohnson merged 3 commits into
CORE-2720-global-css-theme-tokensfrom
CORE-2736-extract-css-color-audit
Sep 9, 2026
Merged

CORE-2736: Extract the CSS colour audit engine into a publishable module#148
RoyEJohnson merged 3 commits into
CORE-2720-global-css-theme-tokensfrom
CORE-2736-extract-css-color-audit

Conversation

@OpenStaxClaude

@OpenStaxClaude OpenStaxClaude commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

CORE-2736

Gets the shared half of the CSS colour audit out of tokens.spec.ts and into something REX can import, so rex-web#3133 can delete its copy. First half of converging the two audits; the REX side is CORE-2737.

Why

#143 and rex-web#3133 each added a colour audit. They are the same design implemented twice, and the duplication already cost us: Copilot's review of the REX PR found four defects, three of them in the shared engine, and they did not line up. Percentage channels were scaled by the decimal 2.55 in REX (so rgb(50%, 50%, 50%) and #808080 could never match) but not here; the hex-grammar and property-context defects were in both. Nothing would have caught the divergence, because the engine lived inside a spec file where the other copy could not reach it.

What moves

src/theme/cssColors.ts — the parsing engine: noise stripping, the declaration scanner, the colour-literal walk, describeColor, the 148-entry named-colour table, the colour-function set. tokens.spec.ts keeps everything that knows about this repo: the palette index, the file walk, KNOWN_OFF_PALETTE, the assertions.

It imports nothing at all — no fs, no path, no node built-ins, verified on the built output. That is the constraint that makes publishing it safe: it resolves through the existing "./*" wildcard subpath, which carries a browser condition in a library that is otherwise browser-only, so a node import here is something a consumer's bundler could try to follow. The file walk belongs to whoever owns the file tree.

No package.json or build-script change was needed. dist/esm/theme/cssColors.js, dist/cjs/theme/cssColors.js and a .d.ts for each are emitted, and the specs stay excluded.

One decision worth review

The published surface is REX's shape, not this repo'sdescribeColor returns structured Rgba | null rather than a pre-formatted key, declarations carries the selector context, and there is a stylesheetColors walk over both.

The ticket's plan assumed this repo's engine would be the one published. Reading both copies side by side, REX's is the superset, and the difference is not cosmetic: its baseline ratchet identifies a colour occurrence by the declaration it was written in, so it needs context from declarations. Publishing this repo's narrower version would have left REX forking declarations to get context back — the duplication this ticket exists to remove, minus the part that already worked. Adopting REX's shape makes the REX side a delete-and-import with no call-site changes.

Two small departures from REX's version, both to keep this repo's behaviour: colorKey returns the hex form (#cccccc, or #000000/0.2 when translucent) rather than comma-separated channels, because that is what an allowlist entry and a failure message have to be recognisable as; and COLOR_FUNCTIONS/COLOR_SHORTHANDS are the union of the two lists.

Behaviour

Unchanged, deliberately. Both engine defects had already been fixed in #143 by the time this was picked up, so this is extraction only — same 90 assertions over the same stylesheets, verified by diffing the test-name list before and after. The four deltas are the malformed-hex cases moving to the engine spec, and four additions covering the acceptance criteria:

  • #ggg resolves to null rather than a bogus key, and unresolvableColors is now a function of its entries so the guard can be shown to fail on a malformed palette value rather than only ever running over a sound theme.
  • animation-name: red and font-family: white stay quiet while color: red, border: 1px solid red and a named colour in a custom property are still reported — tested in both directions, here and in the engine spec.
  • rgb(100%, 100%, 100%) matches --ox-color-white, the percentage-channel bug that was REX-only.

The engine's own tests come across with it, so the published surface is the tested surface: src/theme/cssColors.spec.ts, 105 assertions.

Also here: the module field

7be1a7221 points "module" at ./dist/esm/index.js. It named index.js, which does not exist at the package root, so bundlers that skip the exports map (webpack 4, so CRA 4) fell through to main and bundled dist/cjs — CommonJS, no tree-shaking, 3 MB of REX bundle and a build over workbox's precache limit. Unrelated to the colour audit; asked for in review as item 1 of "What this needs" on rex-web#3137, and it stands on its own. types deliberately stays "index.d.ts"typesVersions rewrites it, and a full path there resolves twice. Detail in this comment.

Still stacked on #143

Based on CORE-2720-global-css-theme-tokens rather than maintokens.spec.ts does not exist on main yet. Retarget once #143 merges.

After it merges we need a version tag, since consumers reference ui-components by git tag rather than npm; the number goes on CORE-2737, which has to bump REX's reference to it. Ahead of that, branch CORE-2736-dist (ec72991ba) carries a built dist/ on top of this branch so a test tag can be cut for REX without putting build output on the PR branch. Tag it and delete it; nothing needs backing out.

🤖 Generated with Claude Code

The audit added in CORE-2720 is ~250 lines of pure CSS parsing that knows
nothing about ui-components. REX needed exactly the same thing and, because
this lived inside a spec file where nothing could import it, got a second
hand-written copy instead — and the two had diverged before either merged.

Moves the parsing engine to src/theme/cssColors.ts so it compiles into dist
and resolves as @openstax/ui-components/theme/cssColors. tokens.spec.ts keeps
the ui-components layer: the palette index, the file walk, KNOWN_OFF_PALETTE
and the assertions.

The published surface is REX's shape rather than this repo's, because REX's
is the superset: it carries the selector `context` its baseline ratchet
identifies an occurrence by, and returns structured channels rather than a
formatted key. Adopting it means the REX side is a delete-and-import with no
call-site changes; the reverse would have left REX forking `declarations` to
get `context` back, which is the duplication this ticket exists to remove.

Behaviour here is unchanged — same 90 assertions over the same stylesheets,
plus the property-context and hex-grammar cases the two copies' reviews
turned up. The engine's own tests come across with it so the published
surface is the tested surface.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The extraction is self-contained, maintains the existing consumer-facing behavior in tokens.spec.ts, and adds comprehensive unit coverage for the new published module.

Pull request overview

Extracts the shared CSS colour-audit “engine” into a publishable, dependency-free module (src/theme/cssColors.ts) so other consumers (notably REX) can reuse the same parsing and scanning logic rather than maintaining a divergent copy inside test code.

Changes:

  • Added a standalone CSS colour parsing/scanning module (cssColors.ts) that finds color literals in CSS declarations and normalizes resolvable values to RGBA.
  • Added dedicated engine tests (cssColors.spec.ts) to cover noise stripping, declaration parsing (incl. context), literal discovery, and normalization/keying.
  • Updated tokens.spec.ts to consume the extracted engine while keeping ui-components-specific policy (theme token set, allowlist, and error wording) in the spec.
File summaries
File Description
src/theme/tokens.spec.ts Switches the palette/token policy tests to use the extracted cssColors engine and adds a few contract-level cases.
src/theme/cssColors.ts New publishable CSS colour audit engine: noise stripping, declaration/context scanning, literal discovery, and color normalization/keying.
src/theme/cssColors.spec.ts New unit tests for the engine’s parsing, scanning, and normalization behavior.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

RoyEJohnson

This comment was marked as resolved.

@OpenStaxClaude

This comment was marked as resolved.

RoyEJohnson

This comment was marked as resolved.

"module": "index.js" names a file that does not exist — there is no index.js
at the package root, only dist/esm/index.js and dist/cjs/index.js. Bundlers
that read the exports map never notice, because exports already routes
browser/import to dist/esm. Webpack 4 does not read exports, so it falls
through the missing module target to main and bundles dist/cjs, and CommonJS
does not tree-shake: REX measures chunk 519 at 5.4 MB that way, over the 5 MB
workbox precache limit, versus 2.4 MB through the ESM entry.

Collateral damage of a types fix rather than an original sin. 1e4b311 (#116)
introduced the esm/cjs split with the correct "./dist/esm/index.js";
7ac2a8a (#118) rewrote module and types together in one edit. The types half
had to change — typesVersions rewrites "*" into dist/esm/, so a full path there
resolves twice — but module is not subject to typesVersions and did not.

So types stays "index.d.ts". Verified after the change: tsc resolves the
package to dist/esm/index.d.ts and the subpath REX imports to
dist/esm/theme/cssColors.d.ts, require() still gets cjs and import still gets
esm through the exports map, and a webpack-4 mainFields resolver
(['browser','module','main']) moves from dist/cjs/index.js to dist/esm/index.js.

Asked for in review, as item 1 of "What this needs" on openstax/rex-web#3137.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@OpenStaxClaude

Copy link
Copy Markdown
Contributor Author

Done — 7be1a72, checks green.

One thing worth saying plainly, since it changes what the fix is: the build already creates dist/esm/index.js, and always has. Nothing in build.bash needed touching. What was broken is the pointer — "module": "index.js" names a file that does not exist at the package root, so the ESM entry was being emitted and then never found. The commit is one line.

Verified three ways

check before after
webpack-4 mainFields resolver (['browser','module','main']) dist/cjs/index.js, CommonJS dist/esm/index.js, ESM
tsc from a consumer dist/esm/index.d.ts unchanged
require / import via exports cjs / esm unchanged

The middle row is the one that could have bitten. I resolved @openstax/ui-components and @openstax/ui-components/theme/cssColors — the subpath REX actually imports — from a consumer with --traceResolution: both still land in dist/esm/, tsc exits 0. types stays "index.d.ts" for the reason your REX writeup gives; I only moved module, which typesVersions does not touch. 476 tests, lint and typecheck pass.

Provenance is slightly sharper than "a regression": 7ac2a8a57 (#118) rewrote module and types in the same edit. The types half had to change — a full path there gets double-prefixed through typesVersions. module is not subject to typesVersions and was swept along.

Tag whenever you like — no back-out this time

Pushed CORE-2736-dist at ec72991ba: the full yarn dist output sitting on top of the fix, on its own branch instead of on the PR branch. Tag it and delete the branch; this PR stays source-only throughout, so there is nothing for you to revert. Note REX is currently pinned at test-core-2736-2, which predates the module fix.

Item 2 is still yours to call

I did item 1 only, since that is what you asked for and it stands on its own. Item 2 is a judgement call I shouldn't make unilaterally, so here is the scope: Sentry.captureReactException has exactly one call site, src/components/ErrorBoundary.tsx:119, and it already sits in a ternary whose other branch is Sentry.captureException. Dropping it is small; what it costs is the component-stack grafting on that path, for every consumer, to keep REX on an EOL runtime.

Worth knowing that item 1 makes item 2 sharper rather than softer. Per REX's own findings, the broken module field was what forced the CommonJS path and hid the missing v7 export; with the entry fixed, a Sentry 7 pin surfaces as a build error instead of a TypeError thrown the moment an error boundary catches. Say the word and I'll do the widen-and-drop as its own PR — or CORE-2853 lands first and it's moot.

I left version at 1.23.9; bumping inside a stacked branch only invites conflicts, and the release number is yours.

🤖 Generated with Claude Code

Comment thread src/theme/cssColors.ts Outdated
* Published deliberately, not incidentally. This engine knows nothing about
* ui-components; REX needed exactly the same thing and, because the first copy lived
* inside a spec file where nothing could import it, got a second hand-written one
* instead. The two had already diverged before either merged. See CORE-2736.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This block can be moved to the PR description, I don't think this context is important to keep long term

@RoyEJohnson
RoyEJohnson merged commit 4424b6f into CORE-2720-global-css-theme-tokens Sep 9, 2026
3 checks passed
@RoyEJohnson
RoyEJohnson deleted the CORE-2736-extract-css-color-audit branch September 9, 2026 18:51
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.

4 participants