@massimo-cassandro/minimo 1.35.1 → 1.36.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.
@@ -163,7 +163,7 @@ When set, each mode has its own `source` array, and the generated CSS composes a
163
163
 
164
164
  Tokens of a non-base mode can reference tokens defined in the base mode (e.g. a dark token using `{color.1}` defined in light, output as `var(--color-1)`). References to a non-base mode's tokens (from the base mode or from another non-base mode) fail with a "reference not defined" error, since the target would be undefined outside its own `@media` block.
165
165
 
166
- `customPropsGroups`, `mergeCustomProps`, `addLayer`, `pxToRem` etc. all apply the same way as with a single `source`. The JSON output (when `jsonBuildPath` is set) is produced once per mode, with the mode name appended to each filename (e.g. `tokens-light.jsonc` / `tokens-dark.jsonc`).
166
+ `customPropsGroups`, `mergeCustomProps`, `addLayer`, `pxToRem` etc. all apply the same way as with a single `source` (`mergeCustomProps` set to an array of strings/RegExps matches the token files of every mode). The JSON output (when `jsonBuildPath` is set) is produced once per mode, with the mode name appended to each filename (e.g. `tokens-light.jsonc` / `tokens-dark.jsonc`).
167
167
 
168
168
  ---
169
169
 
@@ -44,7 +44,7 @@ import { LEGACY_TOKENS_PARSER_NAME } from './legacy-tokens-parser.mjs';
44
44
  * @param {{name:string,prefixes:string[]}[]} opts.customPropsGroups
45
45
  * @param {boolean} opts.pxToRem
46
46
  * @param {string|null} opts.addLayer
47
- * @param {boolean} opts.mergeCustomProps
47
+ * @param {boolean|(string|RegExp)[]} opts.mergeCustomProps true = merge all sources, array = only the matching token source files
48
48
  * @returns {Promise<{cssDestPath: string, totalCustomProps: number, jsonFilesByMode: Record<string, object[]>}>}
49
49
  */
