@massimo-cassandro/minimo 1.33.7 → 1.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/design-tokens/README.md +2 -197
- package/dev-tools/README.md +1 -0
- package/dev-tools/design-tokens-builder/README.md +212 -0
- package/dev-tools/design-tokens-builder/build-tokens-src/build-source-modes.mjs +155 -0
- package/{design-tokens/utilities → dev-tools/design-tokens-builder}/build-tokens-src/config.mjs +35 -1
- package/{design-tokens/utilities → dev-tools/design-tokens-builder}/build-tokens-src/formats/css.mjs +16 -4
- package/{design-tokens/utilities → dev-tools/design-tokens-builder}/build-tokens-src/formats/json.mjs +9 -4
- package/dev-tools/design-tokens-builder/build-tokens-src/legacy-tokens-parser.mjs +105 -0
- package/dev-tools/design-tokens-builder/build-tokens-src/merge-css.mjs +192 -0
- package/{design-tokens/utilities → dev-tools/design-tokens-builder}/build-tokens-src/platforms.mjs +3 -1
- package/dev-tools/design-tokens-builder/build-tokens.mjs +219 -0
- package/{design-tokens/utilities → dev-tools/design-tokens-builder}/check-unresolved-custom-props.mjs +70 -13
- package/{design-tokens → dev-tools/design-tokens-builder}/tokens-config-sample.mjs +55 -3
- package/dev-tools/starter-kit/source_files/root/__project__.code-workspace +4 -0
- package/dev-tools/starter-kit/starter-install.sh +2 -2
- package/package.json +6 -8
- package/design-tokens/utilities/build-tokens-src/merge-css.mjs +0 -60
- package/design-tokens/utilities/build-tokens.mjs +0 -153
- /package/{design-tokens/utilities → dev-tools/design-tokens-builder}/build-tokens-src/resolve-source-paths.mjs +0 -0
- /package/{design-tokens/utilities → dev-tools/design-tokens-builder}/build-tokens-src/transforms.mjs +0 -0
|
@@ -216,20 +216,25 @@ export const collectConcreteFilePaths = async (sd) => {
|
|
|
216
216
|
// @param {string|null} jsonDestFile Aggregated file base name, or null
|
|
217
217
|
// @param {'json'|'jsonc'} jsonFormat Output format
|
|
218
218
|
// @param {'keep'|'calc'|'resolve'} jsonExpression Expression handling mode
|
|
219
|
+
// @param {string} [suffix] Appended to every destination
|
|
220
|
+
// filename, before the extension — used by the sourceModes build
|
|
221
|
+
// (build-source-modes.mjs) to produce one set of files per mode, e.g.
|
|
222
|
+
// "-light" -> "tokens-light.jsonc" / "size-light.jsonc" (default: '')
|
|
219
223
|
// @returns {object[]} File descriptors for the Style Dictionary platform
|
|
220
224
|
|
|
221
225
|
export const buildJsonFiles = (
|
|
222
226
|
concreteFilePaths,
|
|
223
227
|
jsonDestFile,
|
|
224
228
|
jsonFormat = 'json',
|
|
225
|
-
jsonExpression = 'keep'
|
|
229
|
+
jsonExpression = 'keep',
|
|
230
|
+
suffix = ''
|
|
226
231
|
) => {
|
|
227
232
|
const ext = jsonFormat === 'jsonc' ? '.jsonc' : '.json';
|
|
228
233
|
const jsonc = jsonFormat === 'jsonc';
|
|
229
234
|
|
|
230
235
|
if (jsonDestFile) {
|
|
231
236
|
return [{
|
|
232
|
-
destination: jsonDestFile + ext,
|
|
237
|
+
destination: jsonDestFile + suffix + ext,
|
|
233
238
|
format: 'json/tokens',
|
|
234
239
|
options: { jsonc, jsonExpression },
|
|
235
240
|
}];
|
|
@@ -242,7 +247,7 @@ export const buildJsonFiles = (
|
|
|
242
247
|
// one silently overwrite the other.
|
|
243
248
|
const pathsByDestName = new Map();
|
|
244
249
|
for (const filePath of concreteFilePaths) {
|
|
245
|
-
const destName = path.basename(filePath).replace(/\.[^.]+$/, '') + ext;
|
|
250
|
+
const destName = path.basename(filePath).replace(/\.[^.]+$/, '') + suffix + ext;
|
|
246
251
|
if (!pathsByDestName.has(destName)) pathsByDestName.set(destName, []);
|
|
247
252
|
pathsByDestName.get(destName).push(filePath);
|
|
248
253
|
}
|
|
@@ -260,7 +265,7 @@ export const buildJsonFiles = (
|
|
|
260
265
|
return concreteFilePaths.map((filePath) => {
|
|
261
266
|
const absPath = path.resolve(filePath);
|
|
262
267
|
return {
|
|
263
|
-
destination: path.basename(filePath).replace(/\.[^.]+$/, '') + ext,
|
|
268
|
+
destination: path.basename(filePath).replace(/\.[^.]+$/, '') + suffix + ext,
|
|
264
269
|
format: 'json/tokens',
|
|
265
270
|
options: { jsonc, jsonExpression },
|
|
266
271
|
filter: (token) => {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// build-tokens-src/legacy-tokens-parser.mjs
|
|
2
|
+
// Registers a Style Dictionary parser that bridges "legacy" token files
|
|
3
|
+
// (Style Dictionary v3/v4-style syntax: no `$` prefix on `value`/`type`,
|
|
4
|
+
// references written as `{group.token.value}`) into the W3C DTCG syntax
|
|
5
|
+
// (`$value`/`$type`, references as `{group.token}`) required by Style
|
|
6
|
+
// Dictionary v5.
|
|
7
|
+
//
|
|
8
|
+
// Why this is needed: Style Dictionary v5 decides whether a build "usesDtcg"
|
|
9
|
+
// ONCE per build, from whichever source file is processed first (see
|
|
10
|
+
// detectDtcgSyntax() called inside combineJSON.js in style-dictionary) — it
|
|
11
|
+
// is a single global flag, not a per-file check. Mixing a legacy-syntax file
|
|
12
|
+
// (e.g. a file exported from Open Props, or from an old Style Dictionary
|
|
13
|
+
// v3/v4 project) together with DTCG sources therefore causes the legacy
|
|
14
|
+
// file's tokens to be silently dropped regardless of file order: they never
|
|
15
|
+
// carry the `$value` key combineJSON's own tagging (and the rest of the
|
|
16
|
+
// pipeline) expects.
|
|
17
|
+
//
|
|
18
|
+
// This parser intercepts every `.json` source file (not `.jsonc` — that
|
|
19
|
+
// extension is reserved in this project for hand-authored DTCG sources,
|
|
20
|
+
// already handled correctly by Style Dictionary's own built-in loader) and
|
|
21
|
+
// converts, node by node:
|
|
22
|
+
// value -> $value, type -> $type, comment/description -> $description
|
|
23
|
+
// "{group.token.value}" -> "{group.token}" inside reference strings
|
|
24
|
+
// Nodes that already use `$value` (DTCG v5 syntax) are left untouched, so
|
|
25
|
+
// the parser is a safe no-op for files that don't need conversion — no
|
|
26
|
+
// config flag or file list is required, detection is fully automatic.
|
|
27
|
+
//
|
|
28
|
+
// Limitation: `.json` files handled by this parser are parsed with plain
|
|
29
|
+
// JSON.parse (no comments, no trailing commas), unlike Style Dictionary's
|
|
30
|
+
// own built-in loader (which uses JSON5 for .json/.jsonc/.json5). This keeps
|
|
31
|
+
// the builder dependency-free — legacy token exports (Open Props, old Style
|
|
32
|
+
// Dictionary v3/v4 projects) are virtually always strict JSON anyway. Use
|
|
33
|
+
// `.jsonc` or `.mjs` for hand-authored DTCG sources that need comments.
|
|
34
|
+
|
|
35
|
+
import StyleDictionary from 'style-dictionary';
|
|
36
|
+
|
|
37
|
+
export const LEGACY_TOKENS_PARSER_NAME = 'minimo/legacy-tokens';
|
|
38
|
+
|
|
39
|
+
// Matches "{some.token.path.value}" -> captures "some.token.path"
|
|
40
|
+
const LEGACY_REF = /\{([^}]+)\.value\}/g;
|
|
41
|
+
|
|
42
|
+
/** @param {unknown} value @returns {unknown} */
|
|
43
|
+
const convertLegacyRefs = (value) => {
|
|
44
|
+
if (typeof value === 'string') {
|
|
45
|
+
return value.replace(LEGACY_REF, '{$1}');
|
|
46
|
+
}
|
|
47
|
+
if (Array.isArray(value)) {
|
|
48
|
+
return value.map(convertLegacyRefs);
|
|
49
|
+
}
|
|
50
|
+
if (value !== null && typeof value === 'object') {
|
|
51
|
+
return Object.fromEntries(
|
|
52
|
+
Object.entries(value).map(([key, v]) => [key, convertLegacyRefs(v)])
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** @param {unknown} node @returns {node is Record<string, unknown>} */
|
|
59
|
+
const isPlainObject = (node) => node !== null && typeof node === 'object' && !Array.isArray(node);
|
|
60
|
+
|
|
61
|
+
/** @param {unknown} node @returns {unknown} */
|
|
62
|
+
const convertLegacyNode = (node) => {
|
|
63
|
+
if (!isPlainObject(node)) return node;
|
|
64
|
+
|
|
65
|
+
// Already DTCG v5 syntax: leave untouched.
|
|
66
|
+
if (Object.hasOwn(node, '$value')) return node;
|
|
67
|
+
|
|
68
|
+
if (Object.hasOwn(node, 'value')) {
|
|
69
|
+
// Legacy token leaf. Some legacy files (e.g. Open Props) also attach
|
|
70
|
+
// sibling numbered/named children to a node that carries its own
|
|
71
|
+
// `value`/`type` (a group that is also a default token) — those
|
|
72
|
+
// children are converted too, even though Style Dictionary itself
|
|
73
|
+
// cannot represent "group + own $value" ambiguity natively.
|
|
74
|
+
const { value, type, comment, description, ...rest } = node;
|
|
75
|
+
const convertedRest = Object.fromEntries(
|
|
76
|
+
Object.entries(rest).map(([key, child]) => [key, convertLegacyNode(child)])
|
|
77
|
+
);
|
|
78
|
+
const desc = comment ?? description;
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
...convertedRest,
|
|
82
|
+
$value: convertLegacyRefs(value),
|
|
83
|
+
...(type !== undefined ? { $type: type } : {}),
|
|
84
|
+
...(desc !== undefined ? { $description: desc } : {}),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Group node: recurse into children.
|
|
89
|
+
return Object.fromEntries(
|
|
90
|
+
Object.entries(node).map(([key, child]) => [key, convertLegacyNode(child)])
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Registers the parser globally on the StyleDictionary class. To actually
|
|
95
|
+
// run it, a Style Dictionary instance must also opt in via
|
|
96
|
+
// `parsers: [LEGACY_TOKENS_PARSER_NAME]` in its config (registering alone
|
|
97
|
+
// does not activate it for every instance — see Style Dictionary's parser
|
|
98
|
+
// hooks docs).
|
|
99
|
+
export const registerLegacyTokensParser = () => {
|
|
100
|
+
StyleDictionary.registerParser({
|
|
101
|
+
name: LEGACY_TOKENS_PARSER_NAME,
|
|
102
|
+
pattern: /\.json$/,
|
|
103
|
+
parser: ({ contents }) => convertLegacyNode(JSON.parse(contents)),
|
|
104
|
+
});
|
|
105
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// build-tokens-src/merge-css.mjs
|
|
2
|
+
// Supports the `mergeCustomProps` config option: pre-existing custom
|
|
3
|
+
// properties in the CSS destination file take priority over the ones
|
|
4
|
+
// generated by the current build (manual overrides/additions survive rebuilds).
|
|
5
|
+
//
|
|
6
|
+
// Usage (single `source` build):
|
|
7
|
+
// 1. Call loadExistingCustomProps(absDestPath) BEFORE Style Dictionary runs,
|
|
8
|
+
// since it reads the file that is about to be overwritten.
|
|
9
|
+
// 2. The css/variables-sorted format (see formats/css.mjs) calls
|
|
10
|
+
// mergeCustomProps(generatedMap) while building the output, right before
|
|
11
|
+
// serialising the final CSS text.
|
|
12
|
+
//
|
|
13
|
+
// Usage (sourceModes build, see build-source-modes.mjs):
|
|
14
|
+
// 1. Call loadExistingCustomPropsScoped(absDestPath, baseMode) BEFORE any
|
|
15
|
+
// of the per-mode Style Dictionary instances run.
|
|
16
|
+
// 2. The css/variables-sorted format calls mergeCustomProps(generatedMap, mode)
|
|
17
|
+
// once per mode. Declarations are matched only within the same mode's
|
|
18
|
+
// scope (the base `:root { ... }` block, or the `:root { ... }` nested
|
|
19
|
+
// inside that mode's `@media (prefers-color-scheme: <mode>) { ... }`),
|
|
20
|
+
// so the same custom property name defined with different values across
|
|
21
|
+
// modes is never mixed up.
|
|
22
|
+
|
|
23
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
24
|
+
|
|
25
|
+
const CUSTOM_PROP_RE = /^\s*--([\w-]+):\s*(.+)$/gm;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parses `--name: <tail>` declarations out of a CSS text block. <tail> is
|
|
29
|
+
* captured verbatim (value, terminating `;`, and anything else on the same
|
|
30
|
+
* line, e.g. a trailing comment) so it round-trips unchanged through a merge.
|
|
31
|
+
* Only single-line declarations are supported.
|
|
32
|
+
* @param {string} cssText
|
|
33
|
+
* @returns {Record<string,string>} name → declaration-tail map, in first-seen order
|
|
34
|
+
*/
|
|
35
|
+
export const parseCustomProps = (cssText) => {
|
|
36
|
+
/** @type {Record<string,string>} */
|
|
37
|
+
const map = {};
|
|
38
|
+
for (const match of cssText.matchAll(CUSTOM_PROP_RE)) {
|
|
39
|
+
map[match[1]] = match[2].trim();
|
|
40
|
+
}
|
|
41
|
+
return map;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Returns the index of the closing brace matching the `{` at openIndex, by
|
|
45
|
+
// simple depth counting. Braces inside comments/strings are not special-cased
|
|
46
|
+
// — not expected to occur in generated custom-properties CSS.
|
|
47
|
+
/** @param {string} text @param {number} openIndex */
|
|
48
|
+
const matchBraces = (text, openIndex) => {
|
|
49
|
+
let depth = 0;
|
|
50
|
+
for (let i = openIndex; i < text.length; i++) {
|
|
51
|
+
if (text[i] === '{') depth++;
|
|
52
|
+
else if (text[i] === '}') {
|
|
53
|
+
depth--;
|
|
54
|
+
if (depth === 0) return i;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return -1;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Finds every @media (prefers-color-scheme: <mode>) { ... } block in a
|
|
61
|
+
// previously generated sourceModes CSS destFile, with absolute character
|
|
62
|
+
// offsets into cssText. Used to split the file into per-mode scopes (see
|
|
63
|
+
// splitModeScopes() below) and, by check-unresolved-custom-props.mjs, to
|
|
64
|
+
// tell which mode a given custom property declaration belongs to.
|
|
65
|
+
// An optional wrapping `@layer { ... }` (addLayer option) is transparent to
|
|
66
|
+
// this search, since it only looks for the `@media (prefers-color-scheme: ...)`
|
|
67
|
+
// pattern itself.
|
|
68
|
+
// @param {string} cssText
|
|
69
|
+
// @returns {{mode:string,start:number,end:number}[]} start/end are the
|
|
70
|
+
// indices of the block's outer `{`/matching `}` (inclusive)
|
|
71
|
+
export const findMediaModeBlocks = (cssText) => {
|
|
72
|
+
const MEDIA_RE = /@media\s*\(\s*prefers-color-scheme\s*:\s*([\w-]+)\s*\)\s*\{/g;
|
|
73
|
+
/** @type {{mode:string,start:number,end:number}[]} */
|
|
74
|
+
const mediaBlocks = [];
|
|
75
|
+
for (const match of cssText.matchAll(MEDIA_RE)) {
|
|
76
|
+
const openIdx = match.index + match[0].length - 1;
|
|
77
|
+
const closeIdx = matchBraces(cssText, openIdx);
|
|
78
|
+
if (closeIdx === -1) continue;
|
|
79
|
+
mediaBlocks.push({ mode: match[1], start: openIdx, end: closeIdx });
|
|
80
|
+
}
|
|
81
|
+
return mediaBlocks;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Splits a previously generated sourceModes CSS destFile into per-mode
|
|
85
|
+
// scopes (see build-source-modes.mjs for the shape of the generated file):
|
|
86
|
+
// - the base scope is the top-level `:root { ... }` block, i.e. the one
|
|
87
|
+
// NOT nested inside a `@media (prefers-color-scheme: ...)` rule
|
|
88
|
+
// - every other scope is the `:root { ... }` block nested inside its own
|
|
89
|
+
// `@media (prefers-color-scheme: <mode>) { ... }` rule
|
|
90
|
+
// @param {string} cssText
|
|
91
|
+
// @returns {Record<string,string>} mode → CSS body text (base mode keyed as '__base__')
|
|
92
|
+
const splitModeScopes = (cssText) => {
|
|
93
|
+
/** @type {Record<string,string>} */
|
|
94
|
+
const scopes = {};
|
|
95
|
+
|
|
96
|
+
// 1. Every @media (prefers-color-scheme: <mode>) { ... } block, and the
|
|
97
|
+
// :root { ... } body nested inside each one.
|
|
98
|
+
const mediaBlocks = findMediaModeBlocks(cssText);
|
|
99
|
+
for (const { mode, start, end } of mediaBlocks) {
|
|
100
|
+
const body = cssText.slice(start + 1, end);
|
|
101
|
+
const rootMatch = /:root\s*\{/.exec(body);
|
|
102
|
+
if (rootMatch) {
|
|
103
|
+
const rootOpen = rootMatch.index + rootMatch[0].length - 1;
|
|
104
|
+
const rootClose = matchBraces(body, rootOpen);
|
|
105
|
+
if (rootClose !== -1) {
|
|
106
|
+
scopes[mode] = body.slice(rootOpen + 1, rootClose);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 2. The base :root { ... } block: the first one not nested inside any of
|
|
112
|
+
// the media blocks found above.
|
|
113
|
+
const ROOT_RE = /:root\s*\{/g;
|
|
114
|
+
for (const match of cssText.matchAll(ROOT_RE)) {
|
|
115
|
+
const openIdx = match.index + match[0].length - 1;
|
|
116
|
+
const insideMedia = mediaBlocks.some((b) => openIdx > b.start && openIdx < b.end);
|
|
117
|
+
if (insideMedia) continue;
|
|
118
|
+
const closeIdx = matchBraces(cssText, openIdx);
|
|
119
|
+
if (closeIdx === -1) continue;
|
|
120
|
+
scopes.__base__ = cssText.slice(openIdx + 1, closeIdx);
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return scopes;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// null = merge disabled, or destination file not found yet (first build)
|
|
128
|
+
/** @type {Record<string,string>|null} */
|
|
129
|
+
let existingCustomProps = null;
|
|
130
|
+
|
|
131
|
+
// Per-mode equivalent of existingCustomProps, populated by
|
|
132
|
+
// loadExistingCustomPropsScoped() for a sourceModes build.
|
|
133
|
+
/** @type {Record<string, Record<string,string>>|null} */
|
|
134
|
+
let scopedExistingCustomProps = null;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Reads the CSS destination file (if it exists) and stores a name → value
|
|
138
|
+
* map of its current custom properties for later merging.
|
|
139
|
+
* @param {string} absDestPath absolute path of the CSS file to read
|
|
140
|
+
* @returns {Record<string,string>|null}
|
|
141
|
+
*/
|
|
142
|
+
export const loadExistingCustomProps = (absDestPath) => {
|
|
143
|
+
existingCustomProps = existsSync(absDestPath)
|
|
144
|
+
? parseCustomProps(readFileSync(absDestPath, 'utf8'))
|
|
145
|
+
: null;
|
|
146
|
+
return existingCustomProps;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* sourceModes equivalent of loadExistingCustomProps(): reads the CSS
|
|
151
|
+
* destination file (if it exists) and stores a per-mode name → value map,
|
|
152
|
+
* scoped to each mode's own `:root { ... }` block (see splitModeScopes()).
|
|
153
|
+
* @param {string} absDestPath absolute path of the CSS file to read
|
|
154
|
+
* @param {string} baseMode mode whose declarations live in the top-level,
|
|
155
|
+
* non-nested `:root { ... }` block (no default — always passed explicitly
|
|
156
|
+
* by build-source-modes.mjs, from the resolved sourceModesBase)
|
|
157
|
+
* @returns {Record<string, Record<string,string>>|null}
|
|
158
|
+
*/
|
|
159
|
+
export const loadExistingCustomPropsScoped = (absDestPath, baseMode) => {
|
|
160
|
+
if (!existsSync(absDestPath)) {
|
|
161
|
+
scopedExistingCustomProps = null;
|
|
162
|
+
return scopedExistingCustomProps;
|
|
163
|
+
}
|
|
164
|
+
const scopes = splitModeScopes(readFileSync(absDestPath, 'utf8'));
|
|
165
|
+
scopedExistingCustomProps = {};
|
|
166
|
+
if (scopes.__base__ !== undefined) {
|
|
167
|
+
scopedExistingCustomProps[baseMode] = parseCustomProps(scopes.__base__);
|
|
168
|
+
}
|
|
169
|
+
for (const [mode, body] of Object.entries(scopes)) {
|
|
170
|
+
if (mode === '__base__') continue;
|
|
171
|
+
scopedExistingCustomProps[mode] = parseCustomProps(body);
|
|
172
|
+
}
|
|
173
|
+
return scopedExistingCustomProps;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Merges freshly generated custom properties with the ones previously loaded
|
|
178
|
+
* via loadExistingCustomProps() / loadExistingCustomPropsScoped(). Pre-existing
|
|
179
|
+
* values take priority; custom properties found only in the pre-existing file
|
|
180
|
+
* (no longer generated) are kept as well.
|
|
181
|
+
* @param {Record<string,string>} generated name → value map produced by this build
|
|
182
|
+
* @param {string|null} [mode] when set (sourceModes build), looks up the
|
|
183
|
+
* pre-existing props scoped to this mode instead of the flat/global map (default: null)
|
|
184
|
+
* @returns {Record<string,string>}
|
|
185
|
+
*/
|
|
186
|
+
export const mergeCustomProps = (generated, mode = null) => {
|
|
187
|
+
if (mode !== null) {
|
|
188
|
+
const existing = scopedExistingCustomProps?.[mode];
|
|
189
|
+
return existing ? { ...generated, ...existing } : generated;
|
|
190
|
+
}
|
|
191
|
+
return existingCustomProps ? { ...generated, ...existingCustomProps } : generated;
|
|
192
|
+
};
|
package/{design-tokens/utilities → dev-tools/design-tokens-builder}/build-tokens-src/platforms.mjs
RENAMED
|
@@ -28,7 +28,9 @@ export const CSS_TRANSFORMS = [
|
|
|
28
28
|
// Kept minimal on purpose: consuming tools expect original values (e.g. "16px"
|
|
29
29
|
// not "1rem") and alias references ({...}) must be preserved to maintain
|
|
30
30
|
// token links.
|
|
31
|
-
|
|
31
|
+
// Exported so build-source-modes.mjs can build a matching json platform per
|
|
32
|
+
// sourceModes mode.
|
|
33
|
+
export const JSON_TRANSFORMS = [
|
|
32
34
|
'attribute/cti',
|
|
33
35
|
'name/kebab',
|
|
34
36
|
'color/css-modern',
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/// <reference types="node" />
|
|
3
|
+
/* eslint-disable no-console */
|
|
4
|
+
|
|
5
|
+
// build-tokens.mjs
|
|
6
|
+
// Entry point: imports modules, runs the build, lints the CSS output, prints the log.
|
|
7
|
+
//
|
|
8
|
+
// Project structure:
|
|
9
|
+
// build-tokens.mjs <- this file
|
|
10
|
+
// build-tokens-src/config.mjs <- reads --config flag and resolves all paths
|
|
11
|
+
// build-tokens-src/transforms.mjs <- registers custom Style Dictionary transforms
|
|
12
|
+
// build-tokens-src/formats/css.mjs <- registers the css/variables-sorted format
|
|
13
|
+
// build-tokens-src/formats/json.mjs <- registers the json/tokens format, exports helpers
|
|
14
|
+
// build-tokens-src/platforms.mjs <- builds the platforms object (css + optional json)
|
|
15
|
+
// build-tokens-src/build-source-modes.mjs <- alternative build path used when sourceModes is set
|
|
16
|
+
// build-tokens-src/merge-css.mjs <- supports the mergeCustomProps option (flat and per-mode)
|
|
17
|
+
|
|
18
|
+
import StyleDictionary from 'style-dictionary';
|
|
19
|
+
import * as path from 'node:path';
|
|
20
|
+
import { styleText } from 'node:util';
|
|
21
|
+
import { homedir } from 'node:os';
|
|
22
|
+
import stylelint from 'stylelint';
|
|
23
|
+
import { pathToFileURL } from 'node:url';
|
|
24
|
+
|
|
25
|
+
// ── 1. Configuration (args + resolved paths) ────────────────────────────────
|
|
26
|
+
import {
|
|
27
|
+
configAbsPath,
|
|
28
|
+
buildPath,
|
|
29
|
+
destFile,
|
|
30
|
+
stylelintConfigPath,
|
|
31
|
+
jsonBuildPath,
|
|
32
|
+
jsonDestFile,
|
|
33
|
+
jsonFormat,
|
|
34
|
+
jsonExpression,
|
|
35
|
+
source,
|
|
36
|
+
sourceModes,
|
|
37
|
+
sourceModesBase,
|
|
38
|
+
mergeCustomProps,
|
|
39
|
+
customPropsGroups,
|
|
40
|
+
pxToRem,
|
|
41
|
+
addLayer,
|
|
42
|
+
} from './build-tokens-src/config.mjs';
|
|
43
|
+
|
|
44
|
+
// ── 2. Custom transforms ─────────────────────────────────────────────────────
|
|
45
|
+
import './build-tokens-src/transforms.mjs';
|
|
46
|
+
|
|
47
|
+
// ── 2b. Legacy token syntax bridge ───────────────────────────────────────────
|
|
48
|
+
// Converts legacy (non-DTCG, e.g. Open Props) .json/.jsonc source files to
|
|
49
|
+
// DTCG v5 syntax at parse time — see build-tokens-src/legacy-tokens-parser.mjs.
|
|
50
|
+
import { LEGACY_TOKENS_PARSER_NAME, registerLegacyTokensParser } from './build-tokens-src/legacy-tokens-parser.mjs';
|
|
51
|
+
registerLegacyTokensParser();
|
|
52
|
+
|
|
53
|
+
// ── 3. CSS format ─────────────────────────────────────────────────────────────
|
|
54
|
+
// customPropsCount is updated by the format at render time
|
|
55
|
+
// loadExistingCustomProps supports the mergeCustomProps option (see below)
|
|
56
|
+
import { customPropsCount } from './build-tokens-src/formats/css.mjs';
|
|
57
|
+
import { loadExistingCustomProps } from './build-tokens-src/merge-css.mjs';
|
|
58
|
+
|
|
59
|
+
// ── 4. JSON format ────────────────────────────────────────────────────────────
|
|
60
|
+
import { buildJsonFiles, collectConcreteFilePaths } from './build-tokens-src/formats/json.mjs';
|
|
61
|
+
|
|
62
|
+
// ── 5. Platforms builder ──────────────────────────────────────────────────────
|
|
63
|
+
import { buildPlatforms } from './build-tokens-src/platforms.mjs';
|
|
64
|
+
|
|
65
|
+
// ── 5b. sourceModes builder (alternative to the single-source build below) ──
|
|
66
|
+
import { buildSourceModes } from './build-tokens-src/build-source-modes.mjs';
|
|
67
|
+
|
|
68
|
+
// ── 6. Clean json output directory ───────────────────────────────────────────
|
|
69
|
+
// Remove only previously generated *.json/*.jsonc files, so that tokens
|
|
70
|
+
// removed from the source are not left behind as stale artefacts, while any
|
|
71
|
+
// other file placed in jsonBuildPath by the user (e.g. a README) is left
|
|
72
|
+
// untouched. Files are written flat in jsonBuildPath (no subdirectories), so
|
|
73
|
+
// only its top level is scanned. Runs regardless of sourceModes.
|
|
74
|
+
if (jsonBuildPath) {
|
|
75
|
+
const { readdir, rm, mkdir } = await import('fs/promises');
|
|
76
|
+
const existingEntries = await readdir(jsonBuildPath, { withFileTypes: true }).catch(() => []);
|
|
77
|
+
await Promise.all(
|
|
78
|
+
existingEntries
|
|
79
|
+
.filter((entry) => entry.isFile() && /\.jsonc?$/.test(entry.name))
|
|
80
|
+
.map((entry) => rm(path.join(jsonBuildPath, entry.name)))
|
|
81
|
+
);
|
|
82
|
+
await mkdir(jsonBuildPath, { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── 7. Build ──────────────────────────────────────────────────────────────────
|
|
86
|
+
// concreteFilePaths / modeResult are used below in the log section (10).
|
|
87
|
+
/** @type {string[]} */
|
|
88
|
+
let concreteFilePaths = [];
|
|
89
|
+
/** @type {Awaited<ReturnType<typeof buildSourceModes>>|null} */
|
|
90
|
+
let modeResult = null;
|
|
91
|
+
|
|
92
|
+
if (sourceModes) {
|
|
93
|
+
// sourceModes build: one StyleDictionary instance per mode, composed into
|
|
94
|
+
// a single destFile — see build-tokens-src/build-source-modes.mjs.
|
|
95
|
+
modeResult = await buildSourceModes({
|
|
96
|
+
sourceModes,
|
|
97
|
+
baseMode: sourceModesBase,
|
|
98
|
+
buildPath,
|
|
99
|
+
destFile,
|
|
100
|
+
jsonBuildPath,
|
|
101
|
+
jsonDestFile,
|
|
102
|
+
jsonFormat,
|
|
103
|
+
jsonExpression,
|
|
104
|
+
customPropsGroups,
|
|
105
|
+
pxToRem,
|
|
106
|
+
addLayer,
|
|
107
|
+
mergeCustomProps,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
} else {
|
|
111
|
+
// Single-source build (default).
|
|
112
|
+
|
|
113
|
+
// Pre-existing custom properties (mergeCustomProps option): read before
|
|
114
|
+
// Style Dictionary runs, since it overwrites the destination file. The
|
|
115
|
+
// css/variables-sorted format (formats/css.mjs) merges these values in —
|
|
116
|
+
// pre-existing values take priority — right before serialising the final
|
|
117
|
+
// CSS output. See merge-css.mjs.
|
|
118
|
+
if (mergeCustomProps) {
|
|
119
|
+
loadExistingCustomProps(path.join(buildPath, destFile));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Collect concrete source file paths (multi-file JSON mode only). Source
|
|
123
|
+
// entries may be glob patterns. SD expands them internally and stores the
|
|
124
|
+
// concrete path in token.filePath. We need those concrete paths to build
|
|
125
|
+
// the per-file descriptors for the json platform.
|
|
126
|
+
//
|
|
127
|
+
// SD v5 lazy-loads sources: `await sd.hasInitialized` triggers loading;
|
|
128
|
+
// after that sd.allTokens is a plain synchronous array with filePath populated.
|
|
129
|
+
//
|
|
130
|
+
// In single-file mode (jsonDestFile is set) we skip this step entirely.
|
|
131
|
+
// source is only null when sourceModes is set (see config.mjs); this
|
|
132
|
+
// branch runs exclusively when sourceModes is not set.
|
|
133
|
+
const singleSource = /** @type {string[]} */ (source);
|
|
134
|
+
|
|
135
|
+
if (jsonBuildPath && !jsonDestFile) {
|
|
136
|
+
const sdInit = new StyleDictionary({
|
|
137
|
+
source: singleSource,
|
|
138
|
+
parsers: [LEGACY_TOKENS_PARSER_NAME],
|
|
139
|
+
log: { verbosity: 'silent' },
|
|
140
|
+
platforms: {},
|
|
141
|
+
});
|
|
142
|
+
concreteFilePaths = await collectConcreteFilePaths(sdInit);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const sd = new StyleDictionary({
|
|
146
|
+
source: singleSource,
|
|
147
|
+
parsers: [LEGACY_TOKENS_PARSER_NAME],
|
|
148
|
+
log: { verbosity: 'verbose' },
|
|
149
|
+
platforms: buildPlatforms({
|
|
150
|
+
buildPath,
|
|
151
|
+
destFile,
|
|
152
|
+
jsonBuildPath,
|
|
153
|
+
jsonDestFile,
|
|
154
|
+
jsonFormat,
|
|
155
|
+
jsonExpression,
|
|
156
|
+
concreteFilePaths,
|
|
157
|
+
customPropsGroups,
|
|
158
|
+
pxToRem,
|
|
159
|
+
addLayer,
|
|
160
|
+
}),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await sd.buildAllPlatforms();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── 8. CSS lint ───────────────────────────────────────────────────────────────
|
|
167
|
+
const stylelintConfig = await import(pathToFileURL(stylelintConfigPath).href).then(m => m.default);
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
await stylelint.lint({
|
|
171
|
+
config: stylelintConfig,
|
|
172
|
+
files: [path.join(buildPath, destFile)],
|
|
173
|
+
fix: true,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ── 9. Log ────────────────────────────────────────────────────────────────────
|
|
177
|
+
/** @param {string} p */
|
|
178
|
+
const short = (p) => p.replace(homedir(), '~');
|
|
179
|
+
|
|
180
|
+
console.log(styleText(['yellow'], `[build-tokens] config file : ${short(configAbsPath)}`));
|
|
181
|
+
|
|
182
|
+
if (sourceModes) {
|
|
183
|
+
console.log(styleText(['yellow'],
|
|
184
|
+
`[build-tokens] modes : ${Object.keys(sourceModes).join(', ')} (base: ${sourceModesBase})`
|
|
185
|
+
));
|
|
186
|
+
} else {
|
|
187
|
+
console.log(styleText(['yellow'], `[build-tokens] source : ${(/** @type {string[]} */ (source)).map(short)}`));
|
|
188
|
+
}
|
|
189
|
+
console.log(styleText(['yellow'], `[build-tokens] dest file : ${short(path.join(buildPath, destFile))}`));
|
|
190
|
+
|
|
191
|
+
if (jsonBuildPath) {
|
|
192
|
+
if (sourceModes && modeResult) {
|
|
193
|
+
const totalJsonFiles = Object.values(modeResult.jsonFilesByMode)
|
|
194
|
+
.reduce((count, files) => count + files.length, 0);
|
|
195
|
+
console.log(styleText(['yellow'],
|
|
196
|
+
`[build-tokens] json : ${totalJsonFiles} files in ${short(jsonBuildPath)} (${Object.keys(sourceModes).length} modes)`
|
|
197
|
+
));
|
|
198
|
+
} else {
|
|
199
|
+
const jsonFiles = buildJsonFiles(concreteFilePaths, jsonDestFile, jsonFormat, jsonExpression);
|
|
200
|
+
if (jsonFiles.length === 1) {
|
|
201
|
+
console.log(styleText(['yellow'],
|
|
202
|
+
`[build-tokens] json : ${short(path.join(jsonBuildPath, jsonFiles[0].destination))}`
|
|
203
|
+
));
|
|
204
|
+
} else {
|
|
205
|
+
console.log(styleText(['yellow'],
|
|
206
|
+
`[build-tokens] json : ${jsonFiles.length} files in ${short(jsonBuildPath)}`
|
|
207
|
+
));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
console.log(styleText(['yellow'],
|
|
212
|
+
'[build-tokens] json : (disabled — jsonBuildPath not set)'
|
|
213
|
+
));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
console.log(styleText(['green'],
|
|
217
|
+
`[build-tokens] ${sourceModes && modeResult ? modeResult.totalCustomProps : customPropsCount} custom properties processed`
|
|
218
|
+
));
|
|
219
|
+
console.log(styleText(['green'], '**** DONE ****'));
|