Skip to content

feat: add bundled variables.css to the Tokens Studio release pipeline - #5209

Merged
pomfrida merged 3 commits into
mainfrom
feat/tokens-studio-css-bundle
Jul 22, 2026
Merged

feat: add bundled variables.css to the Tokens Studio release pipeline#5209
pomfrida merged 3 commits into
mainfrom
feat/tokens-studio-css-bundle

Conversation

@pomfrida

@pomfrida pomfrida commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Closes #5201. Implements step 1 of the ADR-0010 (#5199) validation plan.

What

  • New script packages/eds-tokens/scripts/generate-css-bundle.mjs — globs src/tokens/css/ recursively (excluding only the output file itself), concatenates the files in sorted path order, and writes the committed src/tokens/css/variables.css with a do-not-edit header. Plain concatenation — the bundle adds no behaviour on top of the generated files (colour-scheme behaviour is governed by ADR-0004; no prefers-color-scheme fallback, per ADR-0013).
  • Deliberately not minified (deviation from the original ADR-0010/Add a bundled variables.css to the Tokens Studio release workflow #5201 wording "concat + minify" — the ADR text in docs: add ADR proposing a single bundled css entry for eds-tokens 3.0.0 #5199 is updated accordingly): the committed file stays a pure function of the source files — no minifier-version churn in release-PR diffs, no third-party tool in the bundle step, and diffs stay reviewable line by line (the Tokens Studio release PRs get no CI, so the human diff review is the only check). Cost is ~0.2 kB gzipped (4.8 vs 4.6 kB); consumers that bundle minify with their own tooling anyway.
  • scripts/assert-no-light-dark.mjs parameterised — optional file path and scheme-scope threshold arguments; defaults are unchanged so the 2.x _build:css chain behaves exactly as before. The threshold is validated (a non-numeric value would otherwise make the comparisons silently pass). The bundle script runs the guard on the output as a regression check (threshold 1 — the new export has exactly one block per scheme).
  • New workflow step in tokens_studio_release.yaml — runs generate:css-bundle after the TS token generation, so every Tokens Studio release PR regenerates the bundle alongside the nine source files.
  • src/tokens/css/variables.css committed now — generated from the current token state so the bundle ships in the next beta without waiting for the next Tokens Studio release. It becomes importable as @equinor/eds-tokens/next/css/variables.css through the ./next/css/* wildcard publish_tokens.yaml already injects — no publish-workflow change needed.

Notes

  • The comfortable density is scoped at :root in the bundle (not [data-density=comfortable]) because it is the base density of the saved EDS-CSS export — this mirrors the source files as-is.
  • Concat order is alphabetical by path: var() references are late-bound so order does not affect resolution; sorting only keeps the committed artifact deterministic.

Verification

  • Two consecutive runs produce byte-identical output (determinism + self-exclusion)
  • Guard passes on the new bundle and, with defaults, still passes on the 2.x build/css/variables.min.css; fails correctly on inputs containing light-dark(, missing a scheme scope, or given a non-numeric threshold
  • Bundle contains one [data-color-scheme=\"light\"] and one \"dark\" scope, compact/relaxed density scopes, and no light-dark(
  • 44.5 kB raw / 4.8 kB gzipped

@pomfrida pomfrida self-assigned this Jul 21, 2026
@pomfrida
pomfrida marked this pull request as draft July 21, 2026 14:52
@pomfrida

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Jul 21, 2026

Copy link
Copy Markdown

Claude finished @pomfrida's task in 3m 23s —— View job


PR Review — Tokens Studio CSS bundle

Reviewed the diff against origin/main and verified the assumptions in the PR description against the actual source. Solid, well-documented change — the design is sound and the doc-comments carry their weight. A few low-severity notes below; nothing blocking.

Verified claims ✅

  • Plain concat is safe — all nine source files use no @import, @charset, or @layer, so ordering/hoisting rules don't apply.
  • Concat order truly doesn't matter, even for definitions — I checked for the same --eds-* custom property being defined in more than one :root-scoped file (primitives, colors, semantic, font, density/comfortable) and found zero collisions. So the "var() is late-bound" argument holds even for the stronger case of duplicate definitions, not just references. Good.
  • comfortable at :root — confirmed density/comfortable.css starts with :root { while compact/relaxed are attribute-scoped, matching the note.
  • Self-exclusion + determinismresolve(file) !== resolve(OUT_FILE) correctly drops the output from its own input, and .sort() keeps it deterministic.

Findings

1. assert-no-light-dark.mjs — a non-numeric threshold silently disables the scope checks (low)
packages/eds-tokens/scripts/assert-no-light-dark.mjs:17

const minScopes = Number(minSchemeScopes)

If minSchemeScopes is ever non-numeric, Number(...) is NaN, and every length < NaN comparison is false — so the light/dark scope assertions pass silently. This is a guard whose entire job is to fail loudly, so a validation guarding against NaN (and < 0) is worth it even though the only current caller passes the literal '1'.
Fix this →

2. Committed artifact stability is tied to an unpinned lightningcss range (low / observation)
generate-css-bundle.mjs minifies via the lightningcss-cli devDep declared as ^1.32.0 in package.json. Because variables.css is committed, a future minor/patch bump of lightningcss that changes minification output would surface as a large, surprising diff on the next Tokens Studio release PR (unrelated to any token change). Not a bug — but if byte-stability of the committed bundle matters, consider whether the caret range is intended here, or note that bundle churn on toolchain bumps is expected.

3. Glob follows stale files if the export set ever shrinks (nit)
The directory-glob approach (deliberately chosen, per the header) means that if a future EDS-CSS export removes a source file, a stale copy left on disk in the release checkout would still be concatenated into the bundle. The current pre-existing export step (studio exports run … --out src/tokens/css) governs cleanup, so this is out of scope for this PR — just flagging that the bundle's contents are "whatever .css is on disk," which is worth keeping in mind.

4. parseArgs is pairs-only (nit)
generate-css-bundle.mjs:80 steps i += 2 and rejects any odd/valueless argument. Fine for the documented --css <dir> --out <file> flag form (and CI passes no args), but it will reject a bare flag or positional arg with a generic message. Acceptable given the documented usage; noting only for completeness.

Minor

  • The lightningcss call spawns pnpm exec lightningcss (generate-css-bundle.mjs:51) while the guard is invoked via process.execPath directly (:69). Both work under pnpm --filter … run; the mild inconsistency is harmless.
  • Script correctness depends on cwd being the package root (relative CSS_DIR default and the relative scripts/assert-no-light-dark.mjs path). That's guaranteed by the pnpm --filter @equinor/eds-tokens run invocation in the workflow, so no action needed.

Overall: clean, minimal, and the risky-looking parts (concat ordering, self-exclusion, scheme scopes) all check out against the real files. 👍


  • Read changed files
  • Review generate-css-bundle.mjs
  • Review assert-no-light-dark.mjs changes
  • Review workflow + package.json
  • Post review

@pomfrida

Copy link
Copy Markdown
Collaborator Author

@claude review

@pomfrida
pomfrida marked this pull request as ready for review July 22, 2026 09:16
@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude finished @pomfrida's task in 2m 28s —— View job


PR Review — Tokens Studio CSS bundle (follow-up)

Re-reviewed the current branch state. The two low-severity findings from the previous round are both resolved, and I verified the key behaviours by actually running the scripts rather than reading only. Clean change — nothing blocking.

Previous findings — resolved ✅

  1. NaN threshold silently disabling scope checks → fixed. assert-no-light-dark.mjs:22 now validates with !Number.isInteger(minScopes) || minScopes < 1 and throws loudly. Verified: abc and 1.5 and 0 all reject; 1 passes silently.
  2. Committed artifact tied to unpinned lightningcss → resolved by design change. The bundle is now plain concatenation (no minifier in the step), so the committed file is a pure function of the source files with no toolchain-version churn. Good call, and well-documented in the script header + PR body.

Verified this round

  • Byte-reproducible. Ran pnpm --filter @equinor/eds-tokens run generate:css-bundle against the committed state → zero drift (git status clean). Concatenates 9 files in sorted path order, self-excludes variables.css.
  • Guard passes on the real bundle at threshold 1, and fails correctly at threshold 2 (scope-missing error).
  • Scheme scopes as claimed: exactly one [data-color-scheme="dark"] (line 2) and one [data-color-scheme="light"] (line 102), zero light-dark( literals.
  • Workflow + package.json wiring is correct: generate:css-bundle script added, and the Bundle CSS into variables.css step runs after TS generation in tokens_studio_release.yaml. The PR body copy is updated to mention the bundle.

Findings

1. The do-not-edit header text is a function of the invocation path, not just content (low / observation)
generate-css-bundle.mjs:37 interpolates CSS_DIR into the header:

const HEADER = `/* Do not edit directly … (concatenation of ${CSS_DIR}/) */\n`

With the default args CI uses this is stable (src/tokens/css/), so the committed file stays reproducible. But it means the artifact is only byte-identical when regenerated with the same cwd + relative --css. I hit this during review: regenerating with an absolute/differently-spelled --css path rewrites the header line and shows as spurious drift, even though the token content is unchanged. Since the bundle's whole selling point is "pure function of the source files," consider making the header path-independent (e.g. a fixed src/tokens/css/ literal, or relative(process.cwd(), CSS_DIR) normalised) so the committed bytes depend only on content. Purely defensive — the current CI invocation is fine.

2. Guard invocation depends on cwd being the package root (nit, no action needed)
generate-css-bundle.mjs:59 spawns the guard via the relative path scripts/assert-no-light-dark.mjs. Confirmed by running the script from the repo root: the concat succeeds but the guard sub-process throws Cannot find module …/scripts/assert-no-light-dark.mjs. Guaranteed correct under the pnpm --filter @equinor/eds-tokens run … invocation the workflow uses (cwd = package root), so no change required — flagging only because the failure mode is a confusing "module not found" rather than a clear "run me from the package root."

3. parseArgs is still pairs-only (nit, unchanged from last round)
generate-css-bundle.mjs:71 steps i += 2 and rejects any valueless/odd argument. Fine for the documented --css <dir> --out <file> form and CI passes no args; noting only for completeness.

Note

  • No unit test covers these build scripts, and (per the PR body) the Tokens Studio release PRs get no CI — so the embedded guard is the only automated check on the generated bundle. That's a reasonable design given the constraints; the guard is doing real work and its threshold is now validated. Just worth keeping in mind that the human diff review remains the primary gate for these release PRs.

Overall: the risky-looking parts (concat ordering, self-exclusion, scheme scopes, threshold validation) all check out against the real files and a live run. 👍

  • Read changed files
  • Review generate-css-bundle.mjs
  • Review assert-no-light-dark.mjs
  • Review workflow + package.json + committed bundle
  • Verify reproducibility + guard behaviour by running the scripts
  • Post review

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.

Add a bundled variables.css to the Tokens Studio release workflow

1 participant