50
50
  export const buildSourceModes = async ({
@@ -66,7 +66,7 @@ export const buildSourceModes = async ({
66
66
  // Read the pre-existing destFile (if any) BEFORE any Style Dictionary
67
67
  // instance runs, since the first one to write would overwrite it.
68
68
  if (mergeCustomProps) {
69
- loadExistingCustomPropsScoped(path.join(buildPath, destFile), baseMode);
69
+ loadExistingCustomPropsScoped(path.join(buildPath, destFile), baseMode, mergeCustomProps);
70
70
  }
71
71
 
72
72
  const cssTransforms = pxToRem
@@ -84,8 +84,19 @@ export const pxToRem = buildConfig.pxToRem !== false;
84
84
 
85
85
  // mergeCustomProps: if true, custom properties already present in the CSS
86
86
  // destination file take priority over the ones generated by this build (see
87
- // build-tokens-src/merge-css.mjs). Default: false.
88
- export const mergeCustomProps = buildConfig.mergeCustomProps === true;
87
+ // build-tokens-src/merge-css.mjs). It can also be an array of strings/RegExps:
88
+ // only the properties generated by the token source files matching one of its
89
+ // entries are merged. An empty or invalid value means false. Default: false.
90
+ /** @type {boolean|(string|RegExp)[]} */
91
+ export const mergeCustomProps = (() => {
92
+ const value = buildConfig.mergeCustomProps;
93
+ if (value === true) return true;
94
+ if (Array.isArray(value)) {
95
+ const entries = value.filter((entry) => typeof entry === 'string' || entry instanceof RegExp);
96
+ return entries.length ? entries : false;
97
+ }
98
+ return false;
99
+ })();
89
100
 
90
101
  // addLayer: wraps the generated custom properties inside a CSS `@layer`
91
102
  // at-rule (see formats/css.mjs), e.g. `@layer project { :root { ... } }`.
@@ -192,6 +192,18 @@ StyleDictionary.registerFormat({
192
192
  // Tokens loaded via `include` (sourceModes: base-mode tokens made
193
193
  // available to other modes) stay in tokenByPath, so references to them
194
194
  // resolve to var(--name), but are not declared in this mode's block.
195
+ // property name → token source file, used by mergeCustomProps when the
196
+ // merge is limited to some source files (a token may generate several
197
+ // properties, e.g. typography)
198
+ /** @type {Record<string,string>} */
199
+ const sourceFileMap = {};
200
+ const trackSource = (token, tokenLines) => {
201
+ for (const name of Object.keys(parseCustomProps(tokenLines.join('\n')))) {
202
+ sourceFileMap[name] = token.filePath;
203
+ }
204
+ return tokenLines;
205
+ };
206
+
195
207
  const lines = dictionary.allTokens
196
208
  .filter((token) => token.isSource !== false)
197
209
  // .sort((a, b) => a.name.localeCompare(b.name, 'en', {numeric: true, sensitivity: 'base'}))
@@ -207,7 +219,7 @@ StyleDictionary.registerFormat({
207
219
  .split('\n')
208
220
  .map((line) => `${line}${commentSuffix}`)
209
221
  .join('\n');
210
- return [typographyLines];
222
+ return trackSource(token, [typographyLines]);
211
223
  }
212
224
 
213
225
  let value;
@@ -259,7 +271,7 @@ StyleDictionary.registerFormat({
259
271
  .replace(/\(\s+/g, '(')
260
272
  .replace(/\s+\)/g, ')');
261
273
 
262
- return [` --${token.name}: ${value};${commentSuffix}`];
274
+ return trackSource(token, [` --${token.name}: ${value};${commentSuffix}`]);
263
275
  });
264
276
 
265
277
  // Build a name → declaration-tail map from the declarations generated
@@ -271,7 +283,7 @@ StyleDictionary.registerFormat({
271
283
  // scopes the merge to that mode, so same-named props with different
272
284
  // values across modes are never mixed up.
273
285
  const generatedProps = parseCustomProps(lines.join('\n'));
274
- const finalProps = mergeCustomProps(generatedProps, options.mode ?? null);
286
+ const finalProps = mergeCustomProps(generatedProps, options.mode ?? null, sourceFileMap);
275
287
 
276
288
  // Final output is always sorted alphabetically ascending by property
277
289
  // name, regardless of merge — this also keeps pre-existing-only
@@ -10,6 +10,12 @@
10
10
  // mergeCustomProps(generatedMap) while building the output, right before
11
11
  // serialising the final CSS text.
12
12
  //
13
+ // `mergeCustomProps` may also be an array of strings/RegExps: in that case
14
+ // only the generated properties whose token source file matches one of the
15
+ // entries are merged (pre-existing value wins); the others are overwritten by
16
+ // the generated value. Pre-existing properties that are no longer generated
17
+ // (no token, hence no source file) are always kept. See matchesSource().
18
+ //
13
19
  // Usage (sourceModes build, see build-source-modes.mjs):
14
20
  // 1. Call loadExistingCustomPropsScoped(absDestPath, baseMode) BEFORE any
15
21
  // of the per-mode Style Dictionary instances run.
@@ -36,7 +42,11 @@ export const parseCustomProps = (cssText) => {
36
42
  /** @type {Record<string,string>} */
37
43
  const map = {};
38
44
  for (const match of cssText.matchAll(CUSTOM_PROP_RE)) {
39
- map[match[1]] = match[2].trim();
45
+ let tail = match[2].trim();
46
+ // A multi-line comment is cut at the end of the line (only single-line
47
+ // declarations are parsed): close it, or the resulting CSS would be invalid
48
+ if (tail.lastIndexOf('/*') > tail.lastIndexOf('*/')) tail += ' */';
49
+ map[match[1]] = tail;
40
50
  }
41
51
  return map;
42
52
  };
@@ -124,6 +134,32 @@ const splitModeScopes = (cssText) => {
124
134
  return scopes;
125
135
  };
126
136
 
137
+ // `true` = merge every source; an array = merge only the properties generated
138
+ // by the token source files matching one of its entries. Set by the
139
+ // loadExistingCustomProps*() functions.
140
+ /** @type {true|(string|RegExp)[]} */
141
+ let mergeSources = true;
142
+
143
+ /**
144
+ * Whether a token source file matches one of the `mergeCustomProps` array
145
+ * entries: a string matches if it is contained in the file path (path
146
+ * separators normalised to `/`), a RegExp if it tests true against it.
147
+ * @param {(string|RegExp)[]} sources
148
+ * @param {string|undefined} filePath token source file path
149
+ * @returns {boolean}
150
+ */
151
+ const matchesSource = (sources, filePath) => {
152
+ if (!filePath) return false;
153
+ const normalized = filePath.replace(/\\/g, '/');
154
+ return sources.some((entry) => {
155
+ if (entry instanceof RegExp) {
156
+ entry.lastIndex = 0;
157
+ return entry.test(normalized);
158
+ }
159
+ return normalized.includes(entry.replace(/\\/g, '/'));
160
+ });
161
+ };
162
+
127
163
  // null = merge disabled, or destination file not found yet (first build)
128
164
  /** @type {Record<string,string>|null} */
129
165
  let existingCustomProps = null;
@@ -137,9 +173,12 @@ let scopedExistingCustomProps = null;
137
173
  * Reads the CSS destination file (if it exists) and stores a name → value
138
174
  * map of its current custom properties for later merging.
139
175
  * @param {string} absDestPath absolute path of the CSS file to read
176
+ * @param {true|(string|RegExp)[]} [sources] `true` merges every source, an
177
+ * array only the token source files matching one of its entries (default: true)
140
178
  * @returns {Record<string,string>|null}
141
179
  */
142
- export const loadExistingCustomProps = (absDestPath) => {
180
+ export const loadExistingCustomProps = (absDestPath, sources = true) => {
181
+ mergeSources = sources;
143
182
  existingCustomProps = existsSync(absDestPath)
144
183
  ? parseCustomProps(readFileSync(absDestPath, 'utf8'))
145
184
  : null;
@@ -154,9 +193,12 @@ export const loadExistingCustomProps = (absDestPath) => {
154
193
  * @param {string} baseMode mode whose declarations live in the top-level,
155
194
  * non-nested `:root { ... }` block (no default — always passed explicitly
156
195
  * by build-source-modes.mjs, from the resolved sourceModesBase)
196
+ * @param {true|(string|RegExp)[]} [sources] `true` merges every source, an
197
+ * array only the token source files matching one of its entries (default: true)
157
198
  * @returns {Record<string, Record<string,string>>|null}
158
199
  */
159
- export const loadExistingCustomPropsScoped = (absDestPath, baseMode) => {
200
+ export const loadExistingCustomPropsScoped = (absDestPath, baseMode, sources = true) => {
201
+ mergeSources = sources;
160
202
  if (!existsSync(absDestPath)) {
161
203
  scopedExistingCustomProps = null;
162
204
  return scopedExistingCustomProps;
@@ -177,16 +219,26 @@ export const loadExistingCustomPropsScoped = (absDestPath, baseMode) => {
177
219
  * Merges freshly generated custom properties with the ones previously loaded
178
220
  * via loadExistingCustomProps() / loadExistingCustomPropsScoped(). Pre-existing
179
221
  * values take priority; custom properties found only in the pre-existing file
180
- * (no longer generated) are kept as well.
222
+ * (no longer generated) are kept as well. When the merge is limited to some
223
+ * token source files (array option), a generated property whose source file
224
+ * does not match is not overridden by its pre-existing value.
181
225
  * @param {Record<string,string>} generated name → value map produced by this build
182
226
  * @param {string|null} [mode] when set (sourceModes build), looks up the
183
227
  * pre-existing props scoped to this mode instead of the flat/global map (default: null)
228
+ * @param {Record<string,string>} [sourceFileMap] property name → token source
229
+ * file path, used only when the merge is limited to some sources (default: {})
184
230
  * @returns {Record<string,string>}
185
231
  */
186
- export const mergeCustomProps = (generated, mode = null) => {
187
- if (mode !== null) {
188
- const existing = scopedExistingCustomProps?.[mode];
189
- return existing ? { ...generated, ...existing } : generated;
232
+ export const mergeCustomProps = (generated, mode = null, sourceFileMap = {}) => {
233
+ const existing = mode !== null ? scopedExistingCustomProps?.[mode] : existingCustomProps;
234
+ if (!existing) return generated;
235
+ if (mergeSources === true) return { ...generated, ...existing };
236
+
237
+ const merged = { ...generated };
238
+ for (const [name, tail] of Object.entries(existing)) {
239
+ if (!(name in generated) || matchesSource(mergeSources, sourceFileMap[name])) {
240
+ merged[name] = tail;
241
+ }
190
242
  }
191
- return existingCustomProps ? { ...generated, ...existingCustomProps } : generated;
243
+ return merged;
192
244
  };
@@ -116,7 +116,7 @@ if (sourceModes) {
116
116
  // pre-existing values take priority — right before serialising the final
117
117
  // CSS output. See merge-css.mjs.
118
118
  if (mergeCustomProps) {
119
- loadExistingCustomProps(path.join(buildPath, destFile));
119
+ loadExistingCustomProps(path.join(buildPath, destFile), mergeCustomProps);
120
120
  }
121
121
 
122
122
  // Collect concrete source file paths (multi-file JSON mode only). Source
@@ -113,13 +113,25 @@ const config = {
113
113
  // are never touched regardless of this option.
114
114
  pxToRem: false,
115
115
 
116
- // If true, custom properties already present in the CSS destination file
117
- // (destFile, from a previous build) take priority over the ones generated
118
- // by this build. Properties found only in the pre-existing file (no longer
119
- // generated) are kept too. Useful to preserve manual overrides/additions
120
- // made directly in the compiled CSS across rebuilds. Ignored on the first
121
- // build, when destFile does not exist yet. Default: false.
122
- // Comments at the end of custom properties are preserved, while full-line ones will be lost.
116
+ // Preserves custom properties already present in the CSS destination file
117
+ // (destFile, from a previous build) across rebuilds: their values take
118
+ // priority over the ones generated by this build. Useful to keep manual
119
+ // overrides/additions made directly in the compiled CSS. Ignored on the
120
+ // first build, when destFile does not exist yet. Accepted values:
121
+ // false (default) - no merge: the destination file is fully regenerated
122
+ // true - merge all the sources
123
+ // array - strings and/or RegExps matched against the path of the
124
+ // token source files (those in `source` or `sourceModes`):
125
+ // only the properties generated by a matching file are
126
+ // merged, the others are overwritten by the generated value.
127
+ // A string matches if contained in the file path.
128
+ // An empty array is treated as false.
129
+ // e.g. ['/theme.minimo.tokens', /colors\.tokens\.(mjs|jsonc?)$/]
130
+ // With true or an array, properties found only in the pre-existing file (no
131
+ // longer generated, e.g. added by hand) are always kept.
132
+ // Comments at the end of a custom property line are preserved; full-line
133
+ // comments are lost, and a multi-line comment is cut at the end of its first
134
+ // line (and closed, to keep the CSS valid).
123
135
  mergeCustomProps: false,
124
136
 
125
137
  // Wraps the generated custom properties inside a CSS `@layer` at-rule,
@@ -202,11 +214,11 @@ const config = {
202
214
  // Additional CSS files that define custom properties used in the project
203
215
  // but not generated by build-tokens (e.g. third-party libraries)
204
216
  extraCustomPropsFiles: [
205
- // '../../node_modules/open-props/open-props.min.css'
217
+ // `${node_modules_path}/open-props/open-props.min.css`
206
218
  ],
207
219
 
208
220
  // Directory containing the CSS files to scan for unresolved custom properties
209
- dirToCheck: '../src',
221
+ dirToCheck: '../../app',
210
222
 
211
223
  // Regular expressions for custom property names to exclude from the check.
212
224
  // Useful for intentionally private/internal props (e.g. prefixed with --_).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massimo-cassandro/minimo",
3
- "version": "1.35.1",
3
+ "version": "1.36.0",
4
4
  "description": "Light css framework & utilities",
5
5
  "type": "module",
6
6
  "main": "./index.js",