@massimo-cassandro/minimo 1.38.4 → 1.38.5
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/dev-tools/design-tokens-builder/README.md +1 -1
- package/dev-tools/design-tokens-builder/build-tokens-src/legacy-tokens-parser.mjs +99 -24
- package/dev-tools/design-tokens-builder/build-tokens.mjs +6 -3
- package/dev-tools/design-tokens-builder/check-unresolved-custom-props.mjs +1 -2
- package/dev-tools/design-tokens-builder/tokens-config-sample.mjs +8 -6
- package/package.json +1 -1
|
@@ -69,7 +69,7 @@ The `source` array accepts any combination of:
|
|
|
69
69
|
|
|
70
70
|
**Legacy token syntax (auto-detected, `.json` only):** `.json` source files written in the older, non-DTCG Style Dictionary syntax (`value`/`type` instead of `$value`/`$type`, references as `{group.token.value}` instead of `{group.token}` — e.g. a file exported from [Open Props](https://open-props.style/)) are converted to DTCG v5 syntax automatically at parse time, node by node. Files already using `$value`/`$type` are left untouched, so no config flag or file list is needed — mixing legacy and DTCG sources in the same `source` array just works. `.jsonc` is not covered (reserved for hand-authored DTCG sources, parsed by Style Dictionary's own loader) and these `.json` files are parsed with plain `JSON.parse` (no comments, no trailing commas). See `build-tokens-src/legacy-tokens-parser.mjs`.
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
A legacy node that is at the same time a token (own `value`/`type`) *and* a group with nested children (e.g. Open Props' `other.ease.out`, which has its own value plus `out.1` ... `out.5`) can't be represented in DTCG v5, where a node with `$value` is always a leaf. Its own value is moved to a child token named `default` (`other.ease.out.default`, i.e. `--other-ease-out-default`), or `base` if a `default` child already exists; if both exist the build fails with an explicit error. References to such a node from the same file are rewritten accordingly; from other files they must use the child name explicitly.
|
|
73
73
|
|
|
74
74
|
#### Source prefixes
|
|
75
75
|
|
|
@@ -25,6 +25,12 @@
|
|
|
25
25
|
// the parser is a safe no-op for files that don't need conversion — no
|
|
26
26
|
// config flag or file list is required, detection is fully automatic.
|
|
27
27
|
//
|
|
28
|
+
// "Hybrid" nodes (a token with its own value that is also a group with
|
|
29
|
+
// nested children, e.g. Open Props' other.ease.out) can't be represented in
|
|
30
|
+
// DTCG v5, where a node with $value is a leaf. The own value is moved to a
|
|
31
|
+
// child token named `default` (or `base` if `default` is already a child
|
|
32
|
+
// name; the build fails if both are taken) — see collectHybridNodes().
|
|
33
|
+
//
|
|
28
34
|
// Limitation: `.json` files handled by this parser are parsed with plain
|
|
29
35
|
// JSON.parse (no comments, no trailing commas), unlike Style Dictionary's
|
|
30
36
|
// own built-in loader (which uses JSON5 for .json/.jsonc/.json5). This keeps
|
|
@@ -39,58 +45,127 @@ export const LEGACY_TOKENS_PARSER_NAME = 'minimo/legacy-tokens';
|
|
|
39
45
|
// Matches "{some.token.path.value}" -> captures "some.token.path"
|
|
40
46
|
const LEGACY_REF = /\{([^}]+)\.value\}/g;
|
|
41
47
|
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
// Name of the child token that receives the own value of a "hybrid" node
|
|
49
|
+
// (see collectHybridNodes()); FALLBACK_LEAF is used when a child with the
|
|
50
|
+
// preferred name already exists.
|
|
51
|
+
const PREFERRED_LEAF = 'default';
|
|
52
|
+
const FALLBACK_LEAF = 'base';
|
|
53
|
+
|
|
54
|
+
/** @param {unknown} node @returns {node is Record<string, unknown>} */
|
|
55
|
+
const isPlainObject = (node) => node !== null && typeof node === 'object' && !Array.isArray(node);
|
|
56
|
+
|
|
57
|
+
// True if the node is (or contains, at any depth) a token, legacy or DTCG.
|
|
58
|
+
/** @param {unknown} node @returns {boolean} */
|
|
59
|
+
const hasTokenDescendant = (node) => isPlainObject(node)
|
|
60
|
+
&& (Object.hasOwn(node, 'value') || Object.hasOwn(node, '$value')
|
|
61
|
+
|| Object.values(node).some(hasTokenDescendant));
|
|
62
|
+
|
|
63
|
+
/*
|
|
64
|
+
* A "hybrid" legacy node is a token (own `value`) that is also a group, i.e.
|
|
65
|
+
* has children that are tokens or groups of tokens (e.g. Open Props'
|
|
66
|
+
* other.ease.out, with its own value plus out.1 ... out.5). The own value is
|
|
67
|
+
* moved to a child token: `default`, or `base` if `default` is already a
|
|
68
|
+
* child name. If both are taken the build fails. Returns a map
|
|
69
|
+
* dot.path -> chosen child name, used to convert the node itself and to
|
|
70
|
+
* rewrite references pointing to it (references are only rewritten within
|
|
71
|
+
* the same file: a reference from another file to a hybrid node must use the
|
|
72
|
+
* child name explicitly).
|
|
73
|
+
*/
|
|
74
|
+
/**
|
|
75
|
+
* @param {unknown} node
|
|
76
|
+
* @param {string[]} path
|
|
77
|
+
* @param {string} filePath
|
|
78
|
+
* @param {Map<string,string>} [hybrids]
|
|
79
|
+
* @returns {Map<string,string>}
|
|
80
|
+
*/
|
|
81
|
+
const collectHybridNodes = (node, path, filePath, hybrids = new Map()) => {
|
|
82
|
+
if (!isPlainObject(node) || Object.hasOwn(node, '$value')) return hybrids;
|
|
83
|
+
|
|
84
|
+
if (Object.hasOwn(node, 'value')
|
|
85
|
+
&& Object.entries(node).some(([key, child]) => key !== 'value' && hasTokenDescendant(child))) {
|
|
86
|
+
const leaf = [PREFERRED_LEAF, FALLBACK_LEAF].find((name) => !Object.hasOwn(node, name));
|
|
87
|
+
if (!leaf) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`[build-tokens] legacy tokens: node "${path.join('.')}" in ${filePath} has its own value and children, `
|
|
90
|
+
+ `but both "${PREFERRED_LEAF}" and "${FALLBACK_LEAF}" child names are already taken`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
hybrids.set(path.join('.'), leaf);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const [key, child] of Object.entries(node)) {
|
|
97
|
+
collectHybridNodes(child, [...path, key], filePath, hybrids);
|
|
98
|
+
}
|
|
99
|
+
return hybrids;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {unknown} value
|
|
104
|
+
* @param {Map<string,string>} hybrids
|
|
105
|
+
* @returns {unknown}
|
|
106
|
+
*/
|
|
107
|
+
const convertLegacyRefs = (value, hybrids) => {
|
|
44
108
|
if (typeof value === 'string') {
|
|
45
|
-
return value.replace(LEGACY_REF,
|
|
109
|
+
return value.replace(LEGACY_REF, (_, ref) => (
|
|
110
|
+
hybrids.has(ref) ? `{${ref}.${hybrids.get(ref)}}` : `{${ref}}`
|
|
111
|
+
));
|
|
46
112
|
}
|
|
47
113
|
if (Array.isArray(value)) {
|
|
48
|
-
return value.map(convertLegacyRefs);
|
|
114
|
+
return value.map((v) => convertLegacyRefs(v, hybrids));
|
|
49
115
|
}
|
|
50
116
|
if (value !== null && typeof value === 'object') {
|
|
51
117
|
return Object.fromEntries(
|
|
52
|
-
Object.entries(value).map(([key, v]) => [key, convertLegacyRefs(v)])
|
|
118
|
+
Object.entries(value).map(([key, v]) => [key, convertLegacyRefs(v, hybrids)])
|
|
53
119
|
);
|
|
54
120
|
}
|
|
55
121
|
return value;
|
|
56
122
|
};
|
|
57
123
|
|
|
58
|
-
/**
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
124
|
+
/**
|
|
125
|
+
* @param {unknown} node
|
|
126
|
+
* @param {string[]} path
|
|
127
|
+
* @param {Map<string,string>} hybrids
|
|
128
|
+
* @returns {unknown}
|
|
129
|
+
*/
|
|
130
|
+
const convertLegacyNode = (node, path, hybrids) => {
|
|
63
131
|
if (!isPlainObject(node)) return node;
|
|
64
132
|
|
|
65
133
|
// Already DTCG v5 syntax: leave untouched.
|
|
66
134
|
if (Object.hasOwn(node, '$value')) return node;
|
|
67
135
|
|
|
68
136
|
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
137
|
const { value, type, comment, description, ...rest } = node;
|
|
75
|
-
const convertedRest = Object.fromEntries(
|
|
76
|
-
Object.entries(rest).map(([key, child]) => [key, convertLegacyNode(child)])
|
|
77
|
-
);
|
|
78
138
|
const desc = comment ?? description;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
...convertedRest,
|
|
82
|
-
$value: convertLegacyRefs(value),
|
|
139
|
+
const token = {
|
|
140
|
+
$value: convertLegacyRefs(value, hybrids),
|
|
83
141
|
...(type !== undefined ? { $type: type } : {}),
|
|
84
142
|
...(desc !== undefined ? { $description: desc } : {}),
|
|
85
143
|
};
|
|
144
|
+
|
|
145
|
+
const hybridLeaf = hybrids.get(path.join('.'));
|
|
146
|
+
if (hybridLeaf === undefined) return token;
|
|
147
|
+
|
|
148
|
+
// Hybrid node: becomes a group, its own value moves to a child token.
|
|
149
|
+
return {
|
|
150
|
+
...Object.fromEntries(
|
|
151
|
+
Object.entries(rest).map(([key, child]) => [key, convertLegacyNode(child, [...path, key], hybrids)])
|
|
152
|
+
),
|
|
153
|
+
[hybridLeaf]: token,
|
|
154
|
+
};
|
|
86
155
|
}
|
|
87
156
|
|
|
88
157
|
// Group node: recurse into children.
|
|
89
158
|
return Object.fromEntries(
|
|
90
|
-
Object.entries(node).map(([key, child]) => [key, convertLegacyNode(child)])
|
|
159
|
+
Object.entries(node).map(([key, child]) => [key, convertLegacyNode(child, [...path, key], hybrids)])
|
|
91
160
|
);
|
|
92
161
|
};
|
|
93
162
|
|
|
163
|
+
/** @param {string} contents @param {string} filePath @returns {unknown} */
|
|
164
|
+
const parseLegacyFile = (contents, filePath) => {
|
|
165
|
+
const tree = JSON.parse(contents);
|
|
166
|
+
return convertLegacyNode(tree, [], collectHybridNodes(tree, [], filePath));
|
|
167
|
+
};
|
|
168
|
+
|
|
94
169
|
// Registers the parser globally on the StyleDictionary class. To actually
|
|
95
170
|
// run it, a Style Dictionary instance must also opt in via
|
|
96
171
|
// `parsers: [LEGACY_TOKENS_PARSER_NAME]` in its config (registering alone
|
|
@@ -100,6 +175,6 @@ export const registerLegacyTokensParser = () => {
|
|
|
100
175
|
StyleDictionary.registerParser({
|
|
101
176
|
name: LEGACY_TOKENS_PARSER_NAME,
|
|
102
177
|
pattern: /\.json$/,
|
|
103
|
-
parser: ({ contents }) =>
|
|
178
|
+
parser: ({ contents, filePath }) => parseLegacyFile(contents, filePath ?? ''),
|
|
104
179
|
});
|
|
105
180
|
};
|
|
@@ -83,14 +83,17 @@ import { buildSourceModes } from './build-tokens-src/build-source-modes.mjs';
|
|
|
83
83
|
// untouched. Files are written flat in jsonBuildPath (no subdirectories), so
|
|
84
84
|
// only its top level is scanned. Runs regardless of sourceModes.
|
|
85
85
|
if (jsonBuildPath) {
|
|
86
|
+
// Local const: narrowing of the imported `jsonBuildPath` binding is lost
|
|
87
|
+
// inside the .map() callback below.
|
|
88
|
+
const jsonDir = jsonBuildPath;
|
|
86
89
|
const { readdir, rm, mkdir } = await import('fs/promises');
|
|
87
|
-
const existingEntries = await readdir(
|
|
90
|
+
const existingEntries = await readdir(jsonDir, { withFileTypes: true }).catch(() => []);
|
|
88
91
|
await Promise.all(
|
|
89
92
|
existingEntries
|
|
90
93
|
.filter((entry) => entry.isFile() && /\.jsonc?$/.test(entry.name))
|
|
91
|
-
.map((entry) => rm(path.join(
|
|
94
|
+
.map((entry) => rm(path.join(jsonDir, entry.name)))
|
|
92
95
|
);
|
|
93
|
-
await mkdir(
|
|
96
|
+
await mkdir(jsonDir, { recursive: true });
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
// ── 7. Build ──────────────────────────────────────────────────────────────────
|
|
@@ -58,7 +58,6 @@ async function run() {
|
|
|
58
58
|
|
|
59
59
|
// Path of the generated CSS file containing all custom property definitions
|
|
60
60
|
const custom_prop_file_path = path.resolve(configDir, path.join(config.buildPath, config.destFile));
|
|
61
|
-
const custom_prop_filename = path.basename(custom_prop_file_path);
|
|
62
61
|
|
|
63
62
|
// Directory to scan for CSS files
|
|
64
63
|
const checkdir = path.resolve(configDir, config.dirToCheck);
|
|
@@ -69,7 +68,7 @@ async function run() {
|
|
|
69
68
|
|
|
70
69
|
// Collect all .css files in the scan directory, excluding any file under a
|
|
71
70
|
// directory whose name starts with "TODO" (work-in-progress folders).
|
|
72
|
-
// The token file itself (
|
|
71
|
+
// The token file itself (custom_prop_file_path) is intentionally included:
|
|
73
72
|
// custom properties are often composed from other custom properties
|
|
74
73
|
// (e.g. `--malert-box-shadow: ... var(--malert-box-shadow-color) ...`)
|
|
75
74
|
// and those internal var() references must count as usage, otherwise the
|
|
@@ -40,12 +40,14 @@ const config = {
|
|
|
40
40
|
// .jsonc, reserved for hand-authored DTCG sources. These .json files are
|
|
41
41
|
// parsed with plain JSON.parse (no comments, no trailing commas).
|
|
42
42
|
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
43
|
+
// A legacy node that is at the same time a token (own value/type) AND a
|
|
44
|
+
// group with nested children (e.g. Open Props' other.ease.out, which has
|
|
45
|
+
// its own value plus out.1 ... out.5) cannot be represented in DTCG v5 —
|
|
46
|
+
// a node with $value is always a leaf. Its own value is moved to a child
|
|
47
|
+
// token named `default` (other.ease.out.default, i.e. --other-ease-out-default),
|
|
48
|
+
// or `base` if a `default` child already exists; if both exist the build
|
|
49
|
+
// fails. References to such a node from the same file are rewritten
|
|
50
|
+
// accordingly; from other files they must use the child name explicitly.
|
|
49
51
|
//
|
|
50
52
|
// WARNING: do not use path.join() to build glob patterns — it may corrupt
|
|
51
53
|
// the pattern syntax. Use template literals instead:
|