@massimo-cassandro/minimo 1.33.7 → 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.
Files changed (20) hide show
  1. package/design-tokens/README.md +2 -197
  2. package/dev-tools/README.md +1 -0
  3. package/dev-tools/design-tokens-builder/README.md +212 -0
  4. package/dev-tools/design-tokens-builder/build-tokens-src/build-source-modes.mjs +155 -0
  5. package/{design-tokens/utilities → dev-tools/design-tokens-builder}/build-tokens-src/config.mjs +35 -1
  6. package/{design-tokens/utilities → dev-tools/design-tokens-builder}/build-tokens-src/formats/css.mjs +16 -4
  7. package/{design-tokens/utilities → dev-tools/design-tokens-builder}/build-tokens-src/formats/json.mjs +9 -4
  8. package/dev-tools/design-tokens-builder/build-tokens-src/legacy-tokens-parser.mjs +105 -0
  9. package/dev-tools/design-tokens-builder/build-tokens-src/merge-css.mjs +192 -0
  10. package/{design-tokens/utilities → dev-tools/design-tokens-builder}/build-tokens-src/platforms.mjs +3 -1
  11. package/dev-tools/design-tokens-builder/build-tokens.mjs +219 -0
  12. package/{design-tokens/utilities → dev-tools/design-tokens-builder}/check-unresolved-custom-props.mjs +70 -13
  13. package/{design-tokens → dev-tools/design-tokens-builder}/tokens-config-sample.mjs +55 -3
  14. package/dev-tools/starter-kit/source_files/root/__project__.code-workspace +4 -0
  15. package/dev-tools/starter-kit/starter-install.sh +2 -2
  16. package/package.json +6 -8
  17. package/design-tokens/utilities/build-tokens-src/merge-css.mjs +0 -60
  18. package/design-tokens/utilities/build-tokens.mjs +0 -153
  19. /package/{design-tokens/utilities → dev-tools/design-tokens-builder}/build-tokens-src/resolve-source-paths.mjs +0 -0
  20. /package/{design-tokens/utilities → dev-tools/design-tokens-builder}/build-tokens-src/transforms.mjs +0 -0
@@ -25,6 +25,10 @@ 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 { resolveSourcePaths } from './build-tokens-src/resolve-source-paths.mjs';
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();
28
32
 
29
33
  // const __filename = fileURLToPath(import.meta.url);
30
34
  // const __dirname = dirname(__filename);
