@massimo-cassandro/minimo 1.38.3 → 1.38.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -69,7 +69,7 @@ The `source` array accepts any combination of:
69
69
 
70
70
  **Legacy token syntax (auto-detected, `.json` only):** `.json` source files written in the older, non-DTCG Style Dictionary syntax (`value`/`type` instead of `$value`/`$type`, references as `{group.token.value}` instead of `{group.token}` — e.g. a file exported from [Open Props](https://open-props.style/)) are converted to DTCG v5 syntax automatically at parse time, node by node. Files already using `$value`/`$type` are left untouched, so no config flag or file list is needed — mixing legacy and DTCG sources in the same `source` array just works. `.jsonc` is not covered (reserved for hand-authored DTCG sources, parsed by Style Dictionary's own loader) and these `.json` files are parsed with plain `JSON.parse` (no comments, no trailing commas). See `build-tokens-src/legacy-tokens-parser.mjs`.
71
71
 
72
- Known limitation: a legacy node that is at the same time a token (own `value`/`type`) *and* a group with further nested children (e.g. Open Props' `other.ease.out`, which has its own value plus `out.1` ... `out.5`) can't be represented in DTCG v5 — a node with `$value` is always a leaf, Style Dictionary doesn't descend into its children. Only the node's own value is converted; the nested children are silently lost.
72
+ A legacy node that is at the same time a token (own `value`/`type`) *and* a group with nested children (e.g. Open Props' `other.ease.out`, which has its own value plus `out.1` ... `out.5`) can't be represented in DTCG v5, where a node with `$value` is always a leaf. Its own value is moved to a child token named `default` (`other.ease.out.default`, i.e. `--other-ease-out-default`), or `base` if a `default` child already exists; if both exist the build fails with an explicit error. References to such a node from the same file are rewritten accordingly; from other files they must use the child name explicitly.
73
73
 
74
74
  #### Source prefixes
75
75
 
@@ -25,6 +25,12 @@
25
25
  // the parser is a safe no-op for files that don't need conversion — no
26
26
  // config flag or file list is required, detection is fully automatic.
27
27
  //
28
+ // "Hybrid" nodes (a token with its own value that is also a group with
29
+ // nested children, e.g. Open Props' other.ease.out) can't be represented in
30
+ // DTCG v5, where a node with $value is a leaf. The own value is moved to a
31
+ // child token named `default` (or `base` if `default` is already a child
32
+ // name; the build fails if both are taken) — see collectHybridNodes().
33
+ //
28
34
  // Limitation: `.json` files handled by this parser are parsed with plain
29
35
  // JSON.parse (no comments, no trailing commas), unlike Style Dictionary's
30
36
  // own built-in loader (which uses JSON5 for .json/.jsonc/.json5). This keeps
@@ -39,58 +45,127 @@ export const LEGACY_TOKENS_PARSER_NAME = 'minimo/legacy-tokens';
39
45
  // Matches "{some.token.path.value}" -> captures "some.token.path"
40
46
  const LEGACY_REF = /\{([^}]+)\.value\}/g;
41
47
 
42
- /** @param {unknown} value @returns {unknown} */
43
- const convertLegacyRefs = (value) => {
48
+ // Name of the child token that receives the own value of a "hybrid" node
49
+ // (see collectHybridNodes()); FALLBACK_LEAF is used when a child with the
50
+ // preferred name already exists.
51
+ const PREFERRED_LEAF = 'default';
52
+ const FALLBACK_LEAF = 'base';
53
+
54
+ /** @param {unknown} node @returns {node is Record<string, unknown>} */
55
+ const isPlainObject = (node) => node !== null && typeof node === 'object' && !Array.isArray(node);
56
+
57
+ // True if the node is (or contains, at any depth) a token, legacy or DTCG.
58
+ /** @param {unknown} node @returns {boolean} */
59
+ const hasTokenDescendant = (node) => isPlainObject(node)
60
+ && (Object.hasOwn(node, 'value') || Object.hasOwn(node, '$value')
61
+ || Object.values(node).some(hasTokenDescendant));
62
+
63
+ /*
64
+ * A "hybrid" legacy node is a token (own `value`) that is also a group, i.e.
65
+ * has children that are tokens or groups of tokens (e.g. Open Props'
66
+ * other.ease.out, with its own value plus out.1 ... out.5). The own value is
67
+ * moved to a child token: `default`, or `base` if `default` is already a
68
+ * child name. If both are taken the build fails. Returns a map
69
+ * dot.path -> chosen child name, used to convert the node itself and to
70
+ * rewrite references pointing to it (references are only rewritten within
71
+ * the same file: a reference from another file to a hybrid node must use the
72
+ * child name explicitly).
73
+ */
74
+ /**
75
+ * @param {unknown} node
76
+ * @param {string[]} path
77
+ * @param {string} filePath
78
+ * @param {Map<string,string>} [hybrids]
79
+ * @returns {Map<string,string>}
80
+ */
81
+ const collectHybridNodes = (node, path, filePath, hybrids = new Map()) => {
82
+ if (!isPlainObject(node) || Object.hasOwn(node, '$value')) return hybrids;
83
+
84
+ if (Object.hasOwn(node, 'value')
85
+ && Object.entries(node).some(([key, child]) => key !== 'value' && hasTokenDescendant(child))) {
86
+ const leaf = [PREFERRED_LEAF, FALLBACK_LEAF].find((name) => !Object.hasOwn(node, name));
87
+ if (!leaf) {
88
+ throw new Error(
89
+ `[build-tokens] legacy tokens: node "${path.join('.')}" in ${filePath} has its own value and children, `
90
+ + `but both "${PREFERRED_LEAF}" and "${FALLBACK_LEAF}" child names are already taken`
91
+ );
92
+ }
93
+ hybrids.set(path.join('.'), leaf);
94
+ }
95
+
96
+ for (const [key, child] of Object.entries(node)) {
97
+ collectHybridNodes(child, [...path, key], filePath, hybrids);
98
+ }
99
+ return hybrids;
100
+ };
101
+
102
+ /**
103
+ * @param {unknown} value
104
+ * @param {Map<string,string>} hybrids
105
+ * @returns {unknown}
106
+ */
107
+ const convertLegacyRefs = (value, hybrids) => {
44
108
  if (typeof value === 'string') {
45
- return value.replace(LEGACY_REF, '{$1}');
109
+ return value.replace(LEGACY_REF, (_, ref) => (
110
+ hybrids.has(ref) ? `{${ref}.${hybrids.get(ref)}}` : `{${ref}}`
111
+ ));
46
112
  }
47
113
  if (Array.isArray(value)) {
48
- return value.map(convertLegacyRefs);
114
+ return value.map((v) => convertLegacyRefs(v, hybrids));
49
115
  }
50
116
  if (value !== null && typeof value === 'object') {
51
117
  return Object.fromEntries(
52
- Object.entries(value).map(([key, v]) => [key, convertLegacyRefs(v)])
118
+ Object.entries(value).map(([key, v]) => [key, convertLegacyRefs(v, hybrids)])
53
119
  );
54
120
  }
55
121
  return value;
56
122
  };
57
123
 
58
- /** @param {unknown} node @returns {node is Record<string, unknown>} */
59
- const isPlainObject = (node) => node !== null && typeof node === 'object' && !Array.isArray(node);
60
-
61
- /** @param {unknown} node @returns {unknown} */
62
- const convertLegacyNode = (node) => {
124
+ /**
125
+ * @param {unknown} node
126
+ * @param {string[]} path
127
+ * @param {Map<string,string>} hybrids
128
+ * @returns {unknown}
129
+ */
130
+ const convertLegacyNode = (node, path, hybrids) => {
63
131
  if (!isPlainObject(node)) return node;
64
132
 
65
133
  // Already DTCG v5 syntax: leave untouched.
66
134
  if (Object.hasOwn(node, '$value')) return node;
67
135
 
68
136
  if (Object.hasOwn(node, 'value')) {
69
- // Legacy token leaf. Some legacy files (e.g. Open Props) also attach
70
- // sibling numbered/named children to a node that carries its own
71
- // `value`/`type` (a group that is also a default token) — those
72
- // children are converted too, even though Style Dictionary itself
73
- // cannot represent "group + own $value" ambiguity natively.
74
137
  const { value, type, comment, description, ...rest } = node;
75
- const convertedRest = Object.fromEntries(
76
- Object.entries(rest).map(([key, child]) => [key, convertLegacyNode(child)])
77
- );
78
138
  const desc = comment ?? description;
79
-
80
- return {
81
- ...convertedRest,
82
- $value: convertLegacyRefs(value),
139
+ const token = {
140
+ $value: convertLegacyRefs(value, hybrids),
83
141
  ...(type !== undefined ? { $type: type } : {}),
84
142
  ...(desc !== undefined ? { $description: desc } : {}),
85
143
  };
144
+
145
+ const hybridLeaf = hybrids.get(path.join('.'));
146
+ if (hybridLeaf === undefined) return token;
147
+
148
+ // Hybrid node: becomes a group, its own value moves to a child token.
149
+ return {
150
+ ...Object.fromEntries(
151
+ Object.entries(rest).map(([key, child]) => [key, convertLegacyNode(child, [...path, key], hybrids)])
152
+ ),
153
+ [hybridLeaf]: token,
154
+ };
86
155
  }
87
156
 
88
157
  // Group node: recurse into children.
89
158
  return Object.fromEntries(
90
- Object.entries(node).map(([key, child]) => [key, convertLegacyNode(child)])
159
+ Object.entries(node).map(([key, child]) => [key, convertLegacyNode(child, [...path, key], hybrids)])
91
160
  );
92
161
  };
93
162
 
163
+ /** @param {string} contents @param {string} filePath @returns {unknown} */
164
+ const parseLegacyFile = (contents, filePath) => {
165
+ const tree = JSON.parse(contents);
166
+ return convertLegacyNode(tree, [], collectHybridNodes(tree, [], filePath));
167
+ };
168
+
94
169
  // Registers the parser globally on the StyleDictionary class. To actually
95
170
  // run it, a Style Dictionary instance must also opt in via
96
171
  // `parsers: [LEGACY_TOKENS_PARSER_NAME]` in its config (registering alone
@@ -100,6 +175,6 @@ export const registerLegacyTokensParser = () => {
100
175
  StyleDictionary.registerParser({
101
176
  name: LEGACY_TOKENS_PARSER_NAME,
102
177
  pattern: /\.json$/,
103
- parser: ({ contents }) => convertLegacyNode(JSON.parse(contents)),
178
+ parser: ({ contents, filePath }) => parseLegacyFile(contents, filePath ?? ''),
104
179
  });
105
180
  };
@@ -4,7 +4,9 @@
4
4
  // and a `dark` mode, custom properties present in BOTH — matched by name —
5
5
  // are written once, as `--name: light-dark(<light-value>, <dark-value>);`,
6
6
  // instead of being split across the base mode's top-level `:root { ... }`
7
- // block and a `@media (prefers-color-scheme: dark) { ... }` block.
7
+ // block and a `@media (prefers-color-scheme: dark) { ... }` block. If the two
8
+ // values are identical, light-dark() would be redundant: the bare value is
9
+ // written instead, e.g. `--name: <value>;`.
8
10
  //
9
11
  // Only `light` and `dark` are affected:
10
12
  // - a third mode (e.g. a custom `high-contrast` key in sourceModes) is left
@@ -74,7 +76,12 @@ export const applyLightDarkFunc = ({ modeFinalProps, modeNames, baseMode, select
74
76
  // Prefers the light-side trailing comment (e.g. a token $description);
75
77
  // falls back to the dark-side one if light has none.
76
78
  const comment = light.comment || dark.comment;
77
- sharedProps[name] = `light-dark(${light.value}, ${dark.value});${comment ? ` ${comment}` : ''}`;
79
+ // Equal light/dark values: light-dark(x, x) would be redundant — use the
80
+ // bare value directly instead.
81
+ const value = light.value === dark.value
82
+ ? light.value
83
+ : `light-dark(${light.value}, ${dark.value})`;
84
+ sharedProps[name] = `${value};${comment ? ` ${comment}` : ''}`;
78
85
  }
79
86
 
80
87
  const stripCommon = (props) => {
@@ -83,14 +83,17 @@ import { buildSourceModes } from './build-tokens-src/build-source-modes.mjs';
83
83
  // untouched. Files are written flat in jsonBuildPath (no subdirectories), so
84
84
  // only its top level is scanned. Runs regardless of sourceModes.
85
85
  if (jsonBuildPath) {
86
+ // Local const: narrowing of the imported `jsonBuildPath` binding is lost
87
+ // inside the .map() callback below.
88
+ const jsonDir = jsonBuildPath;
86
89
  const { readdir, rm, mkdir } = await import('fs/promises');
87
- const existingEntries = await readdir(jsonBuildPath, { withFileTypes: true }).catch(() => []);
90
+ const existingEntries = await readdir(jsonDir, { withFileTypes: true }).catch(() => []);
88
91
  await Promise.all(
89
92
  existingEntries
90
93
  .filter((entry) => entry.isFile() && /\.jsonc?$/.test(entry.name))
91
- .map((entry) => rm(path.join(jsonBuildPath, entry.name)))
94
+ .map((entry) => rm(path.join(jsonDir, entry.name)))
92
95
  );
93
- await mkdir(jsonBuildPath, { recursive: true });
96
+ await mkdir(jsonDir, { recursive: true });
94
97
  }
95
98
 
96
99
  // ── 7. Build ──────────────────────────────────────────────────────────────────
@@ -58,7 +58,6 @@ async function run() {
58
58
 
59
59
  // Path of the generated CSS file containing all custom property definitions
60
60
  const custom_prop_file_path = path.resolve(configDir, path.join(config.buildPath, config.destFile));
61
- const custom_prop_filename = path.basename(custom_prop_file_path);
62
61
 
63
62
  // Directory to scan for CSS files
64
63
  const checkdir = path.resolve(configDir, config.dirToCheck);
@@ -69,7 +68,7 @@ async function run() {
69
68
 
70
69
  // Collect all .css files in the scan directory, excluding any file under a
71
70
  // directory whose name starts with "TODO" (work-in-progress folders).
72
- // The token file itself (custom_prop_filename) is intentionally included:
71
+ // The token file itself (custom_prop_file_path) is intentionally included:
73
72
  // custom properties are often composed from other custom properties
74
73
  // (e.g. `--malert-box-shadow: ... var(--malert-box-shadow-color) ...`)
75
74
  // and those internal var() references must count as usage, otherwise the
@@ -40,12 +40,14 @@ const config = {
40
40
  // .jsonc, reserved for hand-authored DTCG sources. These .json files are
41
41
  // parsed with plain JSON.parse (no comments, no trailing commas).
42
42
  //
43
- // LIMITATION: a legacy node that is at the same time a token (own
44
- // value/type) AND a group with further nested children (e.g. Open Props'
45
- // other.ease.out, which has its own value plus out.1 ... out.5) cannot be
46
- // represented in DTCG v5 — a node with $value is always a leaf, Style
47
- // Dictionary does not descend into its children. In that case only the
48
- // node's own value is converted; the nested children are silently lost.
43
+ // A legacy node that is at the same time a token (own value/type) AND a
44
+ // group with nested children (e.g. Open Props' other.ease.out, which has
45
+ // its own value plus out.1 ... out.5) cannot be represented in DTCG v5 —
46
+ // a node with $value is always a leaf. Its own value is moved to a child
47
+ // token named `default` (other.ease.out.default, i.e. --other-ease-out-default),
48
+ // or `base` if a `default` child already exists; if both exist the build
49
+ // fails. References to such a node from the same file are rewritten
50
+ // accordingly; from other files they must use the child name explicitly.
49
51
  //
50
52
  // WARNING: do not use path.join() to build glob patterns — it may corrupt
51
53
  // the pattern syntax. Use template literals instead:
@@ -138,7 +138,7 @@ const { CustomPropsPurgeCssPlugin } = runCustomPropsPlugin
138
138
  // NB: PurgeCSS elabora ogni asset css singolarmente, quindi i @keyframes
139
139
  // definiti nello `shared.css` ma usati solo in un altro asset (es. un css di
140
140
  // progetto che usa una animation di minimo) verrebbero rimossi: in tal caso
141
- // aggiungerli a `safelist.keyframes` o impostare `keyframes: false` in
141
+ // aggiungerli a 'safelist.keyframes' o impostare `keyframes: false` in
142
142
  // `purgeCSSOptions`
143
143
  const shared_chunk_paths = (module) => {
144
144
 
@@ -146,7 +146,6 @@ const shared_chunk_paths = (module) => {
146
146
  const sep = '[\\\\/]'; // stringa che produce [\\/] nel pattern
147
147
  const pathsRegexp = new RegExp([
148
148
  'node_modules',
149
- 'app/src',
150
149
  'app/src/js',
151
150
  'app/src/web-components',
152
151
  'app/src/components',
@@ -411,8 +410,7 @@ const config = {
411
410
  // fileName: path.join(output_dir, 'manifest.json'),
412
411
  // sf: fileName: 'manifest.json', // scrive in output.path
413
412
  // // basePath: item.source_dirname
414
- // // removeKeyHash: /(^(_assets\/(?!(fonts\/))))|((\?as_asset)$)/,
415
- // removeKeyHash: true, // /([a-f0-9]{32}\.?)/gi, // /(\?as_asset)$/,
413
+ // // removeKeyHash: /\?.*$/, // /([a-f0-9]{32}\.?)/gi, // /(\?as_asset)$/,
416
414
  // // rimuove i font dal manifest. Non necessari, rendono il file inutilmente grande
417
415
  // filter: isDevelopment? undefined : (FileDescriptor) => {
418
416
  // return /fonts/.test(FileDescriptor.path)? false : true;
@@ -8,6 +8,10 @@ Le versioni precedenti sono conservate in `archived/webpack-config-v2/` del repo
8
8
 
9
9
  ---
10
10
 
11
+ ## Fix da verificare sempre
12
+ * WebpackManifestPlugin: impostare `removeKeyHash: /\?.*$/, // /([a-f0-9]{32}\.?)/gi, // /(\?as_asset)$/,`, sostituendo eventuali impostazione `removeKeyHash: true` se presente
13
+
14
+
11
15
  ## Da v2 a v3
12
16
 
13
17
  Novità della v3: le definizioni delle custom properties non sono più gestite da PurgeCSS (`variables`) né da postcss-jit-props, ma da un plugin webpack dedicato, `custom-props-purgecss-plugin.mjs`, che opera sugli asset CSS finali (dopo la minificazione) e inietta solo le custom properties effettivamente usate (dipendenze transitive incluse), partendo dal file master `app/css/custom-properties.css`. Il plugin è attivabile con un flag, indipendente da `usePurgeCss`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massimo-cassandro/minimo",
3
- "version": "1.38.3",
3
+ "version": "1.38.5",
4
4
  "description": "Light css framework & utilities",
5
5
  "type": "module",
6
6
  "main": "./index.js",