@yahoo/uds-create-config 4.22.1 → 4.23.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.
@@ -19,6 +19,7 @@ import { instanceAt } from "../../spec/instance.js";
19
19
  import { Package } from "./Package.js";
20
20
  import { File } from "./File.js";
21
21
  import { iconDomainInvariants } from "./icon-domain-invariants.js";
22
+ import { rootLayerInvariants } from "./root-layer-invariants.js";
22
23
  import { Token } from "./Token.js";
23
24
  import { intoName, routedPropIn, routesContent } from "../../framework/prop-surface.js";
24
25
  import { z } from "zod";
@@ -744,7 +745,7 @@ function exampleStyleBag(config, primitivePath) {
744
745
  const bag = {};
745
746
  for (const [name, prop] of Object.entries(props)) {
746
747
  if (prop?.type !== "styleProperty" || !isRef(prop.value)) continue;
747
- const leaves = stylePropValueLeaves(config, pathOf(prop.value.__ref));
748
+ const leaves = stylePropValueLeaves(config, pathOf(prop.value.__ref), { retired: false });
748
749
  const value = leaves.find((leaf) => !inert.has(leaf)) ?? leaves[0];
749
750
  if (value !== void 0) bag[name] = value;
750
751
  if (Object.keys(bag).length >= 3) break;
@@ -1371,7 +1372,8 @@ const AnatomyNode = defineSubEntity({
1371
1372
  ...node,
1372
1373
  element: { __ref: `layer:${key}` }
1373
1374
  } : node);
1374
- }
1375
+ },
1376
+ listedIn: ["anatomy/*/children", "anatomy/*/slots/*"]
1375
1377
  });