@@ -90,25 +94,48 @@ async function run() {
90
94
  const definitionRegex = /--([a-z0-9-]+)(?=\s*:)/g;
91
95
 
92
96
  // defined_props keeps file/line info too, so unused properties (section 6)
93
- // can be reported with a link back to their definition.
94
- /** @type {{file: string, line: number, prop: string}[]} */
97
+ // can be reported with a link back to their definition. With sourceModes
98
+ // (see build-tokens-src/config.mjs), the same prop name can legitimately
99
+ // appear once per mode with a different value — `mode` disambiguates
100
+ // which one a given entry is, so section 5 resolves each to the correct
101
+ // per-mode source file instead of mixing them up (see findMediaModeBlocks
102
+ // in build-tokens-src/merge-css.mjs).
103
+ /** @type {{file: string, line: number, prop: string, mode: string|null}[]} */
95
104
  const defined_props = [];
96
105
 
97
106
  const cssContent = fs.readFileSync(custom_prop_file_path, 'utf-8');
98
107
  const custom_prop_relpath = path.relative(configDir, custom_prop_file_path);
108
+
109
+ // sourceModesBase mirrors the default-resolution logic in
110
+ // build-tokens-src/config.mjs: the configured sourceModesBase if it's
111
+ // actually a key of sourceModes, otherwise the first key.
112
+ const sourceModeNames = config.sourceModes ? Object.keys(config.sourceModes) : [];
113
+ const sourceModesBase = sourceModeNames.includes(config.sourceModesBase)
114
+ ? config.sourceModesBase
115
+ : sourceModeNames[0];
116
+ const mediaModeBlocks = config.sourceModes ? findMediaModeBlocks(cssContent) : [];
117
+
118
+ /** @param {number} index @returns {string|null} */
119
+ const modeAtIndex = (index) => {
120
+ if (!config.sourceModes) return null;
121
+ const block = mediaModeBlocks.find(b => index > b.start && index < b.end);
122
+ return block ? block.mode : sourceModesBase;
123
+ };
124
+
99
125
  [...cssContent.matchAll(definitionRegex)].forEach(m => {
100
126
  const line = cssContent.substring(0, m.index).split('\n').length;
101
- defined_props.push({ file: custom_prop_relpath, line, prop: `--${m[1]}` });
127
+ defined_props.push({ file: custom_prop_relpath, line, prop: `--${m[1]}`, mode: modeAtIndex(m.index) });
102
128
  });
103
129
 
104
- // Also include any extra CSS files declared in extraCustomPropsFiles
130
+ // Also include any extra CSS files declared in extraCustomPropsFiles.
131
+ // These are plain (non-sourceModes) files, so mode is always null.
105
132
  for (const extraFile of (config.extraCustomPropsFiles ?? [])) {
106
133
  const extraFilePath = path.resolve(configDir, extraFile);
107
134
  const extraContent = fs.readFileSync(extraFilePath, 'utf-8');
108
135
  const extraRelpath = path.relative(configDir, extraFilePath);
109
136
  [...extraContent.matchAll(definitionRegex)].forEach(m => {
110
137
  const line = extraContent.substring(0, m.index).split('\n').length;
111
- defined_props.push({ file: extraRelpath, line, prop: `--${m[1]}` });
138
+ defined_props.push({ file: extraRelpath, line, prop: `--${m[1]}`, mode: null });
112
139
  });
113
140
  }
114
141
 
@@ -161,28 +188,58 @@ async function run() {
161
188
  // CSS (or an extraCustomPropsFiles entry) — e.g. manual additions kept
162
189
  // across rebuilds via the mergeCustomProps option — have no token source
163
190
  // and are reported by name only.
191
+ //
192
+ // With sourceModes, resolution runs once PER MODE, keyed by mode name in
193
+ // sourceFileByPropByMode: the same prop name can be defined by a
194
+ // different source file in each mode (see the `dead: {...}` example in
195
+ // both a light and dark token file), so a single flattened name -> file
196
+ // map would silently point some entries at the wrong mode's file.
197
+ // sourceFileByProp (mode: null) covers the non-sourceModes case and
198
+ // extraCustomPropsFiles entries.
164
199
  // ---------------------------------------------------------------------------
165
200
  /** @type {Map<string, string>} */
166
201
  const sourceFileByProp = new Map();
167
-
168
- if (checkUnused && Array.isArray(config.source) && config.source.length) {
202
+ /** @type {Map<string, Map<string, string>>} */
203
+ const sourceFileByPropByMode = new Map();
204
+
205
+ /** @param {string[]} sourcePatterns @returns {Promise<Map<string,string>>} */
206
+ const resolveSourceFileMap = async (sourcePatterns) => {
207
+ /** @type {Map<string, string>} */
208
+ const map = new Map();
209
+ const resolvedPaths = resolveSourcePaths(sourcePatterns, configDir);
210
+ if (!resolvedPaths.length) return map;
169
211
  try {
170
- const tokenSourcePaths = resolveSourcePaths(config.source, configDir);
171
212
  const sd = new StyleDictionary({
172
- source: tokenSourcePaths,
213
+ source: resolvedPaths,
214
+ parsers: [LEGACY_TOKENS_PARSER_NAME],
173
215
  log: { verbosity: 'silent' },
174
216
  platforms: { css: { transforms: CSS_TRANSFORMS } },
175
217
  });
176
218
  const dictionary = await sd.getPlatformTokens('css');
177
-
178
219
  for (const token of dictionary.allTokens) {
179
- if (token.filePath) sourceFileByProp.set(`--${token.name}`, token.filePath);
220
+ if (token.filePath) map.set(`--${token.name}`, token.filePath);
180
221
  }
181
222
  } catch {
182
223
  // Non-fatal: unused properties will be reported by name only.
183
224
  }
225
+ return map;
226
+ };
227
+
228
+ if (checkUnused && config.sourceModes) {
229
+ for (const [mode, modeSource] of Object.entries(config.sourceModes)) {
230
+ sourceFileByPropByMode.set(mode, await resolveSourceFileMap(modeSource));
231
+ }
232
+ } else if (checkUnused && Array.isArray(config.source) && config.source.length) {
233
+ const map = await resolveSourceFileMap(config.source);
234
+ for (const [prop, filePath] of map) sourceFileByProp.set(prop, filePath);
184
235
  }
185
236
 
237
+ /** @param {{prop: string, mode: string|null}} item @returns {string|undefined} */
238
+ const resolveSourceFilePath = (item) =>
239
+ item.mode
240
+ ? sourceFileByPropByMode.get(item.mode)?.get(item.prop)
241
+ : sourceFileByProp.get(item.prop);
242
+
186
243
  // ---------------------------------------------------------------------------
187
244
  // 6. Build the list of unused custom properties (defined but never
188
245
  // referenced via var() in any scanned CSS file) — the opposite check
@@ -194,7 +251,7 @@ async function run() {
194
251
  .filter(item => !used_props_set.has(item.prop))
195
252
  .filter(item => {
196
253
  if (!ignoreUnusedInNodeModules) return true;
197
- const sourceFilePath = sourceFileByProp.get(item.prop);
254
+ const sourceFilePath = resolveSourceFilePath(item);
198
255
  if (!sourceFilePath) return true;
199
256
  return !/(^|[\\/])node_modules([\\/]|$)/.test(sourceFilePath);
200
257
  })
@@ -232,7 +289,7 @@ async function run() {
232
289
  '## Unused custom properties\n\n'
233
290
  + unused_props
234
291
  .map(item => {
235
- const sourceFilePath = sourceFileByProp.get(item.prop);
292
+ const sourceFilePath = resolveSourceFilePath(item);
236
293
  if (!sourceFilePath) return `* \`${item.prop}\``;
237
294
 
238
295
  const relSource = path.relative(configDir, sourceFilePath);
@@ -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,13 +56,51 @@ 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}',
49
-
50
63
  ],
51
64
 
65
+ // Optional light/dark (or other) custom properties split — alternative to
66
+ // `source`. If this key is set (a non-null object), `source` above is
67
+ // ignored: each key is a mode name with its own array of source patterns
68
+ // (same rules as `source`: paths, globs, or a mix).
69
+ //
70
+ // The generated CSS composes all modes into a single destFile:
71
+ // - the base mode's props (see sourceModesBase below) go in a top-level
72
+ // `:root { ... }` block, with `color-scheme: <all modes>;` prepended
73
+ // (e.g. "light dark")
74
+ // - every other mode's props go in
75
+ // `@media (prefers-color-scheme: <mode>) { :root { ... } }`, with
76
+ // `color-scheme: <mode>;` prepended
77
+ // customPropsGroups, mergeCustomProps, addLayer, pxToRem etc. all apply
78
+ // the same way as with a single `source`.
79
+ //
80
+ // The JSON output (see JSON TOKENS OUTPUT below) is produced once per
81
+ // mode, with the mode name appended to each filename, e.g. with
82
+ // jsonDestFile: 'tokens' -> "tokens-light.jsonc", "tokens-dark.jsonc".
83
+ //
84
+ // sample:
85
+ // sourceModes: {
86
+ // light: [
87
+ // `${minimo_path}/design-tokens/_src/**/*.tokens.{mjs,jsonc}`,
88
+ // `${minimo_path}/src/**/*.tokens.{mjs,jsonc}`,
89
+ // './project-tokens/light/*.{js,mjs,jsonc,json}',
90
+ // ],
91
+ // dark: [
92
+ // './project-tokens/dark/*.{js,mjs,jsonc,json}',
93
+ // ],
94
+ // },
95
+ sourceModes: null,
96
+
97
+ // With sourceModes set: the mode whose declarations go in the top-level
98
+ // `:root { ... }` block, instead of nested inside its own
99
+ // `@media (prefers-color-scheme: <mode>) { ... }` rule (default: the first
100
+ // key of sourceModes, e.g. 'light'). Ignored when sourceModes is not set.
101
+ sourceModesBase: 'light',
102
+
103
+
52
104
  // If true (default), dimension token values expressed in px are converted
53
105
  // to rem in the generated CSS. Values in other units (em, %, vh, dvw, ...)
54
106
  // are never touched regardless of this option.
@@ -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
 
@@ -75,7 +75,7 @@ optionalPackages=(
75
75
  "@svgdotjs/svg.js | | richiesto da charts/"
76
76
  "blurhash | | richiesto da unsplash-page (create-blurhash-canvas)"
77
77
  "simple-datatables | | richiesto da s-datatable-component"
78
- "style-dictionary | dev | richiesto da design-tokens/utilities (build-tokens)"
78
+ "style-dictionary | dev | richiesto da dev-tools/design-tokens-builder (build-tokens)"
79
79
  "@cybozu/eslint-config | dev | utilizzato da eslint per il check dei file css"
80
80
  "postcss-cli postcss-import postcss-nested cssnano | dev | per css email"
81
81
  )
@@ -421,7 +421,7 @@ force_cat "${MINIMO_PKG_DIR}/src/custom-media.css" "${FRONTEND_INSTALL_PATH}/css
421
421
  force_cat "${MINIMO_PKG_DIR}/src/minimo.css" "${FRONTEND_INSTALL_PATH}/css/index.css"
422
422
 
423
423
  # config di build-tokens: collocato accanto al css generato (custom-properties.css).
424
- force_cat "${MINIMO_PKG_DIR}/design-tokens/tokens-config-sample.mjs" "${FRONTEND_INSTALL_PATH}/css/tokens-config.mjs"
424
+ force_cat "${MINIMO_PKG_DIR}/dev-tools/design-tokens-builder/tokens-config-sample.mjs" "${FRONTEND_INSTALL_PATH}/css/tokens-config.mjs"
425
425
 
426
426
 
427
427
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massimo-cassandro/minimo",
3
- "version": "1.33.7",
3
+ "version": "1.35.0",
4
4
  "description": "Light css framework & utilities",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -20,8 +20,8 @@
20
20
  "access": "public"
21
21
  },
22
22
  "bin": {
23
- "buildTokens": "design-tokens/utilities/build-tokens.mjs",
24
- "checkUnresolvedProps": "design-tokens/utilities/check-unresolved-custom-props.mjs",
23
+ "buildTokens": "dev-tools/design-tokens-builder/build-tokens.mjs",
24
+ "checkUnresolvedProps": "dev-tools/design-tokens-builder/check-unresolved-custom-props.mjs",
25
25
  "update-version": "dev-tools/update-version/index.mjs",
26
26
  "upd@m": "dev-tools/upd@m/index.mjs",
27
27
  "svgIconsTools": "dev-tools/svg-icons-tools/index.mjs",
@@ -36,16 +36,16 @@
36
36
  "UPD-version": "npx update-version # --config=./dev-utilities.config.mjs",
37
37
  "npm-publish": "npm publish",
38
38
  "upd@m": "npx upd@m",
39
- "build-tokens": "node ./design-tokens/utilities/build-tokens.mjs --config ./design-tokens/tokens-config.mjs",
39
+ "build-tokens": "node ./dev-tools/design-tokens-builder/build-tokens.mjs --config ./design-tokens/tokens-config.mjs",
40
40
  "build-margin-padding-css": "node ./scripts/build-margin-padding-css.mjs",
41
41
  "generate:types": "node ./scripts/generate-class-types.mjs",
42
42
  "prepublishOnly": "npm run generate:types",
43
- "check unresolved props": "node ./design-tokens/utilities/check-unresolved-custom-props.mjs --config ./design-tokens/tokens-config.mjs",
43
+ "check unresolved props": "node ./dev-tools/design-tokens-builder/check-unresolved-custom-props.mjs --config ./design-tokens/tokens-config.mjs",
44
44
  "test": "node --test \"test/**/*.test.mjs\"",
45
45
  "test sideEffects tree-shaking": "node ./test/sideEffects-tree-shaking/run-test.mjs",
46
46
  "checkJS": "npx tsc --noEmit -p jsconfig.json",
47
47
  "checkJS ./src": "npx tsc --noEmit --allowJs --checkJs --target ES2023 --module ESNext --allowSyntheticDefaultImports --moduleResolution bundler declarations.d.ts types/global.d.ts src/**/*.js",
48
- "check outdated": "echo '*** ROOT ***' && npm outdated --omit=peer && echo '*** DEMO ***' && npm --prefix ./demo outdated && echo '*** END ***'",
48
+ "npm outdated": "echo '*** ROOT ***' && npm outdated --omit=peer && echo '*** DEMO ***' && npm --prefix ./demo outdated && echo '*** END ***'",
49
49
  "npm upd": "echo '*** ROOT ***' && npm update && echo '*** DEMO ***' && npm --prefix ./demo update && echo '*** END ***'"
50
50
  },
51
51
  "license": "MIT",
@@ -63,10 +63,8 @@
63
63
  "src/**/*.{js,mjs,css,svg,md}",
64
64
  "types/**/*.d.ts",
65
65
  "design-tokens/README.md",
66
- "design-tokens/tokens-config-sample.mjs",
67
66
  "design-tokens/tokens/**/*.{tokens.json,tokens.jsonc}",
68
67
  "design-tokens/_src/**/*.{tokens.json,tokens.jsonc,mjs,js,md}",
69
- "design-tokens/utilities/**/*.{mjs,md}",
70
68
  "charts/**/*.{js,mjs,md}",
71
69
  "dev-tools/**/*",
72
70
  "snippets/**/*",
@@ -1,60 +0,0 @@
1
- // build-tokens-src/merge-css.mjs
2
- // Supports the `mergeCustomProps` config option: pre-existing custom
3
- // properties in the CSS destination file take priority over the ones
4
- // generated by the current build (manual overrides/additions survive rebuilds).
5
- //
6
- // Usage:
7
- // 1. Call loadExistingCustomProps(absDestPath) BEFORE Style Dictionary runs,
8
- // since it reads the file that is about to be overwritten.
9
- // 2. The css/variables-sorted format (see formats/css.mjs) calls
10
- // mergeCustomProps(generatedMap) while building the output, right before
11
- // serialising the final CSS text.
12
-
13
- import { existsSync, readFileSync } from 'node:fs';
14
-
15
- const CUSTOM_PROP_RE = /^\s*--([\w-]+):\s*(.+)$/gm;
16
-
17
- /**
18
- * Parses `--name: <tail>` declarations out of a CSS text block. <tail> is
19
- * captured verbatim (value, terminating `;`, and anything else on the same
20
- * line, e.g. a trailing comment) so it round-trips unchanged through a merge.
21
- * Only single-line declarations are supported.
22
- * @param {string} cssText
23
- * @returns {Record<string,string>} name → declaration-tail map, in first-seen order
24
- */
25
- export const parseCustomProps = (cssText) => {
26
- /** @type {Record<string,string>} */
27
- const map = {};
28
- for (const match of cssText.matchAll(CUSTOM_PROP_RE)) {
29
- map[match[1]] = match[2].trim();
30
- }
31
- return map;
32
- };
33
-
34
- // null = merge disabled, or destination file not found yet (first build)
35
- /** @type {Record<string,string>|null} */
36
- let existingCustomProps = null;
37
-
38
- /**
39
- * Reads the CSS destination file (if it exists) and stores a name → value
40
- * map of its current custom properties for later merging.
41
- * @param {string} absDestPath absolute path of the CSS file to read
42
- * @returns {Record<string,string>|null}
43
- */
44
- export const loadExistingCustomProps = (absDestPath) => {
45
- existingCustomProps = existsSync(absDestPath)
46
- ? parseCustomProps(readFileSync(absDestPath, 'utf8'))
47
- : null;
48
- return existingCustomProps;
49
- };
50
-
51
- /**
52
- * Merges freshly generated custom properties with the ones previously loaded
53
- * via loadExistingCustomProps(). Pre-existing values take priority; custom
54
- * properties found only in the pre-existing file (no longer generated) are
55
- * kept as well.
56
- * @param {Record<string,string>} generated name → value map produced by this build
57
- * @returns {Record<string,string>}
58
- */
59
- export const mergeCustomProps = (generated) =>
60
- existingCustomProps ? { ...generated, ...existingCustomProps } : generated;
@@ -1,153 +0,0 @@
1
- #!/usr/bin/env node
2
- /// <reference types="node" />
3
- /* eslint-disable no-console */
4
-
5
- // build-tokens.mjs
6
- // Entry point: imports modules, runs the build, lints the CSS output, prints the log.
7
- //
8
- // Project structure:
9
- // build-tokens.mjs <- this file
10
- // build-tokens-src/config.mjs <- reads --config flag and resolves all paths
11
- // build-tokens-src/transforms.mjs <- registers custom Style Dictionary transforms
12
- // build-tokens-src/formats/css.mjs <- registers the css/variables-sorted format
13
- // build-tokens-src/formats/json.mjs <- registers the json/tokens format, exports helpers
14
- // build-tokens-src/platforms.mjs <- builds the platforms object (css + optional json)
15
-
16
- import StyleDictionary from 'style-dictionary';
17
- import * as path from 'node:path';
18
- import { styleText } from 'node:util';
19
- import { homedir } from 'node:os';
20
- import stylelint from 'stylelint';
21
- import { pathToFileURL } from 'node:url';
22
-
23
- // ── 1. Configuration (args + resolved paths) ────────────────────────────────
24
- import {
25
- configAbsPath,
26
- buildPath,
27
- destFile,
28
- stylelintConfigPath,
29
- jsonBuildPath,
30
- jsonDestFile,
31
- jsonFormat,
32
- jsonExpression,
33
- source,
34
- mergeCustomProps,
35
- customPropsGroups,
36
- pxToRem,
37
- addLayer,
38
- } from './build-tokens-src/config.mjs';
39
-
40
- // ── 2. Custom transforms ─────────────────────────────────────────────────────
41
- import './build-tokens-src/transforms.mjs';
42
-
43
- // ── 3. CSS format ─────────────────────────────────────────────────────────────
44
- // customPropsCount is updated by the format at render time
45
- // loadExistingCustomProps supports the mergeCustomProps option (see below)
46
- import { customPropsCount } from './build-tokens-src/formats/css.mjs';
47
- import { loadExistingCustomProps } from './build-tokens-src/merge-css.mjs';
48
-
49
- // ── 4. JSON format ────────────────────────────────────────────────────────────
50
- import { buildJsonFiles, collectConcreteFilePaths } from './build-tokens-src/formats/json.mjs';
51
-
52
- // ── 5. Platforms builder ──────────────────────────────────────────────────────
53
- import { buildPlatforms } from './build-tokens-src/platforms.mjs';
54
-
55
- // ── 5b. Pre-existing custom properties (mergeCustomProps option) ────────────
56
- // Read before Style Dictionary runs, since it overwrites the destination
57
- // file. The css/variables-sorted format (formats/css.mjs) merges these
58
- // values in — pre-existing values take priority — right before serialising
59
- // the final CSS output. See merge-css.mjs.
60
- if (mergeCustomProps) {
61
- loadExistingCustomProps(path.join(buildPath, destFile));
62
- }
63
-
64
- // ── 6. Collect concrete source file paths (multi-file JSON mode only) ────────
65
- // Source entries may be glob patterns. SD expands them internally and stores
66
- // the concrete path in token.filePath. We need those concrete paths to build
67
- // the per-file descriptors for the json platform.
68
- //
69
- // SD v5 lazy-loads sources: `await sd.hasInitialized` triggers loading;
70
- // after that sd.allTokens is a plain synchronous array with filePath populated.
71
- //
72
- // In single-file mode (jsonDestFile is set) we skip this step entirely.
73
- let concreteFilePaths = [];
74
-
75
- if (jsonBuildPath && !jsonDestFile) {
76
- const sdInit = new StyleDictionary({ source, log: { verbosity: 'silent' }, platforms: {} });
77
- concreteFilePaths = await collectConcreteFilePaths(sdInit);
78
- }
79
-
80
- // ── 7. Clean json output directory ───────────────────────────────────────────
81
- // Remove only previously generated *.json/*.jsonc files, so that tokens
82
- // removed from the source are not left behind as stale artefacts, while any
83
- // other file placed in jsonBuildPath by the user (e.g. a README) is left
84
- // untouched. Files are written flat in jsonBuildPath (no subdirectories), so
85
- // only its top level is scanned.
86
- if (jsonBuildPath) {
87
- const { readdir, rm, mkdir } = await import('fs/promises');
88
- const existingEntries = await readdir(jsonBuildPath, { withFileTypes: true }).catch(() => []);
89
- await Promise.all(
90
- existingEntries
91
- .filter((entry) => entry.isFile() && /\.jsonc?$/.test(entry.name))
92
- .map((entry) => rm(path.join(jsonBuildPath, entry.name)))
93
- );
94
- await mkdir(jsonBuildPath, { recursive: true });
95
- }
96
-
97
- // ── 8. Build ──────────────────────────────────────────────────────────────────
98
- const sd = new StyleDictionary({
99
- source,
100
- log: { verbosity: 'verbose' },
101
- platforms: buildPlatforms({
102
- buildPath,
103
- destFile,
104
- jsonBuildPath,
105
- jsonDestFile,
106
- jsonFormat,
107
- jsonExpression,
108
- concreteFilePaths,
109
- customPropsGroups,
110
- pxToRem,
111
- addLayer,
112
- }),
113
- });
114
-
115
- await sd.buildAllPlatforms();
116
-
117
- // ── 9. CSS lint ───────────────────────────────────────────────────────────────
118
- const stylelintConfig = await import(pathToFileURL(stylelintConfigPath).href).then(m => m.default);
119
-
120
-
121
- await stylelint.lint({
122
- config: stylelintConfig,
123
- files: [path.join(buildPath, destFile)],
124
- fix: true,
125
- });
126
-
127
- // ── 10. Log ────────────────────────────────────────────────────────────────────
128
- /** @param {string} p */
129
- const short = (p) => p.replace(homedir(), '~');
130
-
131
- console.log(styleText(['yellow'], `[build-tokens] config file : ${short(configAbsPath)}`));
132
- console.log(styleText(['yellow'], `[build-tokens] source : ${source.map(short)}`));
133
- console.log(styleText(['yellow'], `[build-tokens] dest file : ${short(path.join(buildPath, destFile))}`));
134
-
135
- if (jsonBuildPath) {
136
- const jsonFiles = buildJsonFiles(concreteFilePaths, jsonDestFile, jsonFormat, jsonExpression);
137
- if (jsonFiles.length === 1) {
138
- console.log(styleText(['yellow'],
139
- `[build-tokens] json : ${short(path.join(jsonBuildPath, jsonFiles[0].destination))}`
140
- ));
141
- } else {
142
- console.log(styleText(['yellow'],
143
- `[build-tokens] json : ${jsonFiles.length} files in ${short(jsonBuildPath)}`
144
- ));
145
- }
146
- } else {
147
- console.log(styleText(['yellow'],
148
- '[build-tokens] json : (disabled — jsonBuildPath not set)'
149
- ));
150
- }
151
-
152
- console.log(styleText(['green'], `[build-tokens] ${customPropsCount} custom properties processed`));
153
- console.log(styleText(['green'], '**** DONE ****'));