@massimo-cassandro/minimo 1.33.6 → 1.34.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 +208 -0
- package/dev-tools/design-tokens-builder/build-tokens-src/build-source-modes.mjs +152 -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/merge-css.mjs +192 -0
- package/{design-tokens/utilities → dev-tools/design-tokens-builder}/build-tokens-src/platforms.mjs +3 -1
- package/{design-tokens/utilities → dev-tools/design-tokens-builder}/build-tokens.mjs +99 -45
- package/{design-tokens/utilities → dev-tools/design-tokens-builder}/check-unresolved-custom-props.mjs +66 -13
- package/{design-tokens → dev-tools/design-tokens-builder}/tokens-config-sample.mjs +39 -1
- package/dev-tools/starter-kit/source_files/root/package-tpl.json +2 -2
- package/dev-tools/starter-kit/starter-install.sh +2 -2
- package/package.json +6 -8
- package/src/web-components/s-datatable-component/src/parse-cols.js +1 -1
- package/design-tokens/utilities/build-tokens-src/merge-css.mjs +0 -60
- /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,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',
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
// build-tokens-src/formats/css.mjs <- registers the css/variables-sorted format
|
|
13
13
|
// build-tokens-src/formats/json.mjs <- registers the json/tokens format, exports helpers
|
|
14
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)
|
|
15
17
|
|
|
16
18
|
import StyleDictionary from 'style-dictionary';
|
|
17
19
|
import * as path from 'node:path';
|
|
@@ -31,6 +33,8 @@ import {
|
|
|
31
33
|
jsonFormat,
|
|
32
34
|
jsonExpression,
|
|
33
35
|
source,
|
|
36
|
+
sourceModes,
|
|
37
|
+
sourceModesBase,
|
|
34
38
|
mergeCustomProps,
|
|
35
39
|
customPropsGroups,
|
|
36
40
|
pxToRem,
|
|
@@ -52,37 +56,15 @@ import { buildJsonFiles, collectConcreteFilePaths } from './build-tokens-src/for
|
|
|
52
56
|
// ── 5. Platforms builder ──────────────────────────────────────────────────────
|
|
53
57
|
import { buildPlatforms } from './build-tokens-src/platforms.mjs';
|
|
54
58
|
|
|
55
|
-
// ── 5b.
|
|
56
|
-
|
|
57
|
-
// file. The css/variables-sorted format (formats/css.mjs) merges these
|
|
58
|
-
// values in — pre-existing values take priority — right before serialising
|
|
59
|
-
// the final CSS output. See merge-css.mjs.
|
|
60
|
-
if (mergeCustomProps) {
|
|
61
|
-
loadExistingCustomProps(path.join(buildPath, destFile));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// ── 6. Collect concrete source file paths (multi-file JSON mode only) ────────
|
|
65
|
-
// Source entries may be glob patterns. SD expands them internally and stores
|
|
66
|
-
// the concrete path in token.filePath. We need those concrete paths to build
|
|
67
|
-
// the per-file descriptors for the json platform.
|
|
68
|
-
//
|
|
69
|
-
// SD v5 lazy-loads sources: `await sd.hasInitialized` triggers loading;
|
|
70
|
-
// after that sd.allTokens is a plain synchronous array with filePath populated.
|
|
71
|
-
//
|
|
72
|
-
// In single-file mode (jsonDestFile is set) we skip this step entirely.
|
|
73
|
-
let concreteFilePaths = [];
|
|
74
|
-
|
|
75
|
-
if (jsonBuildPath && !jsonDestFile) {
|
|
76
|
-
const sdInit = new StyleDictionary({ source, log: { verbosity: 'silent' }, platforms: {} });
|
|
77
|
-
concreteFilePaths = await collectConcreteFilePaths(sdInit);
|
|
78
|
-
}
|
|
59
|
+
// ── 5b. sourceModes builder (alternative to the single-source build below) ──
|
|
60
|
+
import { buildSourceModes } from './build-tokens-src/build-source-modes.mjs';
|
|
79
61
|
|
|
80
|
-
// ──
|
|
62
|
+
// ── 6. Clean json output directory ───────────────────────────────────────────
|
|
81
63
|
// Remove only previously generated *.json/*.jsonc files, so that tokens
|
|
82
64
|
// removed from the source are not left behind as stale artefacts, while any
|
|
83
65
|
// other file placed in jsonBuildPath by the user (e.g. a README) is left
|
|
84
66
|
// untouched. Files are written flat in jsonBuildPath (no subdirectories), so
|
|
85
|
-
// only its top level is scanned.
|
|
67
|
+
// only its top level is scanned. Runs regardless of sourceModes.
|
|
86
68
|
if (jsonBuildPath) {
|
|
87
69
|
const { readdir, rm, mkdir } = await import('fs/promises');
|
|
88
70
|
const existingEntries = await readdir(jsonBuildPath, { withFileTypes: true }).catch(() => []);
|
|
@@ -94,27 +76,82 @@ if (jsonBuildPath) {
|
|
|
94
76
|
await mkdir(jsonBuildPath, { recursive: true });
|
|
95
77
|
}
|
|
96
78
|
|
|
97
|
-
// ──
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
79
|
+
// ── 7. Build ──────────────────────────────────────────────────────────────────
|
|
80
|
+
// concreteFilePaths / modeResult are used below in the log section (10).
|
|
81
|
+
/** @type {string[]} */
|
|
82
|
+
let concreteFilePaths = [];
|
|
83
|
+
/** @type {Awaited<ReturnType<typeof buildSourceModes>>|null} */
|
|
84
|
+
let modeResult = null;
|
|
85
|
+
|
|
86
|
+
if (sourceModes) {
|
|
87
|
+
// sourceModes build: one StyleDictionary instance per mode, composed into
|
|
88
|
+
// a single destFile — see build-tokens-src/build-source-modes.mjs.
|
|
89
|
+
modeResult = await buildSourceModes({
|
|
90
|
+
sourceModes,
|
|
91
|
+
baseMode: sourceModesBase,
|
|
102
92
|
buildPath,
|
|
103
93
|
destFile,
|
|
104
94
|
jsonBuildPath,
|
|
105
95
|
jsonDestFile,
|
|
106
96
|
jsonFormat,
|
|
107
97
|
jsonExpression,
|
|
108
|
-
concreteFilePaths,
|
|
109
98
|
customPropsGroups,
|
|
110
99
|
pxToRem,
|
|
111
100
|
addLayer,
|
|
112
|
-
|
|
113
|
-
});
|
|
101
|
+
mergeCustomProps,
|
|
102
|
+
});
|
|
114
103
|
|
|
115
|
-
|
|
104
|
+
} else {
|
|
105
|
+
// Single-source build (default).
|
|
106
|
+
|
|
107
|
+
// Pre-existing custom properties (mergeCustomProps option): read before
|
|
108
|
+
// Style Dictionary runs, since it overwrites the destination file. The
|
|
109
|
+
// css/variables-sorted format (formats/css.mjs) merges these values in —
|
|
110
|
+
// pre-existing values take priority — right before serialising the final
|
|
111
|
+
// CSS output. See merge-css.mjs.
|
|
112
|
+
if (mergeCustomProps) {
|
|
113
|
+
loadExistingCustomProps(path.join(buildPath, destFile));
|
|
114
|
+
}
|
|
116
115
|
|
|
117
|
-
//
|
|
116
|
+
// Collect concrete source file paths (multi-file JSON mode only). Source
|
|
117
|
+
// entries may be glob patterns. SD expands them internally and stores the
|
|
118
|
+
// concrete path in token.filePath. We need those concrete paths to build
|
|
119
|
+
// the per-file descriptors for the json platform.
|
|
120
|
+
//
|
|
121
|
+
// SD v5 lazy-loads sources: `await sd.hasInitialized` triggers loading;
|
|
122
|
+
// after that sd.allTokens is a plain synchronous array with filePath populated.
|
|
123
|
+
//
|
|
124
|
+
// In single-file mode (jsonDestFile is set) we skip this step entirely.
|
|
125
|
+
// source is only null when sourceModes is set (see config.mjs); this
|
|
126
|
+
// branch runs exclusively when sourceModes is not set.
|
|
127
|
+
const singleSource = /** @type {string[]} */ (source);
|
|
128
|
+
|
|
129
|
+
if (jsonBuildPath && !jsonDestFile) {
|
|
130
|
+
const sdInit = new StyleDictionary({ source: singleSource, log: { verbosity: 'silent' }, platforms: {} });
|
|
131
|
+
concreteFilePaths = await collectConcreteFilePaths(sdInit);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const sd = new StyleDictionary({
|
|
135
|
+
source: singleSource,
|
|
136
|
+
log: { verbosity: 'verbose' },
|
|
137
|
+
platforms: buildPlatforms({
|
|
138
|
+
buildPath,
|
|
139
|
+
destFile,
|
|
140
|
+
jsonBuildPath,
|
|
141
|
+
jsonDestFile,
|
|
142
|
+
jsonFormat,
|
|
143
|
+
jsonExpression,
|
|
144
|
+
concreteFilePaths,
|
|
145
|
+
customPropsGroups,
|
|
146
|
+
pxToRem,
|
|
147
|
+
addLayer,
|
|
148
|
+
}),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
await sd.buildAllPlatforms();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── 8. CSS lint ───────────────────────────────────────────────────────────────
|
|
118
155
|
const stylelintConfig = await import(pathToFileURL(stylelintConfigPath).href).then(m => m.default);
|
|
119
156
|
|
|
120
157
|
|
|
@@ -124,24 +161,39 @@ await stylelint.lint({
|
|
|
124
161
|
fix: true,
|
|
125
162
|
});
|
|
126
163
|
|
|
127
|
-
// ──
|
|
164
|
+
// ── 9. Log ────────────────────────────────────────────────────────────────────
|
|
128
165
|
/** @param {string} p */
|
|
129
166
|
const short = (p) => p.replace(homedir(), '~');
|
|
130
167
|
|
|
131
168
|
console.log(styleText(['yellow'], `[build-tokens] config file : ${short(configAbsPath)}`));
|
|
132
|
-
|
|
169
|
+
|
|
170
|
+
if (sourceModes) {
|
|
171
|
+
console.log(styleText(['yellow'],
|
|
172
|
+
`[build-tokens] modes : ${Object.keys(sourceModes).join(', ')} (base: ${sourceModesBase})`
|
|
173
|
+
));
|
|
174
|
+
} else {
|
|
175
|
+
console.log(styleText(['yellow'], `[build-tokens] source : ${(/** @type {string[]} */ (source)).map(short)}`));
|
|
176
|
+
}
|
|
133
177
|
console.log(styleText(['yellow'], `[build-tokens] dest file : ${short(path.join(buildPath, destFile))}`));
|
|
134
178
|
|
|
135
179
|
if (jsonBuildPath) {
|
|
136
|
-
|
|
137
|
-
|
|
180
|
+
if (sourceModes && modeResult) {
|
|
181
|
+
const totalJsonFiles = Object.values(modeResult.jsonFilesByMode)
|
|
182
|
+
.reduce((count, files) => count + files.length, 0);
|
|
138
183
|
console.log(styleText(['yellow'],
|
|
139
|
-
`[build-tokens] json : ${short(
|
|
184
|
+
`[build-tokens] json : ${totalJsonFiles} files in ${short(jsonBuildPath)} (${Object.keys(sourceModes).length} modes)`
|
|
140
185
|
));
|
|
141
186
|
} else {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
187
|
+
const jsonFiles = buildJsonFiles(concreteFilePaths, jsonDestFile, jsonFormat, jsonExpression);
|
|
188
|
+
if (jsonFiles.length === 1) {
|
|
189
|
+
console.log(styleText(['yellow'],
|
|
190
|
+
`[build-tokens] json : ${short(path.join(jsonBuildPath, jsonFiles[0].destination))}`
|
|
191
|
+
));
|
|
192
|
+
} else {
|
|
193
|
+
console.log(styleText(['yellow'],
|
|
194
|
+
`[build-tokens] json : ${jsonFiles.length} files in ${short(jsonBuildPath)}`
|
|
195
|
+
));
|
|
196
|
+
}
|
|
145
197
|
}
|
|
146
198
|
} else {
|
|
147
199
|
console.log(styleText(['yellow'],
|
|
@@ -149,5 +201,7 @@ if (jsonBuildPath) {
|
|
|
149
201
|
));
|
|
150
202
|
}
|
|
151
203
|
|
|
152
|
-
console.log(styleText(['green'],
|
|
204
|
+
console.log(styleText(['green'],
|
|
205
|
+
`[build-tokens] ${sourceModes && modeResult ? modeResult.totalCustomProps : customPropsCount} custom properties processed`
|
|
206
|
+
));
|
|
153
207
|
console.log(styleText(['green'], '**** DONE ****'));
|
|
@@ -25,6 +25,7 @@ import StyleDictionary from 'style-dictionary';
|
|
|
25
25
|
import './build-tokens-src/transforms.mjs';
|
|
26
26
|
import { CSS_TRANSFORMS } from './build-tokens-src/platforms.mjs';
|
|
27
27
|
import { resolveSourcePaths } from './build-tokens-src/resolve-source-paths.mjs';
|
|
28
|
+
import { findMediaModeBlocks } from './build-tokens-src/merge-css.mjs';
|
|
28
29
|
|
|
29
30
|
// const __filename = fileURLToPath(import.meta.url);
|
|
30
31
|
// const __dirname = dirname(__filename);
|
|
@@ -90,25 +91,48 @@ async function run() {
|
|
|
90
91
|
const definitionRegex = /--([a-z0-9-]+)(?=\s*:)/g;
|
|
91
92
|
|
|
92
93
|
// defined_props keeps file/line info too, so unused properties (section 6)
|
|
93
|
-
// can be reported with a link back to their definition.
|
|
94
|
-
|
|
94
|
+
// can be reported with a link back to their definition. With sourceModes
|
|
95
|
+
// (see build-tokens-src/config.mjs), the same prop name can legitimately
|
|
96
|
+
// appear once per mode with a different value — `mode` disambiguates
|
|
97
|
+
// which one a given entry is, so section 5 resolves each to the correct
|
|
98
|
+
// per-mode source file instead of mixing them up (see findMediaModeBlocks
|
|
99
|
+
// in build-tokens-src/merge-css.mjs).
|
|
100
|
+
/** @type {{file: string, line: number, prop: string, mode: string|null}[]} */
|
|
95
101
|
const defined_props = [];
|
|
96
102
|
|
|
97
103
|
const cssContent = fs.readFileSync(custom_prop_file_path, 'utf-8');
|
|
98
104
|
const custom_prop_relpath = path.relative(configDir, custom_prop_file_path);
|
|
105
|
+
|
|
106
|
+
// sourceModesBase mirrors the default-resolution logic in
|
|
107
|
+
// build-tokens-src/config.mjs: the configured sourceModesBase if it's
|
|
108
|
+
// actually a key of sourceModes, otherwise the first key.
|
|
109
|
+
const sourceModeNames = config.sourceModes ? Object.keys(config.sourceModes) : [];
|
|
110
|
+
const sourceModesBase = sourceModeNames.includes(config.sourceModesBase)
|
|
111
|
+
? config.sourceModesBase
|
|
112
|
+
: sourceModeNames[0];
|
|
113
|
+
const mediaModeBlocks = config.sourceModes ? findMediaModeBlocks(cssContent) : [];
|
|
114
|
+
|
|
115
|
+
/** @param {number} index @returns {string|null} */
|
|
116
|
+
const modeAtIndex = (index) => {
|
|
117
|
+
if (!config.sourceModes) return null;
|
|
118
|
+
const block = mediaModeBlocks.find(b => index > b.start && index < b.end);
|
|
119
|
+
return block ? block.mode : sourceModesBase;
|
|
120
|
+
};
|
|
121
|
+
|
|
99
122
|
[...cssContent.matchAll(definitionRegex)].forEach(m => {
|
|
100
123
|
const line = cssContent.substring(0, m.index).split('\n').length;
|
|
101
|
-
defined_props.push({ file: custom_prop_relpath, line, prop: `--${m[1]}
|
|
124
|
+
defined_props.push({ file: custom_prop_relpath, line, prop: `--${m[1]}`, mode: modeAtIndex(m.index) });
|
|
102
125
|
});
|
|
103
126
|
|
|
104
|
-
// Also include any extra CSS files declared in extraCustomPropsFiles
|
|
127
|
+
// Also include any extra CSS files declared in extraCustomPropsFiles.
|
|
128
|
+
// These are plain (non-sourceModes) files, so mode is always null.
|
|
105
129
|
for (const extraFile of (config.extraCustomPropsFiles ?? [])) {
|
|
106
130
|
const extraFilePath = path.resolve(configDir, extraFile);
|
|
107
131
|
const extraContent = fs.readFileSync(extraFilePath, 'utf-8');
|
|
108
132
|
const extraRelpath = path.relative(configDir, extraFilePath);
|
|
109
133
|
[...extraContent.matchAll(definitionRegex)].forEach(m => {
|
|
110
134
|
const line = extraContent.substring(0, m.index).split('\n').length;
|
|
111
|
-
defined_props.push({ file: extraRelpath, line, prop: `--${m[1]}
|
|
135
|
+
defined_props.push({ file: extraRelpath, line, prop: `--${m[1]}`, mode: null });
|
|
112
136
|
});
|
|
113
137
|
}
|
|
114
138
|
|
|
@@ -161,28 +185,57 @@ async function run() {
|
|
|
161
185
|
// CSS (or an extraCustomPropsFiles entry) — e.g. manual additions kept
|
|
162
186
|
// across rebuilds via the mergeCustomProps option — have no token source
|
|
163
187
|
// and are reported by name only.
|
|
188
|
+
//
|
|
189
|
+
// With sourceModes, resolution runs once PER MODE, keyed by mode name in
|
|
190
|
+
// sourceFileByPropByMode: the same prop name can be defined by a
|
|
191
|
+
// different source file in each mode (see the `dead: {...}` example in
|
|
192
|
+
// both a light and dark token file), so a single flattened name -> file
|
|
193
|
+
// map would silently point some entries at the wrong mode's file.
|
|
194
|
+
// sourceFileByProp (mode: null) covers the non-sourceModes case and
|
|
195
|
+
// extraCustomPropsFiles entries.
|
|
164
196
|
// ---------------------------------------------------------------------------
|
|
165
197
|
/** @type {Map<string, string>} */
|
|
166
198
|
const sourceFileByProp = new Map();
|
|
167
|
-
|
|
168
|
-
|
|
199
|
+
/** @type {Map<string, Map<string, string>>} */
|
|
200
|
+
const sourceFileByPropByMode = new Map();
|
|
201
|
+
|
|
202
|
+
/** @param {string[]} sourcePatterns @returns {Promise<Map<string,string>>} */
|
|
203
|
+
const resolveSourceFileMap = async (sourcePatterns) => {
|
|
204
|
+
/** @type {Map<string, string>} */
|
|
205
|
+
const map = new Map();
|
|
206
|
+
const resolvedPaths = resolveSourcePaths(sourcePatterns, configDir);
|
|
207
|
+
if (!resolvedPaths.length) return map;
|
|
169
208
|
try {
|
|
170
|
-
const tokenSourcePaths = resolveSourcePaths(config.source, configDir);
|
|
171
209
|
const sd = new StyleDictionary({
|
|
172
|
-
source:
|
|
210
|
+
source: resolvedPaths,
|
|
173
211
|
log: { verbosity: 'silent' },
|
|
174
212
|
platforms: { css: { transforms: CSS_TRANSFORMS } },
|
|
175
213
|
});
|
|
176
214
|
const dictionary = await sd.getPlatformTokens('css');
|
|
177
|
-
|
|
178
215
|
for (const token of dictionary.allTokens) {
|
|
179
|
-
if (token.filePath)
|
|
216
|
+
if (token.filePath) map.set(`--${token.name}`, token.filePath);
|
|
180
217
|
}
|
|
181
218
|
} catch {
|
|
182
219
|
// Non-fatal: unused properties will be reported by name only.
|
|
183
220
|
}
|
|
221
|
+
return map;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
if (checkUnused && config.sourceModes) {
|
|
225
|
+
for (const [mode, modeSource] of Object.entries(config.sourceModes)) {
|
|
226
|
+
sourceFileByPropByMode.set(mode, await resolveSourceFileMap(modeSource));
|
|
227
|
+
}
|
|
228
|
+
} else if (checkUnused && Array.isArray(config.source) && config.source.length) {
|
|
229
|
+
const map = await resolveSourceFileMap(config.source);
|
|
230
|
+
for (const [prop, filePath] of map) sourceFileByProp.set(prop, filePath);
|
|
184
231
|
}
|
|
185
232
|
|
|
233
|
+
/** @param {{prop: string, mode: string|null}} item @returns {string|undefined} */
|
|
234
|
+
const resolveSourceFilePath = (item) =>
|
|
235
|
+
item.mode
|
|
236
|
+
? sourceFileByPropByMode.get(item.mode)?.get(item.prop)
|
|
237
|
+
: sourceFileByProp.get(item.prop);
|
|
238
|
+
|
|
186
239
|
// ---------------------------------------------------------------------------
|
|
187
240
|
// 6. Build the list of unused custom properties (defined but never
|
|
188
241
|
// referenced via var() in any scanned CSS file) — the opposite check
|
|
@@ -194,7 +247,7 @@ async function run() {
|
|
|
194
247
|
.filter(item => !used_props_set.has(item.prop))
|
|
195
248
|
.filter(item => {
|
|
196
249
|
if (!ignoreUnusedInNodeModules) return true;
|
|
197
|
-
const sourceFilePath =
|
|
250
|
+
const sourceFilePath = resolveSourceFilePath(item);
|
|
198
251
|
if (!sourceFilePath) return true;
|
|
199
252
|
return !/(^|[\\/])node_modules([\\/]|$)/.test(sourceFilePath);
|
|
200
253
|
})
|
|
@@ -232,7 +285,7 @@ async function run() {
|
|
|
232
285
|
'## Unused custom properties\n\n'
|
|
233
286
|
+ unused_props
|
|
234
287
|
.map(item => {
|
|
235
|
-
const sourceFilePath =
|
|
288
|
+
const sourceFilePath = resolveSourceFilePath(item);
|
|
236
289
|
if (!sourceFilePath) return `* \`${item.prop}\``;
|
|
237
290
|
|
|
238
291
|
const relSource = path.relative(configDir, sourceFilePath);
|
|
@@ -46,9 +46,47 @@ const config = {
|
|
|
46
46
|
|
|
47
47
|
// your project tokens
|
|
48
48
|
'./project-tokens/*.{js,mjs,jsonc,json}',
|
|
49
|
-
|
|
50
49
|
],
|
|
51
50
|
|
|
51
|
+
// Optional light/dark (or other) custom properties split — alternative to
|
|
52
|
+
// `source`. If this key is set (a non-null object), `source` above is
|
|
53
|
+
// ignored: each key is a mode name with its own array of source patterns
|
|
54
|
+
// (same rules as `source`: paths, globs, or a mix).
|
|
55
|
+
//
|
|
56
|
+
// The generated CSS composes all modes into a single destFile:
|
|
57
|
+
// - the base mode's props (see sourceModesBase below) go in a top-level
|
|
58
|
+
// `:root { ... }` block, with `color-scheme: <all modes>;` prepended
|
|
59
|
+
// (e.g. "light dark")
|
|
60
|
+
// - every other mode's props go in
|
|
61
|
+
// `@media (prefers-color-scheme: <mode>) { :root { ... } }`, with
|
|
62
|
+
// `color-scheme: <mode>;` prepended
|
|
63
|
+
// customPropsGroups, mergeCustomProps, addLayer, pxToRem etc. all apply
|
|
64
|
+
// the same way as with a single `source`.
|
|
65
|
+
//
|
|
66
|
+
// The JSON output (see JSON TOKENS OUTPUT below) is produced once per
|
|
67
|
+
// mode, with the mode name appended to each filename, e.g. with
|
|
68
|
+
// jsonDestFile: 'tokens' -> "tokens-light.jsonc", "tokens-dark.jsonc".
|
|
69
|
+
//
|
|
70
|
+
// sample:
|
|
71
|
+
// sourceModes: {
|
|
72
|
+
// light: [
|
|
73
|
+
// `${minimo_path}/design-tokens/_src/**/*.tokens.{mjs,jsonc}`,
|
|
74
|
+
// `${minimo_path}/src/**/*.tokens.{mjs,jsonc}`,
|
|
75
|
+
// './project-tokens/light/*.{js,mjs,jsonc,json}',
|
|
76
|
+
// ],
|
|
77
|
+
// dark: [
|
|
78
|
+
// './project-tokens/dark/*.{js,mjs,jsonc,json}',
|
|
79
|
+
// ],
|
|
80
|
+
// },
|
|
81
|
+
sourceModes: null,
|
|
82
|
+
|
|
83
|
+
// With sourceModes set: the mode whose declarations go in the top-level
|
|
84
|
+
// `:root { ... }` block, instead of nested inside its own
|
|
85
|
+
// `@media (prefers-color-scheme: <mode>) { ... }` rule (default: the first
|
|
86
|
+
// key of sourceModes, e.g. 'light'). Ignored when sourceModes is not set.
|
|
87
|
+
sourceModesBase: 'light',
|
|
88
|
+
|
|
89
|
+
|
|
52
90
|
// If true (default), dimension token values expressed in px are converted
|
|
53
91
|
// to rem in the generated CSS. Values in other units (em, %, vh, dvw, ...)
|
|
54
92
|
// are never touched regardless of this option.
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
"*.avif"
|
|
18
18
|
],
|
|
19
19
|
"scripts": {
|
|
20
|
-
"progetto JS webpack DEV": "
|
|
20
|
+
"progetto JS webpack DEV": "NODE_ENV=development webpack serve --config ./webpack.config.mjs #--open-app-name 'Google Chrome'",
|
|
21
21
|
"php server": "php -S localhost:[[port8000]] # -t root_dir/",
|
|
22
|
-
"progetto sf webpack DEV": "
|
|
22
|
+
"progetto sf webpack DEV": "rm -rf ./public/_dev && NODE_ENV=development webpack serve --config ./webpack.config.mjs # http://192.168.1.135:[[port5700]",
|
|
23
23
|
"symfony server": "symfony serve --port=[[port8000]] --listen-ip=0.0.0.0 # http://192.168.1.135:[[port8000]] # -d (deamon): rimane aperto anche dopo la chiusura del terminale, terminare con `symfony server:stop`",
|
|
24
24
|
"webpack PROD": "npm run 'UPD-version' && NODE_ENV=production webpack --config ./webpack.config.mjs --stats minimal",
|
|
25
25
|
"eslint-check": "npx eslint app/",
|