@massimo-cassandro/minimo 1.38.5 → 1.39.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/dev-tools/design-tokens-builder/README.md +36 -1
- package/dev-tools/design-tokens-builder/build-tokens-src/build-source-modes.mjs +8 -3
- package/dev-tools/design-tokens-builder/build-tokens-src/config.mjs +12 -6
- package/dev-tools/design-tokens-builder/build-tokens-src/legacy-tokens-parser.mjs +4 -1
- package/dev-tools/design-tokens-builder/build-tokens-src/source-prefixes.mjs +27 -7
- package/dev-tools/design-tokens-builder/build-tokens-src/source-transforms.mjs +195 -0
- package/dev-tools/design-tokens-builder/build-tokens-src/token-rename-parser.mjs +257 -0
- package/dev-tools/design-tokens-builder/build-tokens.mjs +32 -2
- package/dev-tools/design-tokens-builder/check-unresolved-custom-props.mjs +22 -6
- package/dev-tools/design-tokens-builder/tokens-config-sample.mjs +57 -10
- package/dev-tools/starter-kit/source_files/root/webpack-config-modules/css-rules.mjs +9 -0
- package/dev-tools/starter-kit/webpack-config-upg.md +1 -1
- package/package.json +2 -1
|
@@ -4,6 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
A set of Node.js scripts for managing CSS design tokens in a project. Built on top of [Style Dictionary v5](https://styledictionary.com/).
|
|
6
6
|
|
|
7
|
+
### Purpose
|
|
8
|
+
|
|
9
|
+
The purpose of this builder is to merge minimo's own design tokens with the tokens used in a project — typically coming from Figma or Penpot — and produce the custom properties used during development.
|
|
10
|
+
|
|
11
|
+
The goal is to keep the project's token naming as close as possible to minimo's own, so that minimo's components can be customized without any further intervention.
|
|
12
|
+
|
|
13
|
+
The best strategy is to name variables in Figma/Penpot after minimo's own names from the start, so that overriding a value stays a transparent, automatic process.
|
|
14
|
+
|
|
15
|
+
In a preexisting project, though, some names may not match (e.g. the project's base color is named `primary`, while minimo's equivalent is `primary.100`). Rather than fixing the generated custom properties by hand after every build, a source entry can carry a `transform` option (see [Source token renaming](#source-token-renaming-transform) below) that remaps token names to minimo's own naming before they are even handed to Style Dictionary.
|
|
16
|
+
|
|
7
17
|
### Scripts
|
|
8
18
|
|
|
9
19
|
| Script | Purpose |
|
|
@@ -61,7 +71,7 @@ npx buildTokens --config ./path/to/tokens-config.mjs
|
|
|
61
71
|
The `source` array accepts any combination of:
|
|
62
72
|
- Concrete file paths: `'./tokens/colors.jsonc'`
|
|
63
73
|
- Glob patterns: `'./tokens/**/*.{json,jsonc,mjs}'`
|
|
64
|
-
- `{ src, prefix }` objects, to add a prefix to the custom properties of those files (see [Source prefixes](#source-prefixes))
|
|
74
|
+
- `{ src, prefix, transform }` objects, to add a prefix to the custom properties of those files (see [Source prefixes](#source-prefixes)) and/or to rename token nodes within those files (see [Source token renaming](#source-token-renaming-transform))
|
|
65
75
|
|
|
66
76
|
**Supported formats:**
|
|
67
77
|
- `.json` / `.jsonc` — parsed directly as token data
|
|
@@ -92,6 +102,31 @@ With this config Open Props' `gray.0` becomes `--op-gray-0`. `prefix` accepts `'
|
|
|
92
102
|
|
|
93
103
|
See `build-tokens-src/source-prefixes.mjs`.
|
|
94
104
|
|
|
105
|
+
#### Source token renaming (`transform`)
|
|
106
|
+
|
|
107
|
+
An entry of `source` (or of a `sourceModes` mode) can also carry a `transform` object: a token found at a given dot-path in those files is moved to another dot-path, **before** Style Dictionary resolves `{references}` or builds the CSS/JSON output — unlike `prefix` above, which is a purely cosmetic CSS name change applied after the fact.
|
|
108
|
+
|
|
109
|
+
Typical use case: a preexisting project (e.g. driven by Figma/Penpot) names its own base color `primary`, while minimo's own tokens reference `{primary.100}`. Instead of renaming the generated custom property by hand after every build, or hunting down every minimo reference to change it, `transform` fixes the token's own path once, in config:
|
|
110
|
+
|
|
111
|
+
```js
|
|
112
|
+
source: [
|
|
113
|
+
'./my-tokens/*.mjs',
|
|
114
|
+
{ src: './project-tokens/*.jsonc', transform: { primary: 'primary.100' } },
|
|
115
|
+
],
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
With this config, whatever is defined at dot-path `primary` in the matched files is moved to `primary.100` — so it becomes `--primary-100` in the generated CSS, and every existing minimo reference to `{primary.100}` resolves correctly.
|
|
119
|
+
|
|
120
|
+
- **Notation:** both sides of the map use Style Dictionary's own dot-path notation (`'primary.100'`), not the hyphen-joined custom-property notation (`'primary-100'`). Token name segments in this project can themselves contain hyphens (e.g. `btn-close`), so a hyphen can't reliably tell a path separator from a literal part of a segment name — a dot can, since it's already reserved by Style Dictionary as the `{reference}` path separator.
|
|
121
|
+
- **References are rewritten automatically:** once every source file is merged, any `{reference}` to the token's *old* path — in the same file or a completely different one — is rewritten tree-wide to the *new* path (e.g. every `{primary}` in the build becomes `{primary.100}`). This is safe because Style Dictionary resolves `{references}` against the single merged token tree by absolute path: if `primary` existed exactly once, every `{primary}` anywhere unambiguously pointed at that one node.
|
|
122
|
+
- **Exception:** if the *same* source key (e.g. `primary`) is given a *different* destination in two different files' `transform` maps, each rename still works on its own, but the bare reference `{primary}` becomes ambiguous — which destination should it point to? In that case the automatic rewrite is skipped for that key (a warning is logged), and every `{primary}` reference must be fixed by hand to point at the correct one of the two new paths.
|
|
123
|
+
- A source key not found in the file only logs a warning; it does not fail the build.
|
|
124
|
+
- Applies to **both** the CSS and the JSON output (see [JSON output](#json-output) below), since both are rebuilt from the token's path, by then already renamed — no separate handling is needed.
|
|
125
|
+
- Two entries giving different destinations for the same source key in the same file fail the build with an error.
|
|
126
|
+
- `check-unresolved-custom-props.mjs` understands the same syntax.
|
|
127
|
+
|
|
128
|
+
See `build-tokens-src/source-transforms.mjs` and `build-tokens-src/token-rename-parser.mjs`.
|
|
129
|
+
|
|
95
130
|
|
|
96
131
|
#### Token file format (JS example)
|
|
97
132
|
|
|
@@ -36,7 +36,6 @@ import { CSS_TRANSFORMS, JSON_TRANSFORMS } from './platforms.mjs';
|
|
|
36
36
|
import { customPropsCount, lastFinalProps } from './formats/css.mjs';
|
|
37
37
|
import { buildJsonFiles, collectConcreteFilePaths } from './formats/json.mjs';
|
|
38
38
|
import { loadExistingCustomPropsScoped } from './merge-css.mjs';
|
|
39
|
-
import { LEGACY_TOKENS_PARSER_NAME } from './legacy-tokens-parser.mjs';
|
|
40
39
|
import { applyLightDarkFunc } from './light-dark.mjs';
|
|
41
40
|
|
|
42
41
|
/**
|
|
@@ -55,6 +54,8 @@ import { applyLightDarkFunc } from './light-dark.mjs';
|
|
|
55
54
|
* @param {boolean|(string|RegExp)[]} opts.mergeCustomProps true = merge all sources, array = only the matching token source files
|
|
56
55
|
* @param {boolean} opts.useLightDarkFunc reconcile shared `light`/`dark` properties into `light-dark()` calls (default: true — see config.mjs and light-dark.mjs). Ignored unless sourceModes defines both a `light` and a `dark` key.
|
|
57
56
|
* @param {string} opts.customPropsSelector CSS selector wrapping the generated block(s) (default: ':root' — see config.mjs)
|
|
57
|
+
* @param {string[]} opts.parserNames Style Dictionary parser names to opt every instance into (legacy bridge, plus the token-rename parser when at least one source has a `transform` — see build-tokens.mjs)
|
|
58
|
+
* @param {string[]} opts.preprocessorNames Style Dictionary preprocessor names to opt every instance into (the token-rename reference rewriter, when at least one source has a `transform` — see build-tokens.mjs)
|
|
58
59
|
* @returns {Promise<{cssDestPath: string, totalCustomProps: number, jsonFilesByMode: Record<string, object[]>}>}
|
|
59
60
|
*/
|
|
60
61
|
export const buildSourceModes = async ({
|
|
@@ -72,6 +73,8 @@ export const buildSourceModes = async ({
|
|
|
72
73
|
mergeCustomProps,
|
|
73
74
|
useLightDarkFunc,
|
|
74
75
|
customPropsSelector,
|
|
76
|
+
parserNames,
|
|
77
|
+
preprocessorNames,
|
|
75
78
|
}) => {
|
|
76
79
|
const modeNames = Object.keys(sourceModes);
|
|
77
80
|
|
|
@@ -112,7 +115,8 @@ export const buildSourceModes = async ({
|
|
|
112
115
|
const sd = new StyleDictionary({
|
|
113
116
|
include,
|
|
114
117
|
source: sourceModes[mode],
|
|
115
|
-
parsers:
|
|
118
|
+
parsers: parserNames,
|
|
119
|
+
preprocessors: preprocessorNames,
|
|
116
120
|
log: { verbosity: 'verbose' },
|
|
117
121
|
platforms: {
|
|
118
122
|
css: {
|
|
@@ -150,7 +154,8 @@ export const buildSourceModes = async ({
|
|
|
150
154
|
const jsonSd = new StyleDictionary({
|
|
151
155
|
include,
|
|
152
156
|
source: sourceModes[mode],
|
|
153
|
-
parsers:
|
|
157
|
+
parsers: parserNames,
|
|
158
|
+
preprocessors: preprocessorNames,
|
|
154
159
|
log: { verbosity: 'silent' },
|
|
155
160
|
platforms: {
|
|
156
161
|
json: {
|
|
@@ -150,24 +150,30 @@ export const customPropsGroups = Array.isArray(buildConfig.customPropsGroups)
|
|
|
150
150
|
// (buildConfig.sourceModes is a non-null object), `source` is ignored and
|
|
151
151
|
// each mode's own array of source patterns is resolved the same way.
|
|
152
152
|
//
|
|
153
|
-
// Entries of `source` / of each mode can also be `{ src, prefix }`
|
|
154
|
-
// to render the custom properties of those files with a prefix
|
|
155
|
-
// source-prefixes.mjs)
|
|
156
|
-
//
|
|
157
|
-
//
|
|
153
|
+
// Entries of `source` / of each mode can also be `{ src, prefix, transform }`
|
|
154
|
+
// objects, to render the custom properties of those files with a prefix
|
|
155
|
+
// (see source-prefixes.mjs) and/or to rename token nodes within those files
|
|
156
|
+
// (see source-transforms.mjs). They are flattened here into plain patterns
|
|
157
|
+
// for Style Dictionary; the prefixed/transformed ones are also collected in
|
|
158
|
+
// `prefixedSources`/`transformedSources`, to be registered by
|
|
159
|
+
// build-tokens.mjs.
|
|
158
160
|
// ---------------------------------------------------------------------------
|
|
159
161
|
const sourceModesRaw = buildConfig.sourceModes ?? null;
|
|
160
162
|
|
|
161
163
|
/** @type {import('./source-prefixes.mjs').PrefixedSource[]} */
|
|
162
164
|
export const prefixedSources = [];
|
|
163
165
|
|
|
166
|
+
/** @type {import('./source-prefixes.mjs').TransformedSource[]} */
|
|
167
|
+
export const transformedSources = [];
|
|
168
|
+
|
|
164
169
|
/**
|
|
165
170
|
* @param {Parameters<typeof splitSourceEntries>[0]} entries
|
|
166
171
|
* @returns {string[]}
|
|
167
172
|
*/
|
|
168
173
|
const parseSourceEntries = (entries) => {
|
|
169
|
-
const { patterns, prefixed } = splitSourceEntries(entries, configDir);
|
|
174
|
+
const { patterns, prefixed, transformed } = splitSourceEntries(entries, configDir);
|
|
170
175
|
prefixedSources.push(...prefixed);
|
|
176
|
+
transformedSources.push(...transformed);
|
|
171
177
|
return patterns;
|
|
172
178
|
};
|
|
173
179
|
|
|
@@ -160,8 +160,11 @@ const convertLegacyNode = (node, path, hybrids) => {
|
|
|
160
160
|
);
|
|
161
161
|
};
|
|
162
162
|
|
|
163
|
+
// Exported for reuse by token-rename-parser.mjs, which needs the same
|
|
164
|
+
// legacy-or-DTCG .json loading (hybrid-node handling included) for the
|
|
165
|
+
// subset of files that also have a `transform` rename map registered.
|
|
163
166
|
/** @param {string} contents @param {string} filePath @returns {unknown} */
|
|
164
|
-
const parseLegacyFile = (contents, filePath) => {
|
|
167
|
+
export const parseLegacyFile = (contents, filePath) => {
|
|
165
168
|
const tree = JSON.parse(contents);
|
|
166
169
|
return convertLegacyNode(tree, [], collectHybridNodes(tree, [], filePath));
|
|
167
170
|
};
|
|
@@ -18,11 +18,18 @@
|
|
|
18
18
|
// (token.filePath), while `src` may contain glob patterns: registerSourcePrefixes()
|
|
19
19
|
// expands them (through Style Dictionary itself, so the result matches
|
|
20
20
|
// token.filePath exactly) into a file path -> prefix map.
|
|
21
|
+
//
|
|
22
|
+
// The same entry object can also carry a `transform` map (renaming/moving
|
|
23
|
+
// token nodes within the file's tree, e.g. { primary: 'primary.100' }) — see
|
|
24
|
+
// source-transforms.mjs / token-rename-parser.mjs. splitSourceEntries() below
|
|
25
|
+
// extracts both `prefix` and `transform` from the same `{ src, ... }` shape,
|
|
26
|
+
// since they share the same source-matching syntax.
|
|
21
27
|
|
|
22
28
|
import StyleDictionary from 'style-dictionary';
|
|
23
29
|
import { LEGACY_TOKENS_PARSER_NAME } from './legacy-tokens-parser.mjs';
|
|
24
30
|
import { resolveSourcePaths } from './resolve-source-paths.mjs';
|
|
25
31
|
import { collectConcreteFilePaths } from './formats/json.mjs';
|
|
32
|
+
import { normalizeTransformMap } from './source-transforms.mjs';
|
|
26
33
|
|
|
27
34
|
/**
|
|
28
35
|
* @typedef {object} PrefixedSource
|
|
@@ -47,20 +54,29 @@ export const normalizePrefix = (prefix) => {
|
|
|
47
54
|
return value;
|
|
48
55
|
};
|
|
49
56
|
|
|
57
|
+
/**
|
|
58
|
+
* @typedef {object} TransformedSource
|
|
59
|
+
* @property {string[]} src Source paths/globs, already resolved
|
|
60
|
+
* @property {Record<string,string>} transform Validated rename map (see source-transforms.mjs)
|
|
61
|
+
*/
|
|
62
|
+
|
|
50
63
|
/**
|
|
51
64
|
* Splits a `source` (or `sourceModes[mode]`) array, whose entries can be
|
|
52
|
-
* strings or `{ src, prefix }` objects, into the flat list of
|
|
53
|
-
* to Style Dictionary
|
|
54
|
-
*
|
|
55
|
-
*
|
|
65
|
+
* strings or `{ src, prefix, transform }` objects, into the flat list of
|
|
66
|
+
* patterns to pass to Style Dictionary, the list of the prefixed entries and
|
|
67
|
+
* the list of the transformed (renamed) entries. Patterns are resolved
|
|
68
|
+
* relative to baseDir (see resolveSourcePaths).
|
|
69
|
+
* @param {(string|{src: string|string[], prefix?: string, transform?: Record<string,string>})[]} entries
|
|
56
70
|
* @param {string} baseDir
|
|
57
|
-
* @returns {{patterns: string[], prefixed: PrefixedSource[]}}
|
|
71
|
+
* @returns {{patterns: string[], prefixed: PrefixedSource[], transformed: TransformedSource[]}}
|
|
58
72
|
*/
|
|
59
73
|
export const splitSourceEntries = (entries, baseDir) => {
|
|
60
74
|
/** @type {string[]} */
|
|
61
75
|
const patterns = [];
|
|
62
76
|
/** @type {PrefixedSource[]} */
|
|
63
77
|
const prefixed = [];
|
|
78
|
+
/** @type {TransformedSource[]} */
|
|
79
|
+
const transformed = [];
|
|
64
80
|
|
|
65
81
|
for (const entry of entries ?? []) {
|
|
66
82
|
if (typeof entry === 'string') {
|
|
@@ -73,7 +89,7 @@ export const splitSourceEntries = (entries, baseDir) => {
|
|
|
73
89
|
if (!isObjectEntry) {
|
|
74
90
|
throw new Error(
|
|
75
91
|
`[build-tokens] config: invalid source entry ${JSON.stringify(entry)} `
|
|
76
|
-
+ '(expected a string or an object { src, prefix })'
|
|
92
|
+
+ '(expected a string or an object { src, prefix, transform })'
|
|
77
93
|
);
|
|
78
94
|
}
|
|
79
95
|
|
|
@@ -83,9 +99,13 @@ export const splitSourceEntries = (entries, baseDir) => {
|
|
|
83
99
|
if (entry.prefix !== undefined && entry.prefix !== null && entry.prefix !== '') {
|
|
84
100
|
prefixed.push({ src, prefix: normalizePrefix(entry.prefix) });
|
|
85
101
|
}
|
|
102
|
+
|
|
103
|
+
if (entry.transform !== undefined && entry.transform !== null) {
|
|
104
|
+
transformed.push({ src, transform: normalizeTransformMap(entry.transform) });
|
|
105
|
+
}
|
|
86
106
|
}
|
|
87
107
|
|
|
88
|
-
return { patterns, prefixed };
|
|
108
|
+
return { patterns, prefixed, transformed };
|
|
89
109
|
};
|
|
90
110
|
|
|
91
111
|
/**
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// build-tokens-src/source-transforms.mjs
|
|
2
|
+
// Supports per-source token renaming: an entry of `source` (or of a
|
|
3
|
+
// `sourceModes` mode) can carry a `transform` object next to (or instead of)
|
|
4
|
+
// `prefix` — see splitSourceEntries() in source-prefixes.mjs — e.g.
|
|
5
|
+
//
|
|
6
|
+
// { src: './project-tokens/*.jsonc', transform: { primary: 'primary.100' } }
|
|
7
|
+
//
|
|
8
|
+
// so that the token found at dot-path `primary` in those files is moved to
|
|
9
|
+
// dot-path `primary.100` before Style Dictionary ever sees it, i.e. before
|
|
10
|
+
// {references} are resolved and before the CSS/JSON output is built.
|
|
11
|
+
//
|
|
12
|
+
// Typical use case: a preexisting project defines its tokens (e.g. exported
|
|
13
|
+
// from Figma/Penpot) under names that don't match minimo's own naming (e.g.
|
|
14
|
+
// the project's base color is `primary`, minimo's equivalent is
|
|
15
|
+
// `primary.100`). Without this feature every custom property generated from
|
|
16
|
+
// the project's tokens would have to be renamed by hand after each build, or
|
|
17
|
+
// every minimo reference to `{primary.100}` would have to be tracked down and
|
|
18
|
+
// changed instead. `transform` fixes the token's OWN path once, in config.
|
|
19
|
+
//
|
|
20
|
+
// Notation: both sides of the map use Style Dictionary's own dot-path
|
|
21
|
+
// notation (e.g. 'primary.100'), NOT the hyphen-joined custom-property
|
|
22
|
+
// notation (e.g. 'primary-100'). Token name segments in this project can
|
|
23
|
+
// themselves contain hyphens (e.g. btn-close, status-buttons), so a hyphen
|
|
24
|
+
// can't reliably tell a path separator from a literal part of a segment name;
|
|
25
|
+
// a dot can, since Style Dictionary already reserves it as the path
|
|
26
|
+
// separator in {references} and never allows it inside a single segment
|
|
27
|
+
// name.
|
|
28
|
+
//
|
|
29
|
+
// Moving a node would otherwise leave every {reference} to its OLD path
|
|
30
|
+
// dangling (in the same file or a completely different one) — so this module
|
|
31
|
+
// also collects a flat, file-independent from -> to map (getAllRenames()),
|
|
32
|
+
// used by the token-rename PREPROCESSOR (see token-rename-parser.mjs) to
|
|
33
|
+
// rewrite every such {reference} tree-wide, once all files are merged. This
|
|
34
|
+
// is safe to do because Style Dictionary resolves {references} against the
|
|
35
|
+
// single merged token tree by absolute path: if `primary` existed exactly
|
|
36
|
+
// once before the rename, every {primary} anywhere in the build
|
|
37
|
+
// unambiguously pointed at that one node, so every one of them must now
|
|
38
|
+
// become {primary.100} — there is no risk of rewriting an unrelated
|
|
39
|
+
// reference. The one case this can't disambiguate is the same "from" key
|
|
40
|
+
// renamed to a DIFFERENT destination in a different file (each valid on its
|
|
41
|
+
// own, per-file); see registerSourceTransforms() below — that key is
|
|
42
|
+
// excluded from the tree-wide rewrite (with a console warning), and its
|
|
43
|
+
// {references} are left for the user to fix by hand.
|
|
44
|
+
//
|
|
45
|
+
// How it works: unlike `prefix` (a purely cosmetic CSS name transform, see
|
|
46
|
+
// source-prefixes.mjs, which never touches the token tree), `transform`
|
|
47
|
+
// restructures the actual parsed token tree of the source file, via a
|
|
48
|
+
// dedicated Style Dictionary parser (see token-rename-parser.mjs) that runs
|
|
49
|
+
// at parse time, before files are merged and before reference resolution.
|
|
50
|
+
// Since both the CSS and JSON output formats rebuild their tree from
|
|
51
|
+
// token.path (by then already renamed), no separate handling is needed to
|
|
52
|
+
// make the JSON output reflect the renamed structure too.
|
|
53
|
+
|
|
54
|
+
import StyleDictionary from 'style-dictionary';
|
|
55
|
+
import { collectConcreteFilePaths } from './formats/json.mjs';
|
|
56
|
+
import { LEGACY_TOKENS_PARSER_NAME } from './legacy-tokens-parser.mjs';
|
|
57
|
+
|
|
58
|
+
/** @type {Map<string, Record<string,string>>} */
|
|
59
|
+
let transformByFile = new Map();
|
|
60
|
+
|
|
61
|
+
// Flat, file-independent from -> to map, used by the token-rename
|
|
62
|
+
// preprocessor (see token-rename-parser.mjs) to rewrite any {reference}
|
|
63
|
+
// pointing to a renamed token's OLD path, anywhere in the build (same file or
|
|
64
|
+
// another one) — references are resolved by Style Dictionary against the
|
|
65
|
+
// single merged token tree, by absolute path, so a bare dot-path is
|
|
66
|
+
// unambiguous UNLESS the same "from" key was given a different destination
|
|
67
|
+
// in a different file (each valid on its own — see registerSourceTransforms
|
|
68
|
+
// below), in which case that key is excluded here (with a warning) and its
|
|
69
|
+
// {references} are left for the user to fix by hand, same as before this
|
|
70
|
+
// preprocessor existed.
|
|
71
|
+
/** @type {Map<string, string>} */
|
|
72
|
+
let globalRenames = new Map();
|
|
73
|
+
|
|
74
|
+
const DOT_PATH = /^[^.\s][\w-]*(\.[^.\s][\w-]*)*$/;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Validates a `transform` map: every key/value must be a non-empty
|
|
78
|
+
* Style-Dictionary dot-path string (e.g. 'primary.100'), not a
|
|
79
|
+
* hyphen-joined custom-property name (e.g. 'primary-100') — see the notation
|
|
80
|
+
* rationale above.
|
|
81
|
+
* @param {unknown} map
|
|
82
|
+
* @returns {Record<string,string>}
|
|
83
|
+
* @throws {Error} if the map is not a plain object, or a key/value is invalid
|
|
84
|
+
*/
|
|
85
|
+
export const normalizeTransformMap = (map) => {
|
|
86
|
+
if (map === null || typeof map !== 'object' || Array.isArray(map)) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`[build-tokens] config: invalid "transform" value ${JSON.stringify(map)} (expected an object)`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const [from, to] of Object.entries(map)) {
|
|
93
|
+
if (!DOT_PATH.test(from) || !DOT_PATH.test(String(to))) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`[build-tokens] config: invalid "transform" entry "${from}": "${to}" `
|
|
96
|
+
+ '(expected Style Dictionary dot-path strings, e.g. { "primary": "primary.100" })'
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return map;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Expands the `transform` entries' `src` patterns into concrete file paths
|
|
106
|
+
* and stores the file path -> rename map used by getSourceTransform(). Must
|
|
107
|
+
* be called after registerLegacyTokensParser() and before any real build.
|
|
108
|
+
* Maps from different entries touching the same file are merged; the build
|
|
109
|
+
* fails only if two entries disagree on the destination of the same source
|
|
110
|
+
* key.
|
|
111
|
+
* @param {import('./source-prefixes.mjs').TransformedSource[]} transformedSources
|
|
112
|
+
* @returns {Promise<void>}
|
|
113
|
+
* @throws {Error} if the same file gets two conflicting destinations for the same key
|
|
114
|
+
*/
|
|
115
|
+
export const registerSourceTransforms = async (transformedSources) => {
|
|
116
|
+
/** @type {Map<string, Record<string,string>>} */
|
|
117
|
+
const map = new Map();
|
|
118
|
+
// Every destination seen for a given "from" key, across all files — used
|
|
119
|
+
// below to build globalRenames, and to detect the cross-file ambiguous
|
|
120
|
+
// case (same "from" key, different destination in a different file).
|
|
121
|
+
/** @type {Map<string, Set<string>>} */
|
|
122
|
+
const destinationsByFrom = new Map();
|
|
123
|
+
|
|
124
|
+
for (const { src, transform } of transformedSources) {
|
|
125
|
+
const sd = new StyleDictionary({
|
|
126
|
+
source: src,
|
|
127
|
+
parsers: [LEGACY_TOKENS_PARSER_NAME],
|
|
128
|
+
log: { verbosity: 'silent' },
|
|
129
|
+
platforms: {},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
for (const filePath of await collectConcreteFilePaths(sd)) {
|
|
133
|
+
const existing = map.get(filePath) ?? {};
|
|
134
|
+
for (const [from, to] of Object.entries(transform)) {
|
|
135
|
+
if (existing[from] !== undefined && existing[from] !== to) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`[build-tokens] config: "${filePath}" has two different transform destinations `
|
|
138
|
+
+ `for "${from}" ("${existing[from]}" and "${to}")`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
existing[from] = to;
|
|
142
|
+
|
|
143
|
+
if (!destinationsByFrom.has(from)) destinationsByFrom.set(from, new Set());
|
|
144
|
+
destinationsByFrom.get(from).add(to);
|
|
145
|
+
}
|
|
146
|
+
map.set(filePath, existing);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
transformByFile = map;
|
|
151
|
+
|
|
152
|
+
const global = new Map();
|
|
153
|
+
for (const [from, destinations] of destinationsByFrom) {
|
|
154
|
+
if (destinations.size > 1) {
|
|
155
|
+
// eslint-disable-next-line no-console
|
|
156
|
+
console.warn(
|
|
157
|
+
`[build-tokens] transform: "${from}" is renamed to different destinations in different `
|
|
158
|
+
+ `files (${[...destinations].join(', ')}) — {${from}} references will NOT be `
|
|
159
|
+
+ 'auto-rewritten; update them by hand.'
|
|
160
|
+
);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
global.set(from, [...destinations][0]);
|
|
164
|
+
}
|
|
165
|
+
globalRenames = global;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @returns {Map<string, string>} the flat from -> to map used to rewrite
|
|
170
|
+
* {references} across the whole build (see token-rename-parser.mjs)
|
|
171
|
+
*/
|
|
172
|
+
export const getAllRenames = () => globalRenames;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @param {string|undefined} filePath token.filePath
|
|
176
|
+
* @returns {Record<string,string>|undefined} the rename map registered for that file, if any
|
|
177
|
+
*/
|
|
178
|
+
export const getSourceTransform = (filePath) => (filePath ? transformByFile.get(filePath) : undefined);
|
|
179
|
+
|
|
180
|
+
/** @returns {boolean} true if at least one file has a registered transform */
|
|
181
|
+
export const hasRegisteredTransforms = () => transformByFile.size > 0;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Builds the RegExp matching exactly the concrete files that have a
|
|
185
|
+
* registered transform, for the Style Dictionary parser `pattern` (see
|
|
186
|
+
* token-rename-parser.mjs). Returns null if no file has one.
|
|
187
|
+
* @returns {RegExp|null}
|
|
188
|
+
*/
|
|
189
|
+
export const transformedFilesPattern = () => {
|
|
190
|
+
const files = [...transformByFile.keys()];
|
|
191
|
+
if (files.length === 0) return null;
|
|
192
|
+
|
|
193
|
+
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
194
|
+
return new RegExp(`^(${files.map(escapeRegExp).join('|')})$`);
|
|
195
|
+
};
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// build-tokens-src/token-rename-parser.mjs
|
|
2
|
+
// Implements the `transform` feature end-to-end (see source-transforms.mjs):
|
|
3
|
+
//
|
|
4
|
+
// 1. A Style Dictionary PARSER that moves/renames token nodes within a
|
|
5
|
+
// source file's own tree, according to the `transform` map registered for
|
|
6
|
+
// that file, BEFORE Style Dictionary merges files together and resolves
|
|
7
|
+
// {references}.
|
|
8
|
+
// 2. A Style Dictionary PREPROCESSOR that, once every file has been parsed
|
|
9
|
+
// and merged into a single tree (but still before {references} are
|
|
10
|
+
// resolved), rewrites every {reference} in the WHOLE tree that points to
|
|
11
|
+
// one of the renamed tokens' OLD path, to its NEW path — so a reference
|
|
12
|
+
// doesn't have to be hunted down and updated by hand just because the
|
|
13
|
+
// token it points to was moved (in the same file or a completely
|
|
14
|
+
// different one; see getAllRenames() in source-transforms.mjs for why
|
|
15
|
+
// this is safe to do tree-wide).
|
|
16
|
+
//
|
|
17
|
+
// Only the exact concrete files that declare a `transform` are matched by
|
|
18
|
+
// the parser (the `pattern` built by transformedFilesPattern() in
|
|
19
|
+
// source-transforms.mjs); every other file is untouched by it and falls
|
|
20
|
+
// through to Style Dictionary's own built-in loader, or to the legacy bridge
|
|
21
|
+
// parser (legacy-tokens-parser.mjs) for plain .json sources. The
|
|
22
|
+
// preprocessor, instead, always runs over the entire merged tree once
|
|
23
|
+
// registered — it just has nothing to rewrite where no reference matches a
|
|
24
|
+
// renamed path.
|
|
25
|
+
//
|
|
26
|
+
// Style Dictionary's combineJSON always reads a source file's `contents` as
|
|
27
|
+
// raw text and hands it to whichever parser(s) match, regardless of
|
|
28
|
+
// extension (see node_modules/style-dictionary/lib/utils/combineJSON.js) — a
|
|
29
|
+
// custom parser never receives an already-executed .mjs module or an
|
|
30
|
+
// already-JSON5-parsed object. The parser therefore has to load the file
|
|
31
|
+
// itself the same way Style Dictionary's own loader (loadFile.js) would:
|
|
32
|
+
// dynamic `import()` for .mjs/.js, JSON5 for .jsonc/.json5, and the existing
|
|
33
|
+
// legacy-or-DTCG bridge (parseLegacyFile, reused as-is so hybrid-node
|
|
34
|
+
// handling stays consistent) for .json.
|
|
35
|
+
|
|
36
|
+
import StyleDictionary from 'style-dictionary';
|
|
37
|
+
import JSON5 from 'json5';
|
|
38
|
+
import * as path from 'node:path';
|
|
39
|
+
import { readFile } from 'node:fs/promises';
|
|
40
|
+
import { pathToFileURL } from 'node:url';
|
|
41
|
+
import { parseLegacyFile } from './legacy-tokens-parser.mjs';
|
|
42
|
+
import { getSourceTransform, getAllRenames } from './source-transforms.mjs';
|
|
43
|
+
|
|
44
|
+
export const TOKEN_RENAME_PARSER_NAME = 'minimo/token-rename';
|
|
45
|
+
export const TOKEN_RENAME_PREPROCESSOR_NAME = 'minimo/token-rename-refs';
|
|
46
|
+
|
|
47
|
+
/** @param {unknown} node @returns {node is Record<string, unknown>} */
|
|
48
|
+
const isPlainObject = (node) => node !== null && typeof node === 'object' && !Array.isArray(node);
|
|
49
|
+
|
|
50
|
+
/** @param {unknown} tree @param {string[]} pathSegments @returns {unknown} */
|
|
51
|
+
const getAtPath = (tree, pathSegments) => pathSegments.reduce(
|
|
52
|
+
(node, segment) => (isPlainObject(node) ? node[segment] : undefined),
|
|
53
|
+
tree
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// Removes the node at pathSegments (mutates tree), pruning parent groups left
|
|
57
|
+
// empty by the removal (innermost first), so a fully-emptied group doesn't
|
|
58
|
+
// linger as `{}` in the output tree.
|
|
59
|
+
/** @param {Record<string, unknown>} tree @param {string[]} pathSegments */
|
|
60
|
+
const deleteAtPath = (tree, pathSegments) => {
|
|
61
|
+
if (pathSegments.length === 0) return;
|
|
62
|
+
|
|
63
|
+
/** @type {Record<string, unknown>[]} */
|
|
64
|
+
const parents = [];
|
|
65
|
+
let node = /** @type {unknown} */ (tree);
|
|
66
|
+
for (let i = 0; i < pathSegments.length - 1; i++) {
|
|
67
|
+
if (!isPlainObject(node)) return;
|
|
68
|
+
parents.push(node);
|
|
69
|
+
node = node[pathSegments[i]];
|
|
70
|
+
}
|
|
71
|
+
if (!isPlainObject(node)) return;
|
|
72
|
+
|
|
73
|
+
delete node[pathSegments[pathSegments.length - 1]];
|
|
74
|
+
|
|
75
|
+
for (let i = pathSegments.length - 2; i >= 0; i--) {
|
|
76
|
+
const parent = parents[i];
|
|
77
|
+
const key = pathSegments[i];
|
|
78
|
+
if (isPlainObject(parent[key]) && Object.keys(parent[key]).length === 0) {
|
|
79
|
+
delete parent[key];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Inserts `value` at pathSegments (mutates tree), creating intermediate
|
|
85
|
+
// group objects as needed.
|
|
86
|
+
/** @param {Record<string, unknown>} tree @param {string[]} pathSegments @param {unknown} value */
|
|
87
|
+
const setAtPath = (tree, pathSegments, value) => {
|
|
88
|
+
let node = tree;
|
|
89
|
+
for (let i = 0; i < pathSegments.length - 1; i++) {
|
|
90
|
+
const segment = pathSegments[i];
|
|
91
|
+
if (!isPlainObject(node[segment])) node[segment] = {};
|
|
92
|
+
node = /** @type {Record<string, unknown>} */ (node[segment]);
|
|
93
|
+
}
|
|
94
|
+
node[pathSegments[pathSegments.length - 1]] = value;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// A build can create several StyleDictionary instances over the same source
|
|
98
|
+
// file (e.g. one throwaway pass to enumerate concrete file paths, one for the
|
|
99
|
+
// real build — see build-tokens.mjs), each re-running this parser. Tracked so
|
|
100
|
+
// a missing source key is only ever warned about once per process, instead of
|
|
101
|
+
// once per pass.
|
|
102
|
+
/** @type {Set<string>} */
|
|
103
|
+
const warnedMissingKeys = new Set();
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Applies a rename map to a parsed DTCG tree: moves each mapped source path's
|
|
107
|
+
* node to its destination path. Two phases (collect+delete, then insert) so
|
|
108
|
+
* that a destination path used by one entry and a source path used by
|
|
109
|
+
* another never interfere with each other depending on map order. A source
|
|
110
|
+
* key not found in the tree only produces a console warning — it does not
|
|
111
|
+
* fail the build, since a project config may target a key that's only
|
|
112
|
+
* sometimes present.
|
|
113
|
+
* @param {unknown} tree
|
|
114
|
+
* @param {Record<string,string>} renameMap dot-path -> dot-path
|
|
115
|
+
* @param {string} filePath only used for the warning message
|
|
116
|
+
* @returns {unknown}
|
|
117
|
+
*/
|
|
118
|
+
export const applyTokenRenameMap = (tree, renameMap, filePath) => {
|
|
119
|
+
if (!isPlainObject(tree)) return tree;
|
|
120
|
+
|
|
121
|
+
/** @type {{fromPath: string[], toPath: string[], node: unknown}[]} */
|
|
122
|
+
const moves = [];
|
|
123
|
+
|
|
124
|
+
for (const [from, to] of Object.entries(renameMap)) {
|
|
125
|
+
const fromPath = from.split('.');
|
|
126
|
+
const node = getAtPath(tree, fromPath);
|
|
127
|
+
if (node === undefined) {
|
|
128
|
+
const warnKey = `${filePath}::${from}`;
|
|
129
|
+
if (!warnedMissingKeys.has(warnKey)) {
|
|
130
|
+
warnedMissingKeys.add(warnKey);
|
|
131
|
+
// eslint-disable-next-line no-console
|
|
132
|
+
console.warn(
|
|
133
|
+
`[build-tokens] transform: source token "${from}" not found in ${filePath} — skipped`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
moves.push({ fromPath, toPath: to.split('.'), node });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const { fromPath } of moves) deleteAtPath(tree, fromPath);
|
|
142
|
+
for (const { toPath, node } of moves) setAtPath(tree, toPath, node);
|
|
143
|
+
|
|
144
|
+
return tree;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/** @param {string} filePath @returns {Promise<unknown>} */
|
|
148
|
+
const loadTree = async (filePath) => {
|
|
149
|
+
const ext = path.extname(filePath);
|
|
150
|
+
|
|
151
|
+
if (ext === '.mjs' || ext === '.js') {
|
|
152
|
+
const mod = await import(pathToFileURL(filePath).href);
|
|
153
|
+
try {
|
|
154
|
+
return structuredClone(mod.default);
|
|
155
|
+
} catch {
|
|
156
|
+
// Cloning may fail for content with non-cloneable values (e.g.
|
|
157
|
+
// functions) — fall back to the live module object, same as Style
|
|
158
|
+
// Dictionary's own loadFile.js.
|
|
159
|
+
return mod.default;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const contents = await readFile(filePath, 'utf-8');
|
|
164
|
+
|
|
165
|
+
if (ext === '.json') {
|
|
166
|
+
// Reuses the legacy-or-DTCG bridge so hybrid-node conversion stays
|
|
167
|
+
// consistent with every other .json source in the project.
|
|
168
|
+
return parseLegacyFile(contents, filePath);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// .jsonc, .json5, or anything else: the same JSON5 parsing Style
|
|
172
|
+
// Dictionary's own built-in loader uses for these extensions.
|
|
173
|
+
return JSON5.parse(contents);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// Registers the parser globally on the StyleDictionary class. To actually
|
|
177
|
+
// run it, a Style Dictionary instance must also opt in via
|
|
178
|
+
// `parsers: [..., TOKEN_RENAME_PARSER_NAME]` (see build-tokens.mjs,
|
|
179
|
+
// build-source-modes.mjs, check-unresolved-custom-props.mjs) — same
|
|
180
|
+
// activation model as the legacy bridge parser.
|
|
181
|
+
/** @param {RegExp} pattern from transformedFilesPattern() in source-transforms.mjs */
|
|
182
|
+
export const registerTokenRenameParser = (pattern) => {
|
|
183
|
+
StyleDictionary.registerParser({
|
|
184
|
+
name: TOKEN_RENAME_PARSER_NAME,
|
|
185
|
+
pattern,
|
|
186
|
+
parser: async ({ filePath }) => {
|
|
187
|
+
const tree = await loadTree(filePath ?? '');
|
|
188
|
+
const renameMap = getSourceTransform(filePath);
|
|
189
|
+
return renameMap ? applyTokenRenameMap(tree, renameMap, filePath ?? '') : tree;
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// ── Reference rewriting (preprocessor) ──────────────────────────────────────
|
|
195
|
+
//
|
|
196
|
+
// Matches a bare {reference} (not a CSS function containing one, e.g.
|
|
197
|
+
// "color-mix(in srgb, {primary} 60%, #000)" still matches the {primary} part
|
|
198
|
+
// only) — same pattern style as LEGACY_REF in legacy-tokens-parser.mjs.
|
|
199
|
+
const REF = /\{([^}]+)\}/g;
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Replaces every {reference} in `value` (a token's $value, possibly nested —
|
|
203
|
+
* composite/shadow/gradient objects, arrays) whose dot-path exactly matches a
|
|
204
|
+
* "from" key of `renames`, with `{<to>}`. References that don't match any
|
|
205
|
+
* renamed key (including partial/longer paths, e.g. {primary.200} when only
|
|
206
|
+
* "primary" was renamed) are left untouched.
|
|
207
|
+
* @param {unknown} value
|
|
208
|
+
* @param {Map<string,string>} renames
|
|
209
|
+
* @returns {unknown}
|
|
210
|
+
*/
|
|
211
|
+
const rewriteRefs = (value, renames) => {
|
|
212
|
+
if (typeof value === 'string') {
|
|
213
|
+
return value.replace(REF, (match, ref) => (renames.has(ref) ? `{${renames.get(ref)}}` : match));
|
|
214
|
+
}
|
|
215
|
+
if (Array.isArray(value)) return value.map((v) => rewriteRefs(v, renames));
|
|
216
|
+
if (isPlainObject(value)) {
|
|
217
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, rewriteRefs(v, renames)]));
|
|
218
|
+
}
|
|
219
|
+
return value;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Walks the merged token tree (every source file, already combined by Style
|
|
224
|
+
* Dictionary — see StyleDictionary.js's `preprocess()` call site) and, for
|
|
225
|
+
* every leaf token (a node with `$value`), rewrites {references} in its
|
|
226
|
+
* `$value` only — never touching `$type`, `filePath`, `isSource` or any other
|
|
227
|
+
* metadata Style Dictionary attaches to the node.
|
|
228
|
+
* @param {unknown} node
|
|
229
|
+
* @param {Map<string,string>} renames
|
|
230
|
+
* @returns {unknown}
|
|
231
|
+
*/
|
|
232
|
+
const rewriteTreeRefs = (node, renames) => {
|
|
233
|
+
if (!isPlainObject(node)) return node;
|
|
234
|
+
if (Object.hasOwn(node, '$value')) {
|
|
235
|
+
return { ...node, $value: rewriteRefs(node.$value, renames) };
|
|
236
|
+
}
|
|
237
|
+
return Object.fromEntries(
|
|
238
|
+
Object.entries(node).map(([key, child]) => [key, rewriteTreeRefs(child, renames)])
|
|
239
|
+
);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// Registers the preprocessor globally. To actually run it, a Style
|
|
243
|
+
// Dictionary instance must also opt in via
|
|
244
|
+
// `preprocessors: [TOKEN_RENAME_PREPROCESSOR_NAME]` (see build-tokens.mjs,
|
|
245
|
+
// build-source-modes.mjs, check-unresolved-custom-props.mjs). A no-op
|
|
246
|
+
// (returns the tree unchanged) when getAllRenames() is empty — e.g. every
|
|
247
|
+
// registered transform turned out to be file-ambiguous (see
|
|
248
|
+
// registerSourceTransforms() in source-transforms.mjs).
|
|
249
|
+
export const registerTokenRenamePreprocessor = () => {
|
|
250
|
+
StyleDictionary.registerPreprocessor({
|
|
251
|
+
name: TOKEN_RENAME_PREPROCESSOR_NAME,
|
|
252
|
+
preprocessor: (dictionary) => {
|
|
253
|
+
const renames = getAllRenames();
|
|
254
|
+
return renames.size ? rewriteTreeRefs(dictionary, renames) : dictionary;
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
};
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
// build-tokens-src/build-source-modes.mjs <- alternative build path used when sourceModes is set
|
|
16
16
|
// build-tokens-src/merge-css.mjs <- supports the mergeCustomProps option (flat and per-mode)
|
|
17
17
|
// build-tokens-src/source-prefixes.mjs <- supports `{ src, prefix }` entries in source/sourceModes
|
|
18
|
+
// build-tokens-src/source-transforms.mjs <- supports `{ src, transform }` entries in source/sourceModes
|
|
19
|
+
// build-tokens-src/token-rename-parser.mjs <- parser applying the `transform` rename maps
|
|
18
20
|
|
|
19
21
|
import StyleDictionary from 'style-dictionary';
|
|
20
22
|
import * as path from 'node:path';
|
|
@@ -37,6 +39,7 @@ import {
|
|
|
37
39
|
sourceModes,
|
|
38
40
|
sourceModesBase,
|
|
39
41
|
prefixedSources,
|
|
42
|
+
transformedSources,
|
|
40
43
|
mergeCustomProps,
|
|
41
44
|
customPropsGroups,
|
|
42
45
|
pxToRem,
|
|
@@ -61,6 +64,29 @@ registerLegacyTokensParser();
|
|
|
61
64
|
import { registerSourcePrefixes } from './build-tokens-src/source-prefixes.mjs';
|
|
62
65
|
await registerSourcePrefixes(prefixedSources);
|
|
63
66
|
|
|
67
|
+
// ── 2d. Per-source token rename maps ─────────────────────────────────────────
|
|
68
|
+
// Expands the `{ src, transform }` entries of source/sourceModes into the
|
|
69
|
+
// file -> rename-map read by the token-rename parser, and registers that
|
|
70
|
+
// parser (only if at least one file actually has a transform) — see
|
|
71
|
+
// source-transforms.mjs / token-rename-parser.mjs. `parserNames` is reused
|
|
72
|
+
// below and passed down to buildSourceModes() so every Style Dictionary
|
|
73
|
+
// instance opts in consistently.
|
|
74
|
+
import { registerSourceTransforms, transformedFilesPattern, hasRegisteredTransforms } from './build-tokens-src/source-transforms.mjs';
|
|
75
|
+
import {
|
|
76
|
+
TOKEN_RENAME_PARSER_NAME,
|
|
77
|
+
registerTokenRenameParser,
|
|
78
|
+
TOKEN_RENAME_PREPROCESSOR_NAME,
|
|
79
|
+
registerTokenRenamePreprocessor,
|
|
80
|
+
} from './build-tokens-src/token-rename-parser.mjs';
|
|
81
|
+
await registerSourceTransforms(transformedSources);
|
|
82
|
+
const tokenRenamePattern = transformedFilesPattern();
|
|
83
|
+
if (tokenRenamePattern) registerTokenRenameParser(tokenRenamePattern);
|
|
84
|
+
registerTokenRenamePreprocessor();
|
|
85
|
+
const parserNames = [LEGACY_TOKENS_PARSER_NAME, ...(hasRegisteredTransforms() ? [TOKEN_RENAME_PARSER_NAME] : [])];
|
|
86
|
+
// Rewrites {references} to any renamed token's OLD path, tree-wide, once all
|
|
87
|
+
// files are merged (see registerTokenRenamePreprocessor() above).
|
|
88
|
+
const preprocessorNames = hasRegisteredTransforms() ? [TOKEN_RENAME_PREPROCESSOR_NAME] : [];
|
|
89
|
+
|
|
64
90
|
// ── 3. CSS format ─────────────────────────────────────────────────────────────
|
|
65
91
|
// customPropsCount is updated by the format at render time
|
|
66
92
|
// loadExistingCustomProps supports the mergeCustomProps option (see below)
|
|
@@ -121,6 +147,8 @@ if (sourceModes) {
|
|
|
121
147
|
mergeCustomProps,
|
|
122
148
|
useLightDarkFunc,
|
|
123
149
|
customPropsSelector,
|
|
150
|
+
parserNames,
|
|
151
|
+
preprocessorNames,
|
|
124
152
|
});
|
|
125
153
|
|
|
126
154
|
} else {
|
|
@@ -151,7 +179,8 @@ if (sourceModes) {
|
|
|
151
179
|
if (jsonBuildPath && !jsonDestFile) {
|
|
152
180
|
const sdInit = new StyleDictionary({
|
|
153
181
|
source: singleSource,
|
|
154
|
-
parsers:
|
|
182
|
+
parsers: parserNames,
|
|
183
|
+
preprocessors: preprocessorNames,
|
|
155
184
|
log: { verbosity: 'silent' },
|
|
156
185
|
platforms: {},
|
|
157
186
|
});
|
|
@@ -160,7 +189,8 @@ if (sourceModes) {
|
|
|
160
189
|
|
|
161
190
|
const sd = new StyleDictionary({
|
|
162
191
|
source: singleSource,
|
|
163
|
-
parsers:
|
|
192
|
+
parsers: parserNames,
|
|
193
|
+
preprocessors: preprocessorNames,
|
|
164
194
|
log: { verbosity: 'verbose' },
|
|
165
195
|
platforms: buildPlatforms({
|
|
166
196
|
buildPath,
|
|
@@ -25,10 +25,18 @@ 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 { splitSourceEntries, registerSourcePrefixes } from './build-tokens-src/source-prefixes.mjs';
|
|
28
|
+
import { registerSourceTransforms, transformedFilesPattern, hasRegisteredTransforms } from './build-tokens-src/source-transforms.mjs';
|
|
29
|
+
import {
|
|
30
|
+
TOKEN_RENAME_PARSER_NAME,
|
|
31
|
+
registerTokenRenameParser,
|
|
32
|
+
TOKEN_RENAME_PREPROCESSOR_NAME,
|
|
33
|
+
registerTokenRenamePreprocessor,
|
|
34
|
+
} from './build-tokens-src/token-rename-parser.mjs';
|
|
28
35
|
import { findMediaModeBlocks } from './build-tokens-src/merge-css.mjs';
|
|
29
36
|
import { LEGACY_TOKENS_PARSER_NAME, registerLegacyTokensParser } from './build-tokens-src/legacy-tokens-parser.mjs';
|
|
30
37
|
|
|
31
38
|
registerLegacyTokensParser();
|
|
39
|
+
registerTokenRenamePreprocessor();
|
|
32
40
|
|
|
33
41
|
// const __filename = fileURLToPath(import.meta.url);
|
|
34
42
|
// const __dirname = dirname(__filename);
|
|
@@ -204,21 +212,29 @@ async function run() {
|
|
|
204
212
|
// includeEntries: base-mode sources, passed as `include` for non-base
|
|
205
213
|
// modes so cross-mode references resolve (same as build-source-modes.mjs).
|
|
206
214
|
/** @param {Parameters<typeof splitSourceEntries>[0]} sourceEntries @param {Parameters<typeof splitSourceEntries>[0]} [includeEntries] @returns {Promise<Map<string,string>>} */
|
|
207
|
-
// Entries can be strings or `{ src, prefix }` objects (see
|
|
208
|
-
// build-tokens-src/source-prefixes.mjs):
|
|
209
|
-
//
|
|
215
|
+
// Entries can be strings or `{ src, prefix, transform }` objects (see
|
|
216
|
+
// build-tokens-src/source-prefixes.mjs): both are registered before the
|
|
217
|
+
// transform pipeline runs, so token names match the generated CSS.
|
|
210
218
|
const resolveSourceFileMap = async (sourceEntries, includeEntries = []) => {
|
|
211
219
|
/** @type {Map<string, string>} */
|
|
212
220
|
const map = new Map();
|
|
213
|
-
const { patterns: resolvedPaths, prefixed } = splitSourceEntries(sourceEntries, configDir);
|
|
214
|
-
const { patterns: includePaths, prefixed: includePrefixed } = splitSourceEntries(includeEntries, configDir);
|
|
221
|
+
const { patterns: resolvedPaths, prefixed, transformed } = splitSourceEntries(sourceEntries, configDir);
|
|
222
|
+
const { patterns: includePaths, prefixed: includePrefixed, transformed: includeTransformed } = splitSourceEntries(includeEntries, configDir);
|
|
215
223
|
if (!resolvedPaths.length) return map;
|
|
216
224
|
try {
|
|
217
225
|
await registerSourcePrefixes([...prefixed, ...includePrefixed]);
|
|
226
|
+
|
|
227
|
+
await registerSourceTransforms([...transformed, ...includeTransformed]);
|
|
228
|
+
const tokenRenamePattern = transformedFilesPattern();
|
|
229
|
+
if (tokenRenamePattern) registerTokenRenameParser(tokenRenamePattern);
|
|
230
|
+
const parserNames = [LEGACY_TOKENS_PARSER_NAME, ...(hasRegisteredTransforms() ? [TOKEN_RENAME_PARSER_NAME] : [])];
|
|
231
|
+
const preprocessorNames = hasRegisteredTransforms() ? [TOKEN_RENAME_PREPROCESSOR_NAME] : [];
|
|
232
|
+
|
|
218
233
|
const sd = new StyleDictionary({
|
|
219
234
|
include: includePaths,
|
|
220
235
|
source: resolvedPaths,
|
|
221
|
-
parsers:
|
|
236
|
+
parsers: parserNames,
|
|
237
|
+
preprocessors: preprocessorNames,
|
|
222
238
|
log: { verbosity: 'silent' },
|
|
223
239
|
platforms: { css: { transforms: CSS_TRANSFORMS } },
|
|
224
240
|
});
|
|
@@ -54,14 +54,56 @@ const config = {
|
|
|
54
54
|
// OK: `${minimo_path}/**/*.{json,mjs}`
|
|
55
55
|
// NO: path.join(minimo_path, '/**/*.{json,mjs}')
|
|
56
56
|
//
|
|
57
|
-
// An entry can also be an object `{ src, prefix }` (src: string
|
|
58
|
-
// strings/globs)
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
// {
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
57
|
+
// An entry can also be an object `{ src, prefix, transform }` (src: string
|
|
58
|
+
// or array of strings/globs).
|
|
59
|
+
//
|
|
60
|
+
// `prefix`: renders the custom properties of those files with a prefix,
|
|
61
|
+
// e.g. { src: '...open-props...json', prefix: 'op' } turns Open Props'
|
|
62
|
+
// gray.0 into --op-gray-0. Only the property name changes: {references} to
|
|
63
|
+
// those tokens are rendered as var(--op-gray-0), and the JSON output stays
|
|
64
|
+
// unprefixed. 'op', 'op-' and '--op-' are equivalent. Two different
|
|
65
|
+
// prefixes for the same file make the build fail.
|
|
66
|
+
//
|
|
67
|
+
// `transform`: moves/renames token nodes within those files' own tree —
|
|
68
|
+
// e.g. { primary: 'primary.100' } takes whatever is defined at dot-path
|
|
69
|
+
// `primary` and moves it to `primary.100`, BEFORE Style Dictionary resolves
|
|
70
|
+
// {references} or builds the CSS/JSON output (unlike `prefix`, which is a
|
|
71
|
+
// purely cosmetic CSS name change applied after the fact — see
|
|
72
|
+
// build-tokens-src/source-transforms.mjs). Typical use case: a preexisting
|
|
73
|
+
// project (e.g. driven by Figma/Penpot) names its own base color `primary`,
|
|
74
|
+
// while minimo's own tokens reference `{primary.100}` — instead of
|
|
75
|
+
// renaming the property by hand after every build, or hunting down every
|
|
76
|
+
// minimo reference to change it, `transform` fixes the token's own path
|
|
77
|
+
// once, here.
|
|
78
|
+
// - Both sides of the map use Style Dictionary's own DOT-PATH notation
|
|
79
|
+
// ('primary.100'), NOT the hyphen-joined custom-property notation
|
|
80
|
+
// ('primary-100'): token name segments in this project can themselves
|
|
81
|
+
// contain hyphens (e.g. btn-close), so a hyphen can't reliably tell a
|
|
82
|
+
// path separator from a literal part of a segment name — a dot can,
|
|
83
|
+
// since it's already reserved by Style Dictionary as the {reference}
|
|
84
|
+
// path separator.
|
|
85
|
+
// - {References} to the OLD path — in the same file or a completely
|
|
86
|
+
// different one — are rewritten automatically, tree-wide, to the NEW
|
|
87
|
+
// path once all sources are merged: e.g. every {primary} in the whole
|
|
88
|
+
// build becomes {primary.100}, whether minimo's own tokens already
|
|
89
|
+
// wrote {primary.100} directly (no-op) or something else in the
|
|
90
|
+
// project still says {primary}.
|
|
91
|
+
// - WARNING: this rewrite is by design file-independent (a bare
|
|
92
|
+
// {reference} doesn't know which file it "belongs" to) — so if the
|
|
93
|
+
// SAME source key (e.g. "primary") is given a DIFFERENT destination in
|
|
94
|
+
// two different files' `transform` maps, each rename itself still
|
|
95
|
+
// works fine, but {primary} becomes ambiguous: which destination should
|
|
96
|
+
// it be rewritten to? In that case the automatic rewrite is skipped
|
|
97
|
+
// for that key (a warning is logged), and every {primary} reference
|
|
98
|
+
// must be fixed by hand to point at the right one of the two new
|
|
99
|
+
// paths.
|
|
100
|
+
// - A source key not found in the file only logs a warning, it does not
|
|
101
|
+
// fail the build.
|
|
102
|
+
// - Applies to both the CSS and the JSON output (see JSON TOKENS OUTPUT
|
|
103
|
+
// below), since both are rebuilt from the token's path, by then already
|
|
104
|
+
// renamed.
|
|
105
|
+
//
|
|
106
|
+
// Both options work the same way inside sourceModes.
|
|
65
107
|
source: [
|
|
66
108
|
`${minimo_path}/design-tokens/_src/**/*.tokens.{mjs,jsonc}`, // main tokens
|
|
67
109
|
`${minimo_path}/src/**/*.tokens.{mjs,jsonc}`, // components tokens
|
|
@@ -69,8 +111,13 @@ const config = {
|
|
|
69
111
|
// optional extra token source (in this example, openProps, https://open-props.style/)
|
|
70
112
|
// { src: `${node_modules_path}/open-props/open-props.style-dictionary-tokens.json`, prefix: 'op' },
|
|
71
113
|
|
|
72
|
-
// your project tokens
|
|
73
|
-
|
|
114
|
+
// your project tokens — e.g. remaps a Figma/Penpot-exported `primary`
|
|
115
|
+
// color to minimo's own `primary.100` naming
|
|
116
|
+
{
|
|
117
|
+
src: './project-tokens/*.{js,mjs,jsonc,json}',
|
|
118
|
+
prefix: 'my-project',
|
|
119
|
+
transform: { primary: 'primary.100' },
|
|
120
|
+
}
|
|
74
121
|
],
|
|
75
122
|
|
|
76
123
|
// Optional light/dark (or other) custom properties split — alternative to
|
|
@@ -59,6 +59,15 @@ export function cssRules({
|
|
|
59
59
|
sideEffects: true,
|
|
60
60
|
oneOf: [
|
|
61
61
|
|
|
62
|
+
{
|
|
63
|
+
// `import url from './file.css?as_url'`: the file is emitted as is and its URL is returned
|
|
64
|
+
resourceQuery: /as_url/,
|
|
65
|
+
type: 'asset/resource',
|
|
66
|
+
generator: {
|
|
67
|
+
filename: 'css/[name].[contenthash][ext]'
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
|
|
62
71
|
// raw content per web-components
|
|
63
72
|
{
|
|
64
73
|
resourceQuery: /raw/,
|
|
@@ -8,7 +8,7 @@ Le versioni precedenti sono conservate in `archived/webpack-config-v2/` del repo
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
## Fix da verificare
|
|
11
|
+
## Fix e upg da verificare e applicare a tutte le versioni
|
|
12
12
|
* WebpackManifestPlugin: impostare `removeKeyHash: /\?.*$/, // /([a-f0-9]{32}\.?)/gi, // /(\?as_asset)$/,`, sostituendo eventuali impostazione `removeKeyHash: true` se presente
|
|
13
13
|
|
|
14
14
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@massimo-cassandro/minimo",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.39.0",
|
|
4
4
|
"description": "Light css framework & utilities",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
@@ -103,6 +103,7 @@
|
|
|
103
103
|
"@stylistic/stylelint-config": "^5.0.0",
|
|
104
104
|
"@stylistic/stylelint-plugin": "^5.3.0",
|
|
105
105
|
"clipboardy": "^5.3.2",
|
|
106
|
+
"json5": "^2.2.3",
|
|
106
107
|
"sharp": "^0.35.3",
|
|
107
108
|
"sharp-ico": "^0.1.5",
|
|
108
109
|
"stylelint": "^17.15.0",
|