@massimo-cassandro/minimo 1.34.0 → 1.35.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.
@@ -66,6 +66,10 @@ The `source` array accepts any combination of:
66
66
  - `.json` / `.jsonc` — parsed directly as token data
67
67
  - `.mjs` / `.js` — imported as ES modules; must export a W3C DTCG-compliant object as `default`
68
68
 
69
+ **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`.
70
+
71
+ 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
+
69
73
 
70
74
  #### Token file format (JS example)
71
75
 
@@ -29,6 +29,7 @@ import { CSS_TRANSFORMS, JSON_TRANSFORMS } from './platforms.mjs';
29
29
  import { customPropsCount } from './formats/css.mjs';
30
30
  import { buildJsonFiles, collectConcreteFilePaths } from './formats/json.mjs';
31
31
  import { loadExistingCustomPropsScoped } from './merge-css.mjs';
32
+ import { LEGACY_TOKENS_PARSER_NAME } from './legacy-tokens-parser.mjs';
32
33
 
33
34
  /**
34
35
  * @param {object} opts
@@ -81,6 +82,7 @@ export const buildSourceModes = async ({
81
82
  for (const mode of modeNames) {
82
83
  const sd = new StyleDictionary({
83
84
  source: sourceModes[mode],
85
+ parsers: [LEGACY_TOKENS_PARSER_NAME],
84
86
  log: { verbosity: 'verbose' },
85
87
  platforms: {
86
88
  css: {
@@ -115,6 +117,7 @@ export const buildSourceModes = async ({
115
117
 
116
118
  const jsonSd = new StyleDictionary({
117
119
  source: sourceModes[mode],
120
+ parsers: [LEGACY_TOKENS_PARSER_NAME],
118
121
  log: { verbosity: 'silent' },
119
122
  platforms: {
120
123
  json: {
@@ -0,0 +1,105 @@
1
+ // build-tokens-src/legacy-tokens-parser.mjs
2
+ // Registers a Style Dictionary parser that bridges "legacy" token files
3
+ // (Style Dictionary v3/v4-style syntax: no `$` prefix on `value`/`type`,
4
+ // references written as `{group.token.value}`) into the W3C DTCG syntax
5
+ // (`$value`/`$type`, references as `{group.token}`) required by Style
6
+ // Dictionary v5.
7
+ //
8
+ // Why this is needed: Style Dictionary v5 decides whether a build "usesDtcg"
9
+ // ONCE per build, from whichever source file is processed first (see
10
+ // detectDtcgSyntax() called inside combineJSON.js in style-dictionary) — it
11
+ // is a single global flag, not a per-file check. Mixing a legacy-syntax file
12
+ // (e.g. a file exported from Open Props, or from an old Style Dictionary
13
+ // v3/v4 project) together with DTCG sources therefore causes the legacy
14
+ // file's tokens to be silently dropped regardless of file order: they never
15
+ // carry the `$value` key combineJSON's own tagging (and the rest of the
16
+ // pipeline) expects.
17
+ //
18
+ // This parser intercepts every `.json` source file (not `.jsonc` — that
19
+ // extension is reserved in this project for hand-authored DTCG sources,
20
+ // already handled correctly by Style Dictionary's own built-in loader) and
21
+ // converts, node by node:
22
+ // value -> $value, type -> $type, comment/description -> $description
23
+ // "{group.token.value}" -> "{group.token}" inside reference strings
24
+ // Nodes that already use `$value` (DTCG v5 syntax) are left untouched, so
25
+ // the parser is a safe no-op for files that don't need conversion — no
26
+ // config flag or file list is required, detection is fully automatic.
27
+ //
28
+ // Limitation: `.json` files handled by this parser are parsed with plain
29
+ // JSON.parse (no comments, no trailing commas), unlike Style Dictionary's
30
+ // own built-in loader (which uses JSON5 for .json/.jsonc/.json5). This keeps
31
+ // the builder dependency-free — legacy token exports (Open Props, old Style
32
+ // Dictionary v3/v4 projects) are virtually always strict JSON anyway. Use
33
+ // `.jsonc` or `.mjs` for hand-authored DTCG sources that need comments.
34
+
35
+ import StyleDictionary from 'style-dictionary';
36
+
37
+ export const LEGACY_TOKENS_PARSER_NAME = 'minimo/legacy-tokens';
38
+
39
+ // Matches "{some.token.path.value}" -> captures "some.token.path"
40
+ const LEGACY_REF = /\{([^}]+)\.value\}/g;
41
+
42
+ /** @param {unknown} value @returns {unknown} */
43
+ const convertLegacyRefs = (value) => {
44
+ if (typeof value === 'string') {
45
+ return value.replace(LEGACY_REF, '{$1}');
46
+ }
47
+ if (Array.isArray(value)) {
48
+ return value.map(convertLegacyRefs);
49
+ }
50
+ if (value !== null && typeof value === 'object') {
51
+ return Object.fromEntries(
52
+ Object.entries(value).map(([key, v]) => [key, convertLegacyRefs(v)])
53
+ );
54
+ }
55
+ return value;
56
+ };
57
+
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) => {
63
+ if (!isPlainObject(node)) return node;
64
+
65
+ // Already DTCG v5 syntax: leave untouched.
66
+ if (Object.hasOwn(node, '$value')) return node;
67
+
68
+ 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
+ const { value, type, comment, description, ...rest } = node;
75
+ const convertedRest = Object.fromEntries(
76
+ Object.entries(rest).map(([key, child]) => [key, convertLegacyNode(child)])
77
+ );
78
+ const desc = comment ?? description;
79
+
80
+ return {
81
+ ...convertedRest,
82
+ $value: convertLegacyRefs(value),
83
+ ...(type !== undefined ? { $type: type } : {}),
84
+ ...(desc !== undefined ? { $description: desc } : {}),
85
+ };
86
+ }
87
+
88
+ // Group node: recurse into children.
89
+ return Object.fromEntries(
90
+ Object.entries(node).map(([key, child]) => [key, convertLegacyNode(child)])
91
+ );
92
+ };
93
+
94
+ // Registers the parser globally on the StyleDictionary class. To actually
95
+ // run it, a Style Dictionary instance must also opt in via
96
+ // `parsers: [LEGACY_TOKENS_PARSER_NAME]` in its config (registering alone
97
+ // does not activate it for every instance — see Style Dictionary's parser
98
+ // hooks docs).
99
+ export const registerLegacyTokensParser = () => {
100
+ StyleDictionary.registerParser({
101
+ name: LEGACY_TOKENS_PARSER_NAME,
102
+ pattern: /\.json$/,
103
+ parser: ({ contents }) => convertLegacyNode(JSON.parse(contents)),
104
+ });
105
+ };
@@ -44,6 +44,12 @@ import {
44
44
  // ── 2. Custom transforms ─────────────────────────────────────────────────────
45
45
  import './build-tokens-src/transforms.mjs';
46
46
 
47
+ // ── 2b. Legacy token syntax bridge ───────────────────────────────────────────
48
+ // Converts legacy (non-DTCG, e.g. Open Props) .json/.jsonc source files to
49
+ // DTCG v5 syntax at parse time — see build-tokens-src/legacy-tokens-parser.mjs.
50
+ import { LEGACY_TOKENS_PARSER_NAME, registerLegacyTokensParser } from './build-tokens-src/legacy-tokens-parser.mjs';
51
+ registerLegacyTokensParser();
52
+
47
53
  // ── 3. CSS format ─────────────────────────────────────────────────────────────
48
54
  // customPropsCount is updated by the format at render time
49
55
  // loadExistingCustomProps supports the mergeCustomProps option (see below)
@@ -127,12 +133,18 @@ if (sourceModes) {
127
133
  const singleSource = /** @type {string[]} */ (source);
128
134
 
129
135
  if (jsonBuildPath && !jsonDestFile) {
130
- const sdInit = new StyleDictionary({ source: singleSource, log: { verbosity: 'silent' }, platforms: {} });
136
+ const sdInit = new StyleDictionary({
137
+ source: singleSource,
138
+ parsers: [LEGACY_TOKENS_PARSER_NAME],
139
+ log: { verbosity: 'silent' },
140
+ platforms: {},
141
+ });
131
142
  concreteFilePaths = await collectConcreteFilePaths(sdInit);
132
143
  }
133
144
 
134
145
  const sd = new StyleDictionary({
135
146
  source: singleSource,
147
+ parsers: [LEGACY_TOKENS_PARSER_NAME],
136
148
  log: { verbosity: 'verbose' },
137
149
  platforms: buildPlatforms({
138
150
  buildPath,
@@ -26,6 +26,9 @@ import './build-tokens-src/transforms.mjs';
26
26
  import { CSS_TRANSFORMS } from './build-tokens-src/platforms.mjs';
27
27
  import { resolveSourcePaths } from './build-tokens-src/resolve-source-paths.mjs';
28
28
  import { findMediaModeBlocks } from './build-tokens-src/merge-css.mjs';
29
+ import { LEGACY_TOKENS_PARSER_NAME, registerLegacyTokensParser } from './build-tokens-src/legacy-tokens-parser.mjs';
30
+
31
+ registerLegacyTokensParser();
29
32
 
30
33
  // const __filename = fileURLToPath(import.meta.url);
31
34
  // const __dirname = dirname(__filename);
@@ -208,6 +211,7 @@ async function run() {
208
211
  try {
209
212
  const sd = new StyleDictionary({
210
213
  source: resolvedPaths,
214
+ parsers: [LEGACY_TOKENS_PARSER_NAME],
211
215
  log: { verbosity: 'silent' },
212
216
  platforms: { css: { transforms: CSS_TRANSFORMS } },
213
217
  });
@@ -13,7 +13,8 @@
13
13
 
14
14
 
15
15
  // minimo package path in node_modules
16
- const minimo_path = '../../node_modules/@massimo-cassandro/minimo';
16
+ const node_modules_path = '../../node_modules',
17
+ minimo_path = `${node_modules_path}/@massimo-cassandro/minimo`;
17
18
 
18
19
  const config = {
19
20
 
@@ -33,6 +34,19 @@ const config = {
33
34
  // Source token files — accept paths, globs, or a mix of both.
34
35
  // Supported formats: .json, .jsonc, .mjs (must export a DTCG-compliant default object)
35
36
  //
37
+ // Legacy (non-DTCG) .json files — e.g. exported from Open Props — are
38
+ // auto-detected and converted to DTCG v5 syntax at parse time, so they can
39
+ // be freely mixed with regular DTCG sources (see README.md). Not covered:
40
+ // .jsonc, reserved for hand-authored DTCG sources. These .json files are
41
+ // parsed with plain JSON.parse (no comments, no trailing commas).
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.
49
+ //
36
50
  // WARNING: do not use path.join() to build glob patterns — it may corrupt
37
51
  // the pattern syntax. Use template literals instead:
38
52
  // OK: `${minimo_path}/**/*.{json,mjs}`
@@ -42,7 +56,7 @@ const config = {
42
56
  `${minimo_path}/src/**/*.tokens.{mjs,jsonc}`, // components tokens
43
57
 
44
58
  // optional extra token source (in this example, openProps, https://open-props.style/)
45
- // '../../node_modules/open-props/open-props.style-dictionary-tokens.json',
59
+ // `${node_modules_path}/open-props/open-props.style-dictionary-tokens.json`,
46
60
 
47
61
  // your project tokens
48
62
  './project-tokens/*.{js,mjs,jsonc,json}',
@@ -37,6 +37,10 @@
37
37
  {
38
38
  "filePath": "node_modules/open-props",
39
39
  "group": "Default"
40
+ },
41
+ {
42
+ "filePath": "node_modules/open-props/open-props.style-dictionary-tokens.json",
43
+ "group": "Default"
40
44
  }
41
45
  ],
42
46
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massimo-cassandro/minimo",
3
- "version": "1.34.0",
3
+ "version": "1.35.0",
4
4
  "description": "Light css framework & utilities",
5
5
  "type": "module",
6
6
  "main": "./index.js",