@massimo-cassandro/minimo 1.38.5 → 1.39.1

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.
@@ -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: [LEGACY_TOKENS_PARSER_NAME],
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: [LEGACY_TOKENS_PARSER_NAME],
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 }` objects,
154
- // to render the custom properties of those files with a prefix (see
155
- // source-prefixes.mjs). They are flattened here into plain patterns for
156
- // Style Dictionary; the prefixed ones are also collected in
157
- // `prefixedSources`, to be registered by build-tokens.mjs.
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 patterns to pass
53
- * to Style Dictionary and the list of the prefixed entries. Patterns are
54
- * resolved relative to baseDir (see resolveSourcePaths).
55
- * @param {(string|{src: string|string[], prefix?: string})[]} entries
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: [LEGACY_TOKENS_PARSER_NAME],
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: [LEGACY_TOKENS_PARSER_NAME],
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): the prefixes are registered
209
- // before the transform pipeline runs, so token names match the CSS.
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: [LEGACY_TOKENS_PARSER_NAME],
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 or array of
58
- // strings/globs), to render the custom properties of those files with a
59
- // prefix: e.g. { src: '...open-props...json', prefix: 'op' } turns Open
60
- // Props' gray.0 into --op-gray-0. Only the property name changes:
61
- // {references} to those tokens are rendered as var(--op-gray-0), and the JSON
62
- // output stays unprefixed. 'op', 'op-' and '--op-' are equivalent. The same
63
- // syntax works inside sourceModes. Two different prefixes for the same file
64
- // make the build fail.
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
- {src: './project-tokens/*.{js,mjs,jsonc,json}', prefix: 'my-project'}
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 sempre
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.38.5",
3
+ "version": "1.39.1",
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",
package/src/css/grid.css CHANGED
@@ -28,13 +28,16 @@
28
28
  @media (--media-large-viewport-up) {
29
29
  --_cols: 4;
30
30
 
31
- /* versione a 3, 5, 6, 8 colonne */
31
+ /* versione a 3, 4, 5, 6, 8 colonne */
32
32
  &.grid-2 {
33
33
  --_cols: 2;
34
34
  }
35
35
  &.grid-3 {
36
36
  --_cols: 3;
37
37
  }
38
+ &.grid-4 {
39
+ --_cols: 4;
40
+ }
38
41
  &.grid-5 {
39
42
  --_cols: 5;
40
43
  }
@@ -78,6 +78,7 @@ export type MinimoClass =
78
78
  | 'grid'
79
79
  | 'grid-2'
80
80
  | 'grid-3'
81
+ | 'grid-4'
81
82
  | 'grid-5'
82
83
  | 'grid-6'
83
84
  | 'grid-8'