@massimo-cassandro/minimo 1.36.2 → 1.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dev-tools/design-tokens-builder/README.md +22 -0
- package/dev-tools/design-tokens-builder/build-tokens-src/build-source-modes.mjs +57 -14
- package/dev-tools/design-tokens-builder/build-tokens-src/config.mjs +42 -3
- package/dev-tools/design-tokens-builder/build-tokens-src/formats/css.mjs +259 -198
- package/dev-tools/design-tokens-builder/build-tokens-src/light-dark.mjs +126 -0
- package/dev-tools/design-tokens-builder/build-tokens-src/merge-css.mjs +25 -14
- package/dev-tools/design-tokens-builder/build-tokens-src/platforms.mjs +7 -2
- package/dev-tools/design-tokens-builder/build-tokens-src/source-prefixes.mjs +129 -0
- package/dev-tools/design-tokens-builder/build-tokens-src/transforms.mjs +20 -0
- package/dev-tools/design-tokens-builder/build-tokens.mjs +14 -0
- package/dev-tools/design-tokens-builder/check-unresolved-custom-props.mjs +13 -8
- package/dev-tools/design-tokens-builder/tokens-config-sample.mjs +55 -4
- package/dev-tools/starter-kit/starter-install.sh +1 -1
- package/package.json +1 -1
|
@@ -61,6 +61,7 @@ npx buildTokens --config ./path/to/tokens-config.mjs
|
|
|
61
61
|
The `source` array accepts any combination of:
|
|
62
62
|
- Concrete file paths: `'./tokens/colors.jsonc'`
|
|
63
63
|
- 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))
|
|
64
65
|
|
|
65
66
|
**Supported formats:**
|
|
66
67
|
- `.json` / `.jsonc` — parsed directly as token data
|
|
@@ -70,6 +71,27 @@ The `source` array accepts any combination of:
|
|
|
70
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
73
|
|
|
74
|
+
#### Source prefixes
|
|
75
|
+
|
|
76
|
+
An entry of `source` (or of a `sourceModes` mode) can be an object `{ src, prefix }` instead of a string: every custom property generated from the files matched by `src` (a path, a glob or an array of them) is rendered with the given prefix. Useful to avoid name collisions with third-party token sets, or to make their origin explicit:
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
source: [
|
|
80
|
+
'./my-tokens/*.mjs',
|
|
81
|
+
{ src: `${node_modules_path}/open-props/open-props.style-dictionary-tokens.json`, prefix: 'op' },
|
|
82
|
+
],
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
With this config Open Props' `gray.0` becomes `--op-gray-0`. `prefix` accepts `'op'`, `'op-'` or `'--op-'` (all equivalent).
|
|
86
|
+
|
|
87
|
+
- Only the custom property **name** changes, not the token path: `{references}` keep working, and a token of another source referencing `{gray.0}` is rendered as `var(--op-gray-0)`.
|
|
88
|
+
- The JSON output (`jsonBuildPath`) is **not** prefixed: it keeps the original token tree.
|
|
89
|
+
- `customPropsGroups` match the prefixed name, so a group can use `prefixes: ['op']`.
|
|
90
|
+
- Giving two different prefixes to the same file fails the build with an error.
|
|
91
|
+
- `check-unresolved-custom-props.mjs` understands the same syntax. Note that `extraCustomPropsFiles` entries are plain CSS files and are never prefixed.
|
|
92
|
+
|
|
93
|
+
See `build-tokens-src/source-prefixes.mjs`.
|
|
94
|
+
|
|
73
95
|
|
|
74
96
|
#### Token file format (JS example)
|
|
75
97
|
|
|
@@ -4,18 +4,25 @@
|
|
|
4
4
|
// light/dark custom-properties split.
|
|
5
5
|
//
|
|
6
6
|
// For each mode, a separate StyleDictionary instance is built from that
|
|
7
|
-
// mode's own `source` array. The generated
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// (e.g.
|
|
7
|
+
// mode's own `source` array. The generated `<selector> { ... }` CSS blocks
|
|
8
|
+
// (selector: customPropsSelector, default ':root' — see config.mjs) are then
|
|
9
|
+
// composed into a single destFile:
|
|
10
|
+
// - the base mode's block stays as a plain `<selector> { ... }` rule, with
|
|
11
|
+
// a `color-scheme: <all modes joined>;` declaration prepended (e.g.
|
|
12
|
+
// "light dark")
|
|
12
13
|
// - every other mode's block is wrapped in
|
|
13
|
-
// `@media (prefers-color-scheme: <mode>) {
|
|
14
|
+
// `@media (prefers-color-scheme: <mode>) { <selector> { ... } }`, with a
|
|
14
15
|
// `color-scheme: <mode>;` declaration prepended
|
|
15
16
|
// customPropsGroups, mergeCustomProps and pxToRem apply per mode, exactly as
|
|
16
17
|
// in the single-source build. addLayer (if set) wraps the whole composed
|
|
17
18
|
// result in a single `@layer`, rather than one per mode.
|
|
18
19
|
//
|
|
20
|
+
// When sourceModes defines both a `light` and a `dark` key and
|
|
21
|
+
// useLightDarkFunc is true (default), custom properties shared by both are
|
|
22
|
+
// reconciled into a single `light-dark()` declaration instead of being split
|
|
23
|
+
// across the base block and a `@media (prefers-color-scheme: dark)` rule —
|
|
24
|
+
// see light-dark.mjs.
|
|
25
|
+
//
|
|
19
26
|
// JSON tokens (if jsonBuildPath is set) are produced per mode: one set of
|
|
20
27
|
// files per mode, with `-<mode>` appended to each filename (see
|
|
21
28
|
// buildJsonFiles() in formats/json.mjs), e.g. "tokens-light.jsonc" /
|
|
@@ -26,15 +33,16 @@ import StyleDictionary from 'style-dictionary';
|
|
|
26
33
|
import * as path from 'node:path';
|
|
27
34
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
28
35
|
import { CSS_TRANSFORMS, JSON_TRANSFORMS } from './platforms.mjs';
|
|
29
|
-
import { customPropsCount } from './formats/css.mjs';
|
|
36
|
+
import { customPropsCount, lastFinalProps } from './formats/css.mjs';
|
|
30
37
|
import { buildJsonFiles, collectConcreteFilePaths } from './formats/json.mjs';
|
|
31
38
|
import { loadExistingCustomPropsScoped } from './merge-css.mjs';
|
|
32
39
|
import { LEGACY_TOKENS_PARSER_NAME } from './legacy-tokens-parser.mjs';
|
|
40
|
+
import { applyLightDarkFunc } from './light-dark.mjs';
|
|
33
41
|
|
|
34
42
|
/**
|
|
35
43
|
* @param {object} opts
|
|
36
44
|
* @param {Record<string,string[]>} opts.sourceModes Resolved source paths per mode (see config.mjs)
|
|
37
|
-
* @param {string} opts.baseMode Mode whose block stays a top-level
|
|
45
|
+
* @param {string} opts.baseMode Mode whose block stays a top-level `<selector> { ... }` rule
|
|
38
46
|
* @param {string} opts.buildPath Absolute path for CSS output directory
|
|
39
47
|
* @param {string} opts.destFile CSS output filename
|
|
40
48
|
* @param {string|null} opts.jsonBuildPath Absolute path for JSON output (null = disabled)
|
|
@@ -45,6 +53,8 @@ import { LEGACY_TOKENS_PARSER_NAME } from './legacy-tokens-parser.mjs';
|
|
|
45
53
|
* @param {boolean} opts.pxToRem
|
|
46
54
|
* @param {string|null} opts.addLayer
|
|
47
55
|
* @param {boolean|(string|RegExp)[]} opts.mergeCustomProps true = merge all sources, array = only the matching token source files
|
|
56
|
+
* @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
|
+
* @param {string} opts.customPropsSelector CSS selector wrapping the generated block(s) (default: ':root' — see config.mjs)
|
|
48
58
|
* @returns {Promise<{cssDestPath: string, totalCustomProps: number, jsonFilesByMode: Record<string, object[]>}>}
|
|
49
59
|
*/
|
|
50
60
|
export const buildSourceModes = async ({
|
|
@@ -60,13 +70,15 @@ export const buildSourceModes = async ({
|
|
|
60
70
|
pxToRem,
|
|
61
71
|
addLayer,
|
|
62
72
|
mergeCustomProps,
|
|
73
|
+
useLightDarkFunc,
|
|
74
|
+
customPropsSelector,
|
|
63
75
|
}) => {
|
|
64
76
|
const modeNames = Object.keys(sourceModes);
|
|
65
77
|
|
|
66
78
|
// Read the pre-existing destFile (if any) BEFORE any Style Dictionary
|
|
67
79
|
// instance runs, since the first one to write would overwrite it.
|
|
68
80
|
if (mergeCustomProps) {
|
|
69
|
-
loadExistingCustomPropsScoped(path.join(buildPath, destFile), baseMode, mergeCustomProps);
|
|
81
|
+
loadExistingCustomPropsScoped(path.join(buildPath, destFile), baseMode, mergeCustomProps, customPropsSelector);
|
|
70
82
|
}
|
|
71
83
|
|
|
72
84
|
const cssTransforms = pxToRem
|
|
@@ -75,13 +87,22 @@ export const buildSourceModes = async ({
|
|
|
75
87
|
|
|
76
88
|
/** @type {Record<string,string>} */
|
|
77
89
|
const modeBlocks = {};
|
|
78
|
-
|
|
90
|
+
// Per-mode custom-prop count and structured (name -> tail) props map,
|
|
91
|
+
// captured right after each per-mode format call (customPropsCount and
|
|
92
|
+
// lastFinalProps are live bindings, updated synchronously by the format
|
|
93
|
+
// function — see formats/css.mjs). Kept per mode (rather than summed on
|
|
94
|
+
// the fly) so applyLightDarkFunc() can later overwrite just the entries it
|
|
95
|
+
// actually touches (light, dark, and the base mode).
|
|
96
|
+
/** @type {Record<string,number>} */
|
|
97
|
+
const modeCounts = {};
|
|
98
|
+
/** @type {Record<string, Record<string,string>>} */
|
|
99
|
+
const modeFinalProps = {};
|
|
79
100
|
/** @type {Record<string, object[]>} */
|
|
80
101
|
const jsonFilesByMode = {};
|
|
81
102
|
|
|
82
103
|
for (const mode of modeNames) {
|
|
83
104
|
// Non-base modes may reference base-mode tokens (always defined, since
|
|
84
|
-
// the base block is the unconditional top-level
|
|
105
|
+
// the base block is the unconditional top-level `<selector>`). The base
|
|
85
106
|
// sources are passed as `include`: available for reference resolution
|
|
86
107
|
// but flagged isSource: false, and skipped by the css/json formats.
|
|
87
108
|
// Base-mode-only on purpose: a reference to a token that exists only in
|
|
@@ -102,6 +123,7 @@ export const buildSourceModes = async ({
|
|
|
102
123
|
format: 'css/variables-sorted',
|
|
103
124
|
options: {
|
|
104
125
|
outputReferences: true,
|
|
126
|
+
selector: customPropsSelector,
|
|
105
127
|
customPropsGroups,
|
|
106
128
|
mode,
|
|
107
129
|
colorScheme: mode === baseMode ? modeNames.join(' ') : mode,
|
|
@@ -116,9 +138,10 @@ export const buildSourceModes = async ({
|
|
|
116
138
|
// the composed multi-mode file is written once, at the end, below.
|
|
117
139
|
const [formatted] = await sd.formatPlatform('css');
|
|
118
140
|
modeBlocks[mode] = /** @type {string} */ (formatted.output);
|
|
119
|
-
// customPropsCount
|
|
120
|
-
// format function called above (see formats/css.mjs).
|
|
121
|
-
|
|
141
|
+
// customPropsCount / lastFinalProps are live bindings, updated
|
|
142
|
+
// synchronously by the format function called above (see formats/css.mjs).
|
|
143
|
+
modeCounts[mode] = customPropsCount;
|
|
144
|
+
modeFinalProps[mode] = lastFinalProps;
|
|
122
145
|
|
|
123
146
|
if (jsonBuildPath) {
|
|
124
147
|
const concreteFilePaths = jsonDestFile ? [] : await collectConcreteFilePaths(sd);
|
|
@@ -142,6 +165,26 @@ export const buildSourceModes = async ({
|
|
|
142
165
|
}
|
|
143
166
|
}
|
|
144
167
|
|
|
168
|
+
// ── light-dark() reconciliation (light/dark modes only) ──────────────────
|
|
169
|
+
// Rebuilds just the `light`, `dark` and base-mode blocks from the adjusted
|
|
170
|
+
// props maps — see light-dark.mjs. No-op (returns null) unless sourceModes
|
|
171
|
+
// defines both a `light` and a `dark` key.
|
|
172
|
+
if (useLightDarkFunc) {
|
|
173
|
+
const reconciled = applyLightDarkFunc({
|
|
174
|
+
modeFinalProps,
|
|
175
|
+
modeNames,
|
|
176
|
+
baseMode,
|
|
177
|
+
selector: customPropsSelector,
|
|
178
|
+
customPropsGroups,
|
|
179
|
+
});
|
|
180
|
+
if (reconciled) {
|
|
181
|
+
Object.assign(modeBlocks, reconciled.blocks);
|
|
182
|
+
Object.assign(modeCounts, reconciled.counts);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const totalCustomProps = Object.values(modeCounts).reduce((sum, n) => sum + n, 0);
|
|
187
|
+
|
|
145
188
|
// ── Compose the final CSS ────────────────────────────────────────────────
|
|
146
189
|
const otherModes = modeNames.filter((mode) => mode !== baseMode);
|
|
147
190
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import * as path from 'node:path';
|
|
12
12
|
import { pathToFileURL } from 'node:url';
|
|
13
|
-
import {
|
|
13
|
+
import { splitSourceEntries } from './source-prefixes.mjs';
|
|
14
14
|
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
16
|
// Read the --config flag
|
|
@@ -103,6 +103,26 @@ export const mergeCustomProps = (() => {
|
|
|
103
103
|
// Set to null, undefined or false (default) to emit no layer.
|
|
104
104
|
export const addLayer = buildConfig.addLayer ? String(buildConfig.addLayer).trim() : null;
|
|
105
105
|
|
|
106
|
+
// customPropsSelector: CSS selector wrapping the generated custom properties
|
|
107
|
+
// block(s) (see formats/css.mjs buildCssBlock()), instead of the default
|
|
108
|
+
// `:root`. Applies uniformly to a single-source build and to a sourceModes
|
|
109
|
+
// build (the base mode's top-level block and every other mode's block
|
|
110
|
+
// nested under its own `@media (prefers-color-scheme: <mode>)` rule all use
|
|
111
|
+
// the same selector). Falsy or blank values fall back to ':root'.
|
|
112
|
+
export const customPropsSelector = typeof buildConfig.customPropsSelector === 'string'
|
|
113
|
+
&& buildConfig.customPropsSelector.trim()
|
|
114
|
+
? buildConfig.customPropsSelector.trim()
|
|
115
|
+
: ':root';
|
|
116
|
+
|
|
117
|
+
// useLightDarkFunc: with sourceModes set (see below) and defining both a
|
|
118
|
+
// `light` and a `dark` key, custom properties present in both are written
|
|
119
|
+
// once as `--name: light-dark(<light>, <dark>);` instead of being split
|
|
120
|
+
// across the base mode's block and a
|
|
121
|
+
// `@media (prefers-color-scheme: dark) { ... }` rule (see light-dark.mjs).
|
|
122
|
+
// Ignored when sourceModes is not set, or does not define both a light and a
|
|
123
|
+
// dark mode. Default: true.
|
|
124
|
+
export const useLightDarkFunc = buildConfig.useLightDarkFunc !== false;
|
|
125
|
+
|
|
106
126
|
// customPropsGroups: custom properties whose first hyphen-separated name
|
|
107
127
|
// segment matches one of a group's prefixes are pulled out, labelled with
|
|
108
128
|
// that group's name and moved to the beginning of the generated CSS file, in
|
|
@@ -126,14 +146,33 @@ export const customPropsGroups = Array.isArray(buildConfig.customPropsGroups)
|
|
|
126
146
|
// properties split — see build-tokens-src/build-source-modes.mjs. When set
|
|
127
147
|
// (buildConfig.sourceModes is a non-null object), `source` is ignored and
|
|
128
148
|
// each mode's own array of source patterns is resolved the same way.
|
|
149
|
+
//
|
|
150
|
+
// Entries of `source` / of each mode can also be `{ src, prefix }` objects,
|
|
151
|
+
// to render the custom properties of those files with a prefix (see
|
|
152
|
+
// source-prefixes.mjs). They are flattened here into plain patterns for
|
|
153
|
+
// Style Dictionary; the prefixed ones are also collected in
|
|
154
|
+
// `prefixedSources`, to be registered by build-tokens.mjs.
|
|
129
155
|
// ---------------------------------------------------------------------------
|
|
130
156
|
const sourceModesRaw = buildConfig.sourceModes ?? null;
|
|
131
157
|
|
|
158
|
+
/** @type {import('./source-prefixes.mjs').PrefixedSource[]} */
|
|
159
|
+
export const prefixedSources = [];
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @param {Parameters<typeof splitSourceEntries>[0]} entries
|
|
163
|
+
* @returns {string[]}
|
|
164
|
+
*/
|
|
165
|
+
const parseSourceEntries = (entries) => {
|
|
166
|
+
const { patterns, prefixed } = splitSourceEntries(entries, configDir);
|
|
167
|
+
prefixedSources.push(...prefixed);
|
|
168
|
+
return patterns;
|
|
169
|
+
};
|
|
170
|
+
|
|
132
171
|
export const sourceModes = sourceModesRaw
|
|
133
172
|
? Object.fromEntries(
|
|
134
173
|
Object.entries(sourceModesRaw).map(([mode, modeSource]) => [
|
|
135
174
|
mode,
|
|
136
|
-
|
|
175
|
+
parseSourceEntries(modeSource),
|
|
137
176
|
])
|
|
138
177
|
)
|
|
139
178
|
: null;
|
|
@@ -156,4 +195,4 @@ if (sourceModes && buildConfig.sourceModesBase && sourceModesBase !== buildConfi
|
|
|
156
195
|
);
|
|
157
196
|
}
|
|
158
197
|
|
|
159
|
-
export const source = sourceModes ? null :
|
|
198
|
+
export const source = sourceModes ? null : parseSourceEntries(buildConfig.source);
|