Skip to content

Commit 1243030

Browse files
authored
feat: add bundled variables.css to the Tokens Studio release pipeline (#5209)
* feat: add bundled variables.css to the Tokens Studio release pipeline * refactor: ship the CSS bundle unminified and validate the guard threshold * refactor: make the bundle header path-independent and resolve the guard via import.meta.url
1 parent 28c84e5 commit 1243030

5 files changed

Lines changed: 905 additions & 8 deletions

File tree

.github/workflows/tokens_studio_release.yaml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,13 @@ jobs:
8787
# has no TS export format. See the script header for details.
8888
- name: Generate TypeScript tokens
8989
run: pnpm --filter @equinor/eds-tokens run generate:ts-tokens
90+
# Concatenate the generated CSS into one committed bundle (adds
91+
# no behaviour on top of the generated files, deliberately not
92+
# minified — ADR-0010). Importable on the beta line as
93+
# @equinor/eds-tokens/next/css/variables.css via the ./next/css/*
94+
# wildcard publish_tokens.yaml already injects.
95+
- name: Bundle CSS into variables.css
96+
run: pnpm --filter @equinor/eds-tokens run generate:css-bundle
9097
- name: Create Pull Request
9198
uses: peter-evans/create-pull-request@v8
9299
with:
@@ -98,7 +105,7 @@ jobs:
98105
# "Added" in the changelog, which is what a token update is.
99106
commit-message: 'feat: update tokens from Tokens Studio release'
100107
title: 'feat: update tokens from Tokens Studio release'
101-
body: 'Automated pull of the token state after a Tokens Studio release: raw token sets via `studio tokens pull` (sources in `packages/eds-tokens/.studio.json`), generated CSS (EDS-CSS export) into `packages/eds-tokens/src/tokens/css/`, DTCG (EDS-DTCG export) into `src/tokens/dtcg/`, and TypeScript modules generated from the two into `src/tokens/ts/`.'
108+
body: 'Automated pull of the token state after a Tokens Studio release: raw token sets via `studio tokens pull` (sources in `packages/eds-tokens/.studio.json`), generated CSS (EDS-CSS export) into `packages/eds-tokens/src/tokens/css/`, DTCG (EDS-DTCG export) into `src/tokens/dtcg/`, TypeScript modules generated from the two into `src/tokens/ts/`, and the bundled `src/tokens/css/variables.css` (concatenation of the CSS export, per ADR-0010).'
102109
branch: tokens-studio-release
103110
# This workflow runs unattended (release-triggered) and the PR it
104111
# creates gets no CI runs, so a failed pull must not be silent

packages/eds-tokens/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"test": "vitest run",
5454
"test:watch": "vitest",
5555
"generate:ts-tokens": "node scripts/generate-ts-tokens.mjs",
56+
"generate:css-bundle": "node scripts/generate-css-bundle.mjs",
5657
"prettier:check": "prettier --check src/",
5758
"build:variables:color-scheme": "build-color-scheme-variables",
5859
"build:variables:semantic:static": "build-semantic-static-variables",
Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,56 @@
1+
/**
2+
* Regression guard for published CSS bundles: no light-dark() literals,
3+
* no lightningcss polyfill markers, and both explicit color-scheme
4+
* scopes present.
5+
*
6+
* Usage: node scripts/assert-no-light-dark.mjs [file] [minSchemeScopes]
7+
*
8+
* Defaults match the 2.x build (`build/css/variables.min.css`, where
9+
* build-dark-scope widening produces at least 3 occurrences per scheme).
10+
* The Tokens Studio bundle (`src/tokens/css/variables.css`) has exactly
11+
* one block per scheme and is checked with a threshold of 1.
12+
*/
113
import { readFileSync } from 'node:fs'
214

3-
const css = readFileSync('./build/css/variables.min.css', 'utf8')
15+
const [file = './build/css/variables.min.css', minSchemeScopes = '3'] =
16+
process.argv.slice(2)
17+
const minScopes = Number(minSchemeScopes)
18+
19+
// A guard that can be silently disabled is no guard: NaN (from a
20+
// non-numeric argument) makes every `length < minScopes` comparison
21+
// false, so reject bad thresholds loudly instead
22+
if (!Number.isInteger(minScopes) || minScopes < 1) {
23+
throw new Error(
24+
`invalid minSchemeScopes argument "${minSchemeScopes}" — expected a positive integer`,
25+
)
26+
}
27+
28+
const css = readFileSync(file, 'utf8')
429

530
if (css.includes('light-dark(')) {
631
throw new Error(
7-
'variables.min.css still contains light-dark() — build-dark-scope step did not run or was skipped',
32+
`${file} still contains light-dark() — build-dark-scope step did not run or was skipped`,
833
)
934
}
1035

1136
if (/--lightningcss-(light|dark)/.test(css)) {
1237
throw new Error(
13-
'variables.min.css contains lightningcss polyfill markers — a downstream tool downleveled light-dark()',
38+
`${file} contains lightningcss polyfill markers — a downstream tool downleveled light-dark()`,
1439
)
1540
}
1641

1742
const darkScopeMatches =
1843
css.match(/\[data-color-scheme=["']?dark["']?\]/g) ?? []
19-
if (darkScopeMatches.length < 3) {
44+
if (darkScopeMatches.length < minScopes) {
2045
throw new Error(
21-
'variables.min.css is missing expected [data-color-scheme=dark] occurrences (need at least 3: original color-scheme rule + appended primitive token override + semantic token re-declaration)',
46+
`${file} is missing expected [data-color-scheme=dark] occurrences (found ${darkScopeMatches.length}, need at least ${minScopes})`,
2247
)
2348
}
2449

2550
const lightScopeMatches =
2651
css.match(/\[data-color-scheme=["']?light["']?\]/g) ?? []
27-
if (lightScopeMatches.length < 3) {
52+
if (lightScopeMatches.length < minScopes) {
2853
throw new Error(
29-
'variables.min.css is missing expected [data-color-scheme=light] occurrences (need at least 3: original color-scheme rule + widened primitive block + widened semantic block)',
54+
`${file} is missing expected [data-color-scheme=light] occurrences (found ${lightScopeMatches.length}, need at least ${minScopes})`,
3055
)
3156
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/**
2+
* Bundle the Tokens Studio CSS export into one committed file.
3+
*
4+
* Concatenates every generated CSS file under `src/tokens/css/` into
5+
* `src/tokens/css/variables.css` (ADR-0010). The bundle adds no
6+
* behaviour on top of the generated files — every mode file is
7+
* `:root`- or attribute-scoped, so plain concatenation is
8+
* conflict-free, and `var()` references are late-bound so source order
9+
* does not affect resolution. Files are still concatenated in sorted
10+
* path order to keep the committed artifact deterministic.
11+
*
12+
* The bundle is deliberately NOT minified: the committed file stays a
13+
* pure function of the source files (no toolchain-version churn in
14+
* release-PR diffs, no third-party minifier in the pipeline) and diffs
15+
* stay reviewable line by line. Gzip closes most of the size gap, and
16+
* consumers that bundle minify with their own tooling anyway.
17+
*
18+
* The directory is globbed rather than hard-coded so future changes to
19+
* the export set are followed automatically; only the output file
20+
* itself is excluded.
21+
*
22+
* On the beta line the committed bundle is importable as
23+
* `@equinor/eds-tokens/next/css/variables.css` through the
24+
* `./next/css/*` wildcard that publish_tokens.yaml injects.
25+
*
26+
* Usage: node scripts/generate-css-bundle.mjs [--css <dir>] [--out <file>]
27+
*/
28+
import { execFileSync } from 'node:child_process'
29+
import { readFile, readdir, writeFile } from 'node:fs/promises'
30+
import { join, relative, resolve } from 'node:path'
31+
import process from 'node:process'
32+
import { fileURLToPath } from 'node:url'
33+
34+
const args = parseArgs(process.argv.slice(2))
35+
const CSS_DIR = args.css ?? 'src/tokens/css'
36+
const OUT_FILE = args.out ?? join(CSS_DIR, 'variables.css')
37+
38+
// Path-independent on purpose: the committed bytes must be a pure
39+
// function of the source file contents, never of how the script was
40+
// invoked
41+
const HEADER =
42+
'/* Do not edit directly — generated by scripts/generate-css-bundle.mjs (concatenation of the Tokens Studio CSS export) */\n'
43+
44+
const files = (await readdir(CSS_DIR, { recursive: true }))
45+
.filter((file) => file.endsWith('.css'))
46+
.map((file) => join(CSS_DIR, file))
47+
.filter((file) => resolve(file) !== resolve(OUT_FILE))
48+
.sort()
49+
50+
if (files.length === 0) fail(`no CSS files found under ${CSS_DIR}`)
51+
52+
const concatenated = (
53+
await Promise.all(files.map((file) => readFile(file, 'utf8')))
54+
).join('\n')
55+
56+
await writeFile(OUT_FILE, HEADER + concatenated)
57+
58+
// Regression guard reused from the 2.x build: the bundle must contain no
59+
// light-dark() literals or lightningcss polyfill markers, and both
60+
// color-scheme scopes must be present (the new export has exactly one
61+
// block per scheme, hence the threshold of 1)
62+
execFileSync(
63+
process.execPath,
64+
[
65+
fileURLToPath(new URL('assert-no-light-dark.mjs', import.meta.url)),
66+
OUT_FILE,
67+
'1',
68+
],
69+
{ stdio: 'inherit' },
70+
)
71+
72+
console.log(
73+
`generate-css-bundle: wrote ${OUT_FILE} from ${files.length} files (${files
74+
.map((file) => relative(CSS_DIR, file))
75+
.join(', ')})`,
76+
)
77+
78+
function parseArgs(argv) {
79+
const out = {}
80+
for (let i = 0; i < argv.length; i += 2) {
81+
const key = argv[i]?.replace(/^--/, '')
82+
const value = argv[i + 1]
83+
if (!key || !value) fail(`invalid arguments: ${argv.join(' ')}`)
84+
out[key] = value
85+
}
86+
return out
87+
}
88+
89+
function fail(message) {
90+
console.error(`generate-css-bundle: ${message}`)
91+
process.exit(1)
92+
}

0 commit comments

Comments
 (0)