synthesisui 0.16.9 → 0.16.10

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.
@@ -180,6 +180,57 @@ export function parseTokens(css) {
180
180
  * Harmless while nothing asked about existence. The moment the phantom check
181
181
  * did, it called them invented (caught before shipping, 27/07).
182
182
  */
183
+ /**
184
+ * AN ALIAS IS NOT A DEAD END. IT IS USUALLY THE ANSWER.
185
+ *
186
+ * Every real system is written in two layers - `--ds-color-blue-600: #2563eb`
187
+ * on the shelf, `--ds-color-semantic-info: var(--ds-color-blue-600)` for the
188
+ * intent - and the value table only ever held the first. So `tokenFor` answered
189
+ * `#2563eb → --ds-color-blue-600`, the shelf, and its own rule two lines below
190
+ * ("semantic roles name intent; prefer intent") could never fire, because no
191
+ * semantic name was in the table to prefer.
192
+ *
193
+ * That is the one sentence the product is sold on: not "you have a hardcoded
194
+ * colour" but "your system already calls it this". It was naming the wrong
195
+ * thing on every system that layers its tokens, which is all of them. Found
196
+ * when an agent using the MCP tools read the answer, checked it against
197
+ * CLAUDE.md, and corrected the tool (my-test4, 27/07).
198
+ *
199
+ * Following the chain costs one pass and needs no CSS engine: a token either
200
+ * holds a literal or points at exactly one other token.
201
+ */
202
+ export function resolveAliases(raw) {
203
+ const out = new Map();
204
+ const literal = (name, depth) => {
205
+ // A cycle is malformed CSS, not something to crash on. Ten hops is far
206
+ // past any real system and stops the recursion dead.
207
+ if (depth > 10)
208
+ return null;
209
+ const value = raw.get(name);
210
+ if (value === undefined)
211
+ return null;
212
+ const alias = /^var\(\s*(--[a-z0-9_-]+)/i.exec(value.trim());
213
+ return alias ? literal(alias[1].toLowerCase(), depth + 1) : value;
214
+ };
215
+ for (const name of raw.keys()) {
216
+ const value = literal(name, 0);
217
+ if (value !== null)
218
+ out.set(name, value);
219
+ }
220
+ return out;
221
+ }
222
+ /** Every `--ds-*` declaration, aliases kept - the input `resolveAliases` needs.
223
+ * `parseTokens` drops them, which is right for a table of literals and wrong
224
+ * for a table of what things are called. */
225
+ export function parseTokensWithAliases(css) {
226
+ const out = new Map();
227
+ for (const m of css.matchAll(/(--ds-[a-z0-9-]+)\s*:\s*([^;}]+)[;}]/gi)) {
228
+ const name = m[1].toLowerCase();
229
+ if (!out.has(name))
230
+ out.set(name, m[2].trim());
231
+ }
232
+ return out;
233
+ }
183
234
  export function parseDeclaredNames(css) {
184
235
  const out = new Set();
185
236
  for (const m of css.matchAll(/(--[a-z0-9_-]+)\s*:\s*[^;}]+[;}]/gi))
@@ -280,14 +331,35 @@ export function buildTable(input) {
280
331
  const byName = source === "installed"
281
332
  ? parseTokens(input.css)
282
333
  : parseRootTokens(input.css);
334
+ /**
335
+ * Built from the RESOLVED map, not from `byName`. A semantic role that
336
+ * aliases a primitive has to be findable by the primitive's value, or the
337
+ * "prefer intent" rule below is unreachable.
338
+ */
339
+ const resolved = source === "installed"
340
+ ? resolveAliases(parseTokensWithAliases(input.css))
341
+ : byName;
342
+ /**
343
+ * Aliases first in every list.
344
+ *
345
+ * `tokenFor` preferred a name containing `-semantic-`, which covers colour
346
+ * and nothing else: `--ds-spacing-md: var(--ds-spacing-4)` has no such
347
+ * marker, so the answer came back as the raw step rather than the scale name
348
+ * anyone actually writes. The general rule is structural, not lexical - in a
349
+ * layered system the name that POINTS at another name is the layer above.
350
+ */
351
+ const raw = source === "installed" ? parseTokensWithAliases(input.css) : new Map();
352
+ const isAlias = (name) => /^var\(/.test((raw.get(name) ?? "").trim());
283
353
  const byValue = new Map();
284
- for (const [name, value] of byName) {
354
+ for (const [name, value] of resolved) {
285
355
  const key = normalizeValue(value);
286
356
  const list = byValue.get(key);
287
- if (list)
288
- list.push(name);
289
- else
357
+ if (!list)
290
358
  byValue.set(key, [name]);
359
+ else if (isAlias(name))
360
+ list.unshift(name);
361
+ else
362
+ list.push(name);
291
363
  }
292
364
  return {
293
365
  source: byName.size > 0 ? source : null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.9",
3
+ "version": "0.16.10",
4
4
  "description": "Bring SynthesisUI design systems into any project - tokens, typed components, whole pages and an agent-ready CLAUDE.md manifest.",
5
5
  "type": "module",
6
6
  "bin": {