1376
1378
  const componentFields = z.strictObject({
1377
1379
  /**
@@ -2071,7 +2073,7 @@ subtree: z.boolean().default(true) }).optional()
2071
2073
  handler: (input, config) => validateComponentProps(config, String(input.path), input.data.props ?? {})
2072
2074
  }
2073
2075
  },
2074
- invariants: (config) => iconDomainInvariants(config)
2076
+ invariants: (config) => [...iconDomainInvariants(config), ...rootLayerInvariants(config)]
2075
2077
  });
2076
2078
  const baseComponentRef = ComponentEntity.ref.bind(ComponentEntity);
2077
2079
  /** Component refs add the source-only authoring steps to the generic ref-extension value. All of them
@@ -0,0 +1,9 @@
1
+ import type { Config } from '../../framework/Config';
2
+ /**
3
+ * A component's layers include one named `root`. The name is what the anatomy's edge list starts
4
+ * from, where a slot prop's content lands when it has no node of its own, and what the authored
5
+ * TypeScript surface types the root element by, so a component whose layers are named anything
6
+ * else renders a top nothing addresses. A component with no layers at all is not judged here: a
7
+ * leaf renders as itself and declares none.
8
+ */
9
+ export declare const rootLayerInvariants: (config: Config) => string[];
@@ -0,0 +1,25 @@
1
+ import { isPlainRecord } from "./style-bag.js";
2
+ import { createSliceMemo } from "../../framework/memo.js";
3
+ //#region src/entities/system/root-layer-invariants.ts
4
+ /**
5
+ * A component's layers include one named `root`. The name is what the anatomy's edge list starts
6
+ * from, where a slot prop's content lands when it has no node of its own, and what the authored
7
+ * TypeScript surface types the root element by, so a component whose layers are named anything
8
+ * else renders a top nothing addresses. A component with no layers at all is not judged here: a
9
+ * leaf renders as itself and declares none.
10
+ */
11
+ const rootLayerInvariants = createSliceMemo({
12
+ kinds: ["component"],
13
+ compute: (config) => {
14
+ const problems = [];
15
+ for (const item of config.list("component")) {
16
+ const layers = (config.resolve("component", item.path)?.toJSON())?.layers;
17
+ if (!isPlainRecord(layers) || Object.keys(layers).length === 0) continue;
18
+ if ("root" in layers) continue;
19
+ problems.push(`Component "${item.path}" has layers (${Object.keys(layers).join(", ")}) but none named "root"; the root layer is the top of the anatomy and keeps that name.`);
20
+ }
21
+ return problems;
22
+ }
23
+ });
24
+ //#endregion
25
+ export { rootLayerInvariants };
@@ -1623,8 +1623,10 @@ declare class Config {
1623
1623
  *
1624
1624
  * An authoring question, not a resolution one. The entity is still here and still resolves, because
1625
1625
  * references already going through it have to keep working — so nothing that emits a *value* reads
1626
- * this. What does: a surface that lists paths for someone to pick from, and the emitted type union,
1627
- * which is exactly such a list.
1626
+ * this, and neither does the emitted ref-path union: a hidden token is still a legal target for a
1627
+ * `Token.ref` in another token's value, a style prop's `values`, a rule or a render read. What does
1628
+ * read it: a surface that lists values for someone to pick from — a picker, a catalogue, and the
1629
+ * emitted style-prop value unions, which are exactly such a list.
1628
1630
  *
1629
1631
  * True when the entity's own body says `hidden`, when any folder above it does, or when the pin a
1630
1632
  * borrowed row came through hides its path. A folder's flag reaches every member without the members
@@ -476,6 +476,48 @@ function renameKeyedByName(sub, def, from, to) {
476
476
  return out;
477
477
  }
478
478
  /**
479
+ * Follow a member's name into the lists that hold it — an anatomy node's `children` and each of its
480
+ * `slots` entries name sibling nodes by key. `to: undefined` drops the entry: a list naming a key no
481
+ * node has is a child nothing renders, and the node's own key rename would leave exactly that.
482
+ */
483
+ function renameListedNames(sub, def, from, to) {
484
+ if (!sub?.listedIn?.length) return def;
485
+ let out = def;
486
+ for (const field of sub.listedIn) {
487
+ const next = relistAt(out, field.split("/"), from, to);
488
+ if (isPlainObject(next)) out = next;
489
+ }
490
+ return out;
491
+ }
492
+ /**
493
+ * Rewrite `from` → `to` (or drop it, for `to: undefined`) in every string list `path` reaches; the
494
+ * path ends at the list, and a `*` segment stands for every key at that level. Rebuilds only the
495
+ * branches that changed, so an untouched owner keeps its reference.
496
+ */
497
+ function relistAt(node, path, from, to) {
498
+ const [head, ...rest] = path;
499
+ if (head === void 0) {
500
+ if (!Array.isArray(node) || !node.includes(from)) return node;
501
+ return to === void 0 ? node.filter((entry) => entry !== from) : node.map((entry) => entry === from ? to : entry);
502
+ }
503
+ if (!isPlainObject(node)) return node;
504
+ if (head === "*") {
505
+ let changed = false;
506
+ const next = {};
507
+ for (const [key, value] of Object.entries(node)) {
508
+ const rewritten = relistAt(value, rest, from, to);
509
+ if (rewritten !== value) changed = true;
510
+ next[key] = rewritten;
511
+ }
512
+ return changed ? next : node;
513
+ }
514
+ const rewritten = relistAt(node[head], rest, from, to);
515
+ return rewritten === node[head] ? node : {
516
+ ...node,
517
+ [head]: rewritten
518
+ };
519
+ }
520
+ /**
479
521
  * Rewrite `from` → `to` (or drop it, for `to: undefined`) in the record `path` addresses.
480
522
  *
481
523
  * A `*` segment stands for every key at that level, which is what reaches a nested bag: a layer's
@@ -1743,6 +1785,7 @@ var Config = class Config {
1743
1785
  def = rewriteRefs(def, `${refKind}:${refPathOf(patch, patch.key)}`, `${refKind}:${refPathOf(patch, patch.to)}`);
1744
1786
  def = renameNamedValues(sub, def, patch.owner, patch.key, patch.to);
1745
1787
  def = renameKeyedByName(sub, def, patch.key, patch.to);
1788
+ def = renameListedNames(sub, def, patch.key, patch.to);
1746
1789
  const namespaces = sub?.namespaces;
1747
1790
  if (namespaces) def = rewriteRefNamespace(def, namespaces, patch.key, patch.to);
1748
1791
  def = this.rekeyRecords(entry, def, patch.field, patch.key, patch.to);
@@ -1759,6 +1802,7 @@ var Config = class Config {
1759
1802
  def = store(def, collection);
1760
1803
  def = renameNamedValues(sub, def, patch.owner, patch.key, void 0);
1761
1804
  def = renameKeyedByName(sub, def, patch.key, void 0);
1805
+ def = renameListedNames(sub, def, patch.key, void 0);
1762
1806
  def = dropVacated(entry, sub, def);
1763
1807
  def = dropOrphans(sub, def, `${leafOf(patch.field).replace(/s$/, "")}:${refPathOf(patch, patch.key)}`, sub?.namespaces ? {
1764
1808
  kind: sub.namespaces,
@@ -2963,8 +3007,10 @@ var Config = class Config {
2963
3007
  *
2964
3008
  * An authoring question, not a resolution one. The entity is still here and still resolves, because
2965
3009
  * references already going through it have to keep working — so nothing that emits a *value* reads
2966
- * this. What does: a surface that lists paths for someone to pick from, and the emitted type union,
2967
- * which is exactly such a list.
3010
+ * this, and neither does the emitted ref-path union: a hidden token is still a legal target for a
3011
+ * `Token.ref` in another token's value, a style prop's `values`, a rule or a render read. What does
3012
+ * read it: a surface that lists values for someone to pick from — a picker, a catalogue, and the
3013
+ * emitted style-prop value unions, which are exactly such a list.
2968
3014
  *
2969
3015
  * True when the entity's own body says `hidden`, when any folder above it does, or when the pin a
2970
3016
  * borrowed row came through hides its path. A folder's flag reaches every member without the members
@@ -91,6 +91,13 @@ type SubEntityClass<F extends z.ZodTypeAny = z.ZodTypeAny> = CollectionSchema<F>
91
91
  * the renamed prop reports no default and the component reports one for a prop it hasn't got.
92
92
  */
93
93
  readonly keyedByNameIn?: readonly string[];
94
+ /**
95
+ * Fields of the OWNING ENTITY that LIST this member's name among others — an anatomy node's
96
+ * `children` and each of its `slots` entries name sibling nodes by key. A field is a `/` path;
97
+ * `*` stands for every key at that level, and the path ends at the list. A rename rewrites the
98
+ * entry and a delete drops it, since a list holding a key no node has is a child nothing renders.
99
+ */
100
+ readonly listedIn?: readonly string[];
94
101
  /**
95
102
  * Sibling collections whose members exist only to say something ABOUT one of these — a style
96
103
  * rule IS its condition, so a rule conditioned on a variant value that has been removed has
@@ -143,6 +150,7 @@ declare function defineSubEntity<F extends z.ZodTypeAny>(spec: {
143
150
  subEntities?: Readonly<Record<string, SubEntityClass>>;
144
151
  namedValuesIn?: readonly string[];
145
152
  keyedByNameIn?: readonly string[];
153
+ listedIn?: readonly string[];
146
154
  orphans?: readonly string[];
147
155
  }): SubEntityClass<F>;
148
156
  //#endregion
@@ -85,6 +85,7 @@ function defineSubEntity(spec) {
85
85
  subEntities: spec.subEntities,
86
86
  namedValuesIn: spec.namedValuesIn,
87
87
  keyedByNameIn: spec.keyedByNameIn,
88
+ listedIn: spec.listedIn,
88
89
  orphans: spec.orphans,
89
90
  ref: (kind, path, member) => memberRef(kind, path, spec.name, member),
90
91
  get: (config, owner, member, ownerMember) => {
@@ -1,6 +1,6 @@
1
1
  import { Ref } from "./utils/refs.js";
2
2
  import { ItemOf } from "./defineEntity.js";
3
- import { Config } from "./Config.js";
3
+ import { Config, ListOptions } from "./Config.js";
4
4
  import { StylePropLeaf, StyleProperty } from "../entities/system/StyleProperty.js";
5
5
  import { StoredPreviewCondition, UNCONDITIONAL_PREVIEW_KEY, storedPreview, unconditionalPreviewProps } from "./preview-store.js";
6
6
 
@@ -58,20 +58,28 @@ type StylePropEntry = StylePropLeaf;
58
58
  * signature that took it apart made every call site split one by hand, and the three that did
59
59
  * disagreed about whether the `@source` survived the trip — which decides which config the members
60
60
  * come from. */
61
- declare function memberLeaves(config: Config, target: string | Ref): string[];
61
+ declare function memberLeaves(config: Config, target: string | Ref, opts?: ListOptions): string[];
62
62
  /** Resolve a group-bound leaf back to its qualified path (`display2` → `textVariant/display2`).
63
63
  * `undefined` when the leaf isn't a member — what validation rejects. */
64
64
  declare function memberFromLeaf(config: Config, target: string | Ref, leaf: string): string | undefined;
65
65
  /** The refs of a domain written as a list of them, else `undefined`. A string list, a single ref
66
66
  * and a keyed collection all answer `undefined`, so this is the one test for the list form. */
67
67
  declare function refListOf(domain: unknown): string[] | undefined;
68
- declare function stylePropEntries(config: Config, path: string): StylePropEntry[];
68
+ /**
69
+ * Every value a style prop accepts, as `{ leaf, value }` pairs. Total by default: the stylesheet
70
+ * emits a utility per entry, and a value already bound to a retired token keeps resolving through
71
+ * it. `{ retired: false }` is what a surface offers for a new value — a picker, a catalogue, the
72
+ * emitted value union — and leaves out a token that is hidden or deprecated, itself or through a
73
+ * folder above it.
74
+ */
75
+ declare function stylePropEntries(config: Config, path: string, opts?: ListOptions): StylePropEntry[];
69
76
  /**
70
77
  * A style prop's leaf value domain — what a consumer/model actually authors (`warning`, `carbon/2`),
71
78
  * not the qualified storage path (`color/warning`). This is what the catalog, `uds_get_component`,
72
- * `uds_validate_props`, and the UI picker should offer, so the model emits `color="warning"`.
79
+ * `uds_validate_props`, and the UI picker should offer, so the model emits `color="warning"`; an
80
+ * offer reads it with `{ retired: false }`, as {@link stylePropEntries} describes.
73
81
  */
74
- declare function stylePropValueLeaves(config: Config, path: string): string[];
82
+ declare function stylePropValueLeaves(config: Config, path: string, opts?: ListOptions): string[];
75
83
  /**
76
84
  * Resolve a style-prop leaf value back to its qualified token path (or literal) — the inverse of
77
85
  * {@link stylePropValueLeaves}, for validating + rendering an authored `bg="subtle"`. `undefined`
@@ -27,6 +27,10 @@ import { isConditionalPreview, storedPreview, unconditionalPreviewProps } from "
27
27
  */
28
28
  const memberEntryCache = /* @__PURE__ */ new WeakMap();
29
29
  const stylePropEntryCache = /* @__PURE__ */ new WeakMap();
30
+ /** The cache key of a domain read: the target, and whether the read leaves out retired members. */
31
+ function domainKey(target, opts) {
32
+ return opts?.retired === false ? `${target}\u0000offered` : target;
33
+ }
30
34
  /** The per-config half of one of the caches above, created on first use. */
31
35
  function domainCache(cache, config) {
32
36
  let entry = cache.get(config);
@@ -103,18 +107,25 @@ function stylePropValues(config, path) {
103
107
  * weights from the library it renders are all the same question, so they are all this. A prop that
104
108
  * enumerates instead of delegating is a copy of a list nothing keeps in step — 1,530 glyph names in
105
109
  * the component that renders them, which is what the facet arm exists to stop.
110
+ *
111
+ * Total by default, the same way {@link Config.list} is: the stylesheet, the validator and the
112
+ * leaf↔path lookups all read it, and a value already bound to a retired member has to keep
113
+ * resolving. `{ retired: false }` is the offer surface's read — a picker, a catalogue, the emitted
114
+ * value union — and leaves out every member {@link Config.retired} answers true for, asked of the
115
+ * config that holds the member.
106
116
  */
107
- function memberEntries(config, target) {
117
+ function memberEntries(config, target, opts) {
108
118
  const cache = domainCache(memberEntryCache, config);
109
- const key = isRef(target) ? target.__ref : target;
119
+ const key = domainKey(isRef(target) ? target.__ref : target, opts);
110
120
  const hit = cache.get(key);
111
121
  if (hit) return hit;
112
- const expanded = expandMemberEntries(config, target);
122
+ const expanded = expandMemberEntries(config, target, opts);
113
123
  cache.set(key, expanded);
114
124
  return expanded;
115
125
  }
116
- function expandMemberEntries(config, target) {
126
+ function expandMemberEntries(config, target, opts) {
117
127
  const { kind, path, source, member: facet } = splitRef(target);
128
+ const offered = (holder, memberPath) => opts?.retired !== false || !holder.retired(kind, memberPath);
118
129
  /**
119
130
  * Whose members these are.
120
131
  *
@@ -128,7 +139,7 @@ function expandMemberEntries(config, target) {
128
139
  if (!owner) return [];
129
140
  /** The value side stays addressable from here, so a member's path keeps its qualifier. */
130
141
  const qualify = (memberPath) => source === void 0 ? memberPath : `${memberPath}@${source}`;
131
- if (facet === void 0 && path === "") return owner.list(kind).map((item) => ({
142
+ if (facet === void 0 && path === "") return owner.list(kind, opts).map((item) => ({
132
143
  leaf: item.path,
133
144
  path: qualify(item.path)
134
145
  }));
@@ -148,23 +159,26 @@ function expandMemberEntries(config, target) {
148
159
  path: value
149
160
  }));
150
161
  }
151
- if (owner.resolve(kind, path)) return [{
152
- leaf: path.slice(path.lastIndexOf("/") + 1),
153
- path: qualify(path)
154
- }];
162
+ if (owner.resolve(kind, path)) {
163
+ if (!offered(owner, path)) return [];
164
+ return [{
165
+ leaf: path.slice(path.lastIndexOf("/") + 1),
166
+ path: qualify(path)
167
+ }];
168
+ }
155
169
  const leafOf = (memberPath) => memberPath.startsWith(`${path}/`) ? memberPath.slice(path.length + 1) : memberPath;
156
170
  const theirs = owner.members(kind, path);
157
- if (source === void 0) return theirs.map((member) => ({
171
+ if (source === void 0) return theirs.filter((member) => offered(owner, member.path)).map((member) => ({
158
172
  leaf: leafOf(member.path),
159
173
  path: member.path
160
174
  }));
161
175
  const ours = config.members(kind, path);
162
176
  const held = new Set(ours.map((member) => member.path));
163
177
  const covered = new Set(theirs.map((member) => member.path));
164
- return [...theirs.map((member) => ({
178
+ return [...theirs.filter((member) => offered(held.has(member.path) ? config : owner, member.path)).map((member) => ({
165
179
  leaf: leafOf(member.path),
166
180
  path: held.has(member.path) ? member.path : qualify(member.path)
167
- })), ...ours.filter((member) => !covered.has(member.path)).map((member) => ({
181
+ })), ...ours.filter((member) => !covered.has(member.path) && offered(config, member.path)).map((member) => ({
168
182
  leaf: leafOf(member.path),
169
183
  path: member.path
170
184
  }))];
@@ -179,8 +193,8 @@ function groupMetaOf(config, kind, path) {
179
193
  * signature that took it apart made every call site split one by hand, and the three that did
180
194
  * disagreed about whether the `@source` survived the trip — which decides which config the members
181
195
  * come from. */
182
- function memberLeaves(config, target) {
183
- return memberEntries(config, target).map((entry) => entry.leaf);
196
+ function memberLeaves(config, target, opts) {
197
+ return memberEntries(config, target, opts).map((entry) => entry.leaf);
184
198
  }
185
199
  /** Resolve a group-bound leaf back to its qualified path (`display2` → `textVariant/display2`).
186
200
  * `undefined` when the leaf isn't a member — what validation rejects. */
@@ -193,28 +207,37 @@ function refListOf(domain) {
193
207
  if (!Array.isArray(domain) || domain.length === 0 || !domain.every(isRef)) return;
194
208
  return domain.map((entry) => entry.__ref);
195
209
  }
196
- function stylePropEntries(config, path) {
210
+ /**
211
+ * Every value a style prop accepts, as `{ leaf, value }` pairs. Total by default: the stylesheet
212
+ * emits a utility per entry, and a value already bound to a retired token keeps resolving through
213
+ * it. `{ retired: false }` is what a surface offers for a new value — a picker, a catalogue, the
214
+ * emitted value union — and leaves out a token that is hidden or deprecated, itself or through a
215
+ * folder above it.
216
+ */
217
+ function stylePropEntries(config, path, opts) {
197
218
  const cache = domainCache(stylePropEntryCache, config);
198
- const hit = cache.get(path);
219
+ const key = domainKey(path, opts);
220
+ const hit = cache.get(key);
199
221
  if (hit) return hit;
200
- const expanded = expandStylePropEntries(config, path);
201
- cache.set(path, expanded);
222
+ const expanded = expandStylePropEntries(config, path, opts);
223
+ cache.set(key, expanded);
202
224
  return expanded;
203
225
  }
204
- function expandStylePropEntries(config, path) {
226
+ function expandStylePropEntries(config, path, opts) {
205
227
  const prop = config.resolve("style-property", path);
206
228
  if (!prop) return [];
207
229
  const values = prop.toJSON().values ?? [];
208
- const members = (target) => memberEntries(config, target);
230
+ const members = (target) => memberEntries(config, target, opts);
209
231
  return values.flatMap((entry) => leavesOfStylePropValue(entry, members));
210
232
  }
211
233
  /**
212
234
  * A style prop's leaf value domain — what a consumer/model actually authors (`warning`, `carbon/2`),
213
235
  * not the qualified storage path (`color/warning`). This is what the catalog, `uds_get_component`,
214
- * `uds_validate_props`, and the UI picker should offer, so the model emits `color="warning"`.
236
+ * `uds_validate_props`, and the UI picker should offer, so the model emits `color="warning"`; an
237
+ * offer reads it with `{ retired: false }`, as {@link stylePropEntries} describes.
215
238
  */
216
- function stylePropValueLeaves(config, path) {
217
- return stylePropEntries(config, path).map((entry) => entry.leaf);
239
+ function stylePropValueLeaves(config, path, opts) {
240
+ return stylePropEntries(config, path, opts).map((entry) => entry.leaf);
218
241
  }
219
242
  /**
220
243
  * Resolve a style-prop leaf value back to its qualified token path (or literal) — the inverse of
@@ -1171,7 +1194,7 @@ function previewMatrix(config, path) {
1171
1194
  for (const { when } of previewConditions(config, path)) for (const [prop, stored] of Object.entries(when ?? {})) nameAxisPoint(prop, stored);
1172
1195
  for (const [name, decl] of Object.entries(declared)) {
1173
1196
  if (decl?.type === "composite" && isRef(decl.value)) {
1174
- const values = declaredFirst([...memberLeaves(config, decl.value)], props[name] ?? []);
1197
+ const values = declaredFirst([...memberLeaves(config, decl.value, { retired: false })], props[name] ?? []);
1175
1198
  if (values.length > 0) props[name] = values;
1176
1199
  }
1177
1200
  if (decl?.type === "boolean" && props[name]) props[name] = declaredFirst(["false", "true"], props[name] ?? []);