@yahoo/uds-create-config 2.20.0 → 2.21.1

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.
@@ -320,5 +320,11 @@ interface StylePropKeyword {
320
320
  readonly styleProp: string;
321
321
  readonly value: string;
322
322
  }
323
+ /**
324
+ * HTML void elements — tags that must never receive children (React throws).
325
+ * Mirrors the HTML spec's void-element list; codegen's registry descriptor
326
+ * derives its `voidComponents` set from the same tags.
327
+ */
328
+ declare const VOID_HTML_TAGS: ReadonlySet<string>;
323
329
  //#endregion
324
- export { Component, ComponentKind, CompositeInfo, FunctionInfo, Layer, PrimitivePropKind, PropBase, PropInfo, PropKind, ResolvedCompoundOverrides, SlotInfo, SlotValueKind, StylePropInfo, StylePropKeyword, StylePropTokenUsage, VariantInfo };
330
+ export { Component, ComponentKind, CompositeInfo, FunctionInfo, Layer, PrimitivePropKind, PropBase, PropInfo, PropKind, ResolvedCompoundOverrides, SlotInfo, SlotValueKind, StylePropInfo, StylePropKeyword, StylePropTokenUsage, VOID_HTML_TAGS, VariantInfo };
package/dist/Component.js CHANGED
@@ -285,6 +285,7 @@ var Component = class {
285
285
  propsIndex.set(propName, info);
286
286
  }
287
287
  const styleUsage = collectStyleUsage(this.base, this.props, this.compoundProps, this.#resolveStylePropValueToken, this.#isStyleProp);
288
+ const refusesChildren = this.#rootRefusesChildren();
288
289
  this.#derived = {
289
290
  layers: Array.from(this.layers.values()),
290
291
  classNames: componentClassNames(this.#prefixGetter?.() ?? "", this.name),
@@ -300,8 +301,8 @@ var Component = class {
300
301
  stylePropTokenUsages: styleUsage.stylePropTokenUsages,
301
302
  stylePropKeywords: styleUsage.stylePropKeywords,
302
303
  kind: this.#deriveKind(),
303
- acceptsChildren: derivesAcceptsChildren(this.layers, propsArray),
304
- acceptsChildNodes: derivesAcceptsChildNodes(this.layers, propsArray),
304
+ acceptsChildren: !refusesChildren && derivesAcceptsChildren(this.layers, propsArray),
305
+ acceptsChildNodes: !refusesChildren && derivesAcceptsChildNodes(this.layers, propsArray),
305
306
  prop(propName) {
306
307
  return propsIndex.get(propName);
307
308
  }
@@ -309,6 +310,36 @@ var Component = class {
309
310
  }
310
311
  return this.#derived;
311
312
  }
313
+ /**
314
+ * Whether this component's root element can never hold children, making
315
+ * `acceptsChildren`/`acceptsChildNodes` false regardless of the structural
316
+ * signals those derivations read. Two authored facts qualify (UDS-3068):
317
+ *
318
+ * - **A void root tag.** A single-primitive-root component whose effective
319
+ * tag (own `defaultProps.as`, else the root layer's tag, else the
320
+ * value-extends chain's) is a void HTML element (`input`, `img`, …) —
321
+ * React throws if such an element receives children, so treating it as a
322
+ * container injects drop-target Slots that crash every spec render of it.
323
+ * Gated on the single-primitive-root shape: a composed component's render
324
+ * decides where children go, so its root tag says nothing about them.
325
+ *
326
+ * - **An explicitly dropped `children` prop.** The authored
327
+ * `children: undefined` (the documented pattern for void/value elements —
328
+ * Input, Image, Textarea) is detected two ways: authored configs keep the
329
+ * key with an `undefined` value, which IS the drop; hydrated configs
330
+ * compact the key away, so there it's absence-against-ancestry (the
331
+ * merged props lack `children` while an ancestor declares it — only a
332
+ * drop can produce that, since value-extension inherits the parent's
333
+ * full prop surface). This catches `textarea`, which isn't HTML-void yet
334
+ * still rejects element children in React.
335
+ */
336
+ #rootRefusesChildren() {
337
+ if (hasSinglePrimitiveRoot(this.layers)) {
338
+ const tag = resolveRootHtmlTag(this, this.#componentResolver, /* @__PURE__ */ new Set());
339
+ if (tag !== void 0 && VOID_HTML_TAGS.has(tag)) return true;
340
+ }
341
+ return droppedInheritedChildren(this, this.#componentResolver);
342
+ }
312
343
  /** Classify authoring shape: `extends` wins (value-extension is the defining
313
344
  * trait), then anything with multiple layers or a custom render is
314
345
  * `multi-layer`, else a bare single-tag `primitive`. */
@@ -880,6 +911,76 @@ function hasSinglePrimitiveRoot(layers) {
880
911
  return false;
881
912
  }
882
913
  /**
914
+ * HTML void elements — tags that must never receive children (React throws).
915
+ * Mirrors the HTML spec's void-element list; codegen's registry descriptor
916
+ * derives its `voidComponents` set from the same tags.
917
+ */
918
+ const VOID_HTML_TAGS = new Set([
919
+ "area",
920
+ "base",
921
+ "br",
922
+ "col",
923
+ "embed",
924
+ "hr",
925
+ "img",
926
+ "input",
927
+ "link",
928
+ "meta",
929
+ "param",
930
+ "source",
931
+ "track",
932
+ "wbr"
933
+ ]);
934
+ /**
935
+ * The HTML tag a component's root element renders as: own `defaultProps.as`
936
+ * wins (the `Box as="input"` value-extension pattern), else the root layer's
937
+ * tag, else the value-extends chain's answer. Undefined when nothing resolves
938
+ * (composed roots, unknown parents) — callers treat that as "no tag fact".
939
+ * The `seen` guard stops extends cycles.
940
+ *
941
+ * Mirrors codegen's `resolveHtmlTag` (getReactArtifacts.ts), which reads the
942
+ * same facts off `derived.layers[*].renders` — keep the precedence order
943
+ * (`as` → root tag → extends chain) in sync.
944
+ */
945
+ function resolveRootHtmlTag(component, resolve, seen) {
946
+ if (seen.has(component.name)) return void 0;
947
+ seen.add(component.name);
948
+ const as = component.defaultProps?.as;
949
+ if (typeof as === "string") return as;
950
+ const t = component.layers.get("root")?.tag;
951
+ if (typeof t === "string") return t;
952
+ if (typeof t === "object" && t !== null) {
953
+ const marker = t;
954
+ if (marker.__kind === "tag" && typeof marker.ref === "string") return marker.ref;
955
+ }
956
+ const parent = component.extendsFrom && resolve ? resolve(component.extendsFrom) : void 0;
957
+ return parent ? resolveRootHtmlTag(parent, resolve, seen) : void 0;
958
+ }
959
+ /**
960
+ * Whether the author dropped an inherited `children` prop (`children:
961
+ * undefined` in a value-extension — Input/Image/Textarea dropping Box's
962
+ * slot). Two-tier detection: authored configs keep the dropped key with an
963
+ * `undefined` value (the direct trace); hydrated configs compact the key
964
+ * away, leaving absence-against-ancestry — this component's merged props
965
+ * lack `children` while some ancestor declares it. A component that never
966
+ * had a `children` prop anywhere in its chain isn't a drop — it just
967
+ * doesn't participate.
968
+ */
969
+ function droppedInheritedChildren(component, resolve) {
970
+ if (Object.hasOwn(component.props, "children")) return component.props.children === void 0;
971
+ if (!resolve) return false;
972
+ const seen = new Set([component.name]);
973
+ let parentName = component.extendsFrom;
974
+ while (parentName && !seen.has(parentName)) {
975
+ seen.add(parentName);
976
+ const parent = resolve(parentName);
977
+ if (!parent) return false;
978
+ if (parent.props.children !== void 0) return true;
979
+ parentName = parent.extendsFrom;
980
+ }
981
+ return false;
982
+ }
983
+ /**
883
984
  * `acceptsChildren` is true when either:
884
985
  * - the component has a single primitive root layer, OR
885
986
  * - the component declares a `children` slot prop (`children: slot(...)`).
@@ -938,4 +1039,4 @@ function buildCompoundOverrides(entries) {
938
1039
  return out;
939
1040
  }
940
1041
  //#endregion
941
- export { Component, Layer, assertDefinitionAcceptsPlaceable };
1042
+ export { Component, Layer, VOID_HTML_TAGS, assertDefinitionAcceptsPlaceable };
package/dist/Config.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { CssVarRef } from "./types/css-values.js";
2
- import { AnyStylePropDefinition, BuildOptions, CanvasRole, CanvasRoleEntry, CanvasRolesDefinitionInput, CanvasRolesInputConstraint, ComponentDefinition, ComponentGroupDefinition, CompositeStyleDefinition, GlobalStylesDef, LinkedComponentsInput, LinkedSystemEntry, ModeDefinition, ModifierDefinition, MotionDefinitionInput, PlaygroundOptions, SerializedConfig, TokenGroupDefinition, TokenModifierKey } from "./types.js";
2
+ import { AnyStylePropDefinition, BuildOptions, CanvasRole, CanvasRoleEntry, CanvasRolesDefinitionInput, CanvasRolesInputConstraint, ComponentDefinition, ComponentGroupDefinition, CompositeStyleDefinition, GlobalStylesDef, LinkedSystemPin, LinkedSystemsInput, ModeDefinition, ModifierDefinition, MotionDefinitionInput, PlaygroundOptions, SerializedConfig, SystemLink, TokenGroupDefinition, TokenModifierKey } from "./types.js";
3
3
  import { AssetGroupDefinition } from "./defineAssetGroup.js";
4
4
  import { AssetGroup } from "./AssetGroup.js";
5
5
  import { cssVar } from "./brands.js";
@@ -94,11 +94,12 @@ declare class Config {
94
94
  readonly motion: Map<string, MotionDef>;
95
95
  readonly canvasRoles: Map<CanvasRole, readonly CanvasRoleEntry[]>;
96
96
  /**
97
- * Components linked from other systems via {@link registerLinkedComponents},
98
- * keyed by the source system's database UUID. Declaration only — resolution
99
- * (materialized sources, rendering) is a separate, later concern.
97
+ * Source systems pinned via {@link registerLinkedSystems}, keyed by the
98
+ * source system's database UUID. A pin anchors every linked value from that
99
+ * system to one exact published version — registration validates each
100
+ * link-marked value against it.
100
101
  */
101
- readonly linkedComponents: Map<string, LinkedSystemEntry>;
102
+ readonly linkedSystems: Map<string, LinkedSystemPin>;
102
103
  /**
103
104
  * Set top-level config metadata in one call — the single scalar-setter,
104
105
  * replacing the former `withPrefix` / `withPreflight` / `withName` /
@@ -149,20 +150,6 @@ declare class Config {
149
150
  * `config.components.get(type)` when `type` may be a spec/registry key.
150
151
  */
151
152
  getComponent(type: string): Component | undefined;
152
- /**
153
- * Identify an id-qualified linked-component reference
154
- * (`'<systemId>:<Name>'`, e.g. `'96d1678f-…:Button'`). Returns the parsed
155
- * `{ systemId, name }` when the system id is a registered
156
- * {@link linkedComponents} entry AND the bare name is declared in its
157
- * `components` list; `undefined` otherwise (including bare names and
158
- * namespaced registry keys). This is declaration-lookup only — it never
159
- * returns a renderable {@link Component}; resolution is deferred to the
160
- * materialization/rendering work.
161
- */
162
- getLinkedComponentRef(type: string): {
163
- systemId: string;
164
- name: string;
165
- } | undefined;
166
153
  /**
167
154
  * The bare, human-readable component name for a spec `type` — strips this
168
155
  * registry's `<namespace>:` prefix (`'uds:Text'` → `'Text'`) for display, and
@@ -331,18 +318,19 @@ declare class Config {
331
318
  registerComposites(composites: Record<string, CompositeStyleDefinition>): this;
332
319
  registerMotion(motion: Record<string, MotionDefinitionInput>): this;
333
320
  /**
334
- * Declare components linked from other systems. Each record key is the
335
- * source system's database UUID (the identity — globally unique, immutable,
336
- * rename-proof); the entry carries the system's display-name snapshot at
337
- * link time, an exact published version pin, and the bare names of the
338
- * linked components.
321
+ * Pin the source systems this config links values from. Each record key is
322
+ * the source system's database UUID (the identity — globally unique,
323
+ * immutable, rename-proof); the pin carries the system's display-name
324
+ * snapshot at link time and an exact published version.
339
325
  *
340
- * This is a data-only declaration: no imported component values, no
341
- * layer/prop specs. A linked component's qualified reference is
342
- * `<systemId>:<Name>` (see {@link getLinkedComponentRef}); resolution to a
343
- * renderable component is deferred to the materialization/rendering work.
326
+ * One pin per system, one version per pin. Every linked value registered
327
+ * afterwards (a barrel value carrying a `link` marker) is validated against
328
+ * its pin — an unpinned system or a version mismatch throws at
329
+ * registration, so pins must appear before the `register*` calls that use
330
+ * them. The pin's `name` also feeds the registry folder slug the pull
331
+ * pipeline derives.
344
332
  */
345
- registerLinkedComponents(input: LinkedComponentsInput): this;
333
+ registerLinkedSystems(input: LinkedSystemsInput): this;
346
334
  registerComponents(components: Record<string, ComponentDefinition>): this;
347
335
  /**
348
336
  * Build a single component from its definition with this config's context
@@ -437,6 +425,13 @@ declare class Config {
437
425
  registerProviders(providers: Record<string, ProviderComponent<Record<never, never>>>): this;
438
426
  get derived(): {
439
427
  readonly tokens: ReadonlyMap<string, Token>;
428
+ /**
429
+ * Provenance index for linked tokens — `"<systemId>:<sourceKey>"` →
430
+ * token. A `token()` ref authored inside a linked token names SOURCE-
431
+ * system groups, so it resolves through this index (rename-proof: the
432
+ * consumer's local group name never appears in the key).
433
+ */
434
+ readonly tokensBySource: ReadonlyMap<string, Token>;
440
435
  readonly cssVarRefs: Readonly<Record<string, Readonly<Record<string, CssVarRef>>>>;
441
436
  /**
442
437
  * Reverse lookup: a style modifier key (`_dark`) → the mode option that
@@ -446,6 +441,16 @@ declare class Config {
446
441
  */
447
442
  readonly modeOptionsByModifier: ReadonlyMap<TokenModifierKey, ModeOption>;
448
443
  };
444
+ /**
445
+ * Resolve a `token()` ref, provenance-aware — the ONE lookup every ref
446
+ * consumer (registration wiring, `validate()`, codegen emission) should
447
+ * use. A ref authored on a linked token (`context` set) names the SOURCE
448
+ * system's groups, so it resolves through the provenance index only —
449
+ * falling back to the consumer's own keys would let a coincidentally-named
450
+ * local group silently shadow a missing source group. Local refs resolve
451
+ * against consumer keys.
452
+ */
453
+ resolveTokenRef(ref: string, context?: SystemLink): Token | undefined;
449
454
  /**
450
455
  * Validate cross-references. Throws on cycles in token aliases or on
451
456
  * unknown refs in markers (`token()`, `composite()`, `mode()`,
package/dist/Config.js CHANGED
@@ -65,11 +65,12 @@ var Config = class Config {
65
65
  motion = /* @__PURE__ */ new Map();
66
66
  canvasRoles = /* @__PURE__ */ new Map();
67
67
  /**
68
- * Components linked from other systems via {@link registerLinkedComponents},
69
- * keyed by the source system's database UUID. Declaration only — resolution
70
- * (materialized sources, rendering) is a separate, later concern.
68
+ * Source systems pinned via {@link registerLinkedSystems}, keyed by the
69
+ * source system's database UUID. A pin anchors every linked value from that
70
+ * system to one exact published version — registration validates each
71
+ * link-marked value against it.
71
72
  */
72
- linkedComponents = /* @__PURE__ */ new Map();
73
+ linkedSystems = /* @__PURE__ */ new Map();
73
74
  #derived;
74
75
  /**
75
76
  * Set top-level config metadata in one call — the single scalar-setter,
@@ -134,27 +135,6 @@ var Config = class Config {
134
135
  if (namespace !== void 0 && namespace === this.namespace) return this.components.get(name);
135
136
  }
136
137
  /**
137
- * Identify an id-qualified linked-component reference
138
- * (`'<systemId>:<Name>'`, e.g. `'96d1678f-…:Button'`). Returns the parsed
139
- * `{ systemId, name }` when the system id is a registered
140
- * {@link linkedComponents} entry AND the bare name is declared in its
141
- * `components` list; `undefined` otherwise (including bare names and
142
- * namespaced registry keys). This is declaration-lookup only — it never
143
- * returns a renderable {@link Component}; resolution is deferred to the
144
- * materialization/rendering work.
145
- */
146
- getLinkedComponentRef(type) {
147
- const { namespace: systemId, name } = parseRegistryKey(type);
148
- if (systemId === void 0) return void 0;
149
- const entry = this.linkedComponents.get(systemId);
150
- if (!entry) return void 0;
151
- if (entry.components !== "all" && !entry.components.includes(name)) return;
152
- return {
153
- systemId,
154
- name
155
- };
156
- }
157
- /**
158
138
  * The bare, human-readable component name for a spec `type` — strips this
159
139
  * registry's `<namespace>:` prefix (`'uds:Text'` → `'Text'`) for display, and
160
140
  * the `foreign:` sentinel third-party leaves carry (`'foreign:CopyIcon'` →
@@ -900,13 +880,16 @@ var Config = class Config {
900
880
  return this;
901
881
  }
902
882
  registerTokenGroups(groups) {
903
- for (const [namespace, def] of Object.entries(groups)) this.tokenGroups.set(namespace, new TokenGroup({
904
- namespace,
905
- prefixGetter: () => this.prefix,
906
- definition: def,
907
- tokenLookup: (ref) => this.derived.tokens.get(ref),
908
- stylePropertiesLookup: (ns) => this.#stylePropertiesFor(ns)
909
- }));
883
+ for (const [namespace, def] of Object.entries(groups)) {
884
+ for (const [tokenName, tokenDef] of Object.entries(def.tokens)) this.#assertLinkedTokenPinned(namespace, tokenName, tokenDef.link);
885
+ this.tokenGroups.set(namespace, new TokenGroup({
886
+ namespace,
887
+ prefixGetter: () => this.prefix,
888
+ definition: def,
889
+ tokenLookup: (ref, context) => this.resolveTokenRef(ref, context),
890
+ stylePropertiesLookup: (ns) => this.#stylePropertiesFor(ns)
891
+ }));
892
+ }
910
893
  this.#invalidate();
911
894
  return this;
912
895
  }
@@ -938,24 +921,24 @@ var Config = class Config {
938
921
  return this;
939
922
  }
940
923
  /**
941
- * Declare components linked from other systems. Each record key is the
942
- * source system's database UUID (the identity — globally unique, immutable,
943
- * rename-proof); the entry carries the system's display-name snapshot at
944
- * link time, an exact published version pin, and the bare names of the
945
- * linked components.
924
+ * Pin the source systems this config links values from. Each record key is
925
+ * the source system's database UUID (the identity — globally unique,
926
+ * immutable, rename-proof); the pin carries the system's display-name
927
+ * snapshot at link time and an exact published version.
946
928
  *
947
- * This is a data-only declaration: no imported component values, no
948
- * layer/prop specs. A linked component's qualified reference is
949
- * `<systemId>:<Name>` (see {@link getLinkedComponentRef}); resolution to a
950
- * renderable component is deferred to the materialization/rendering work.
929
+ * One pin per system, one version per pin. Every linked value registered
930
+ * afterwards (a barrel value carrying a `link` marker) is validated against
931
+ * its pin — an unpinned system or a version mismatch throws at
932
+ * registration, so pins must appear before the `register*` calls that use
933
+ * them. The pin's `name` also feeds the registry folder slug the pull
934
+ * pipeline derives.
951
935
  */
952
- registerLinkedComponents(input) {
953
- for (const [systemId, entry] of Object.entries(input)) {
954
- validateLinkedSystemEntry(systemId, entry, this.linkedComponents);
955
- this.linkedComponents.set(systemId, {
956
- name: entry.name,
957
- version: entry.version,
958
- components: entry.components === "all" ? "all" : [...entry.components]
936
+ registerLinkedSystems(input) {
937
+ for (const [systemId, pin] of Object.entries(input)) {
938
+ validateLinkedSystemPin(systemId, pin, this.linkedSystems);
939
+ this.linkedSystems.set(systemId, {
940
+ name: pin.name,
941
+ version: pin.version
959
942
  });
960
943
  }
961
944
  return this;
@@ -1254,11 +1237,13 @@ var Config = class Config {
1254
1237
  get derived() {
1255
1238
  if (!this.#derived) {
1256
1239
  const tokens = /* @__PURE__ */ new Map();
1240
+ const tokensBySource = /* @__PURE__ */ new Map();
1257
1241
  const cssVarRefs = {};
1258
1242
  for (const [namespace, group] of this.tokenGroups) {
1259
1243
  const groupRefs = {};
1260
1244
  for (const [name, token] of group.tokens) {
1261
1245
  tokens.set(`${namespace}/${name}`, token);
1246
+ if (token.link) tokensBySource.set(`${token.link.systemId}:${token.link.sourceKey}`, token);
1262
1247
  groupRefs[name] = token.derived.cssVarRef;
1263
1248
  }
1264
1249
  cssVarRefs[namespace] = groupRefs;
@@ -1272,12 +1257,39 @@ var Config = class Config {
1272
1257
  }
1273
1258
  this.#derived = {
1274
1259
  tokens,
1260
+ tokensBySource,
1275
1261
  cssVarRefs,
1276
1262
  modeOptionsByModifier
1277
1263
  };
1278
1264
  }
1279
1265
  return this.#derived;
1280
1266
  }
1267
+ /**
1268
+ * Resolve a `token()` ref, provenance-aware — the ONE lookup every ref
1269
+ * consumer (registration wiring, `validate()`, codegen emission) should
1270
+ * use. A ref authored on a linked token (`context` set) names the SOURCE
1271
+ * system's groups, so it resolves through the provenance index only —
1272
+ * falling back to the consumer's own keys would let a coincidentally-named
1273
+ * local group silently shadow a missing source group. Local refs resolve
1274
+ * against consumer keys.
1275
+ */
1276
+ resolveTokenRef(ref, context) {
1277
+ if (context) return this.derived.tokensBySource.get(`${context.systemId}:${ref}`);
1278
+ return this.derived.tokens.get(ref);
1279
+ }
1280
+ /**
1281
+ * Pin-consistency gate for a link-marked token entering the registry.
1282
+ * Catches hand-forged markers (unpinned system), stale registry barrels
1283
+ * (version mismatch), and mis-kinded markers before they poison lookups.
1284
+ */
1285
+ #assertLinkedTokenPinned(groupName, tokenName, link) {
1286
+ if (!link) return;
1287
+ const label = `Config.registerTokenGroups: token "${groupName}/${tokenName}"`;
1288
+ if (link.kind !== "token") throw new Error(`${label} carries a "${link.kind}" link marker — a token entry can only carry kind "token" (a token-group link spreads to per-token markers).`);
1289
+ const pin = this.linkedSystems.get(link.systemId);
1290
+ if (!pin) throw new Error(`${label} is linked from system "${link.systemId}", which has no registerLinkedSystems pin — pin the system (name + exact version) before registering values linked from it.`);
1291
+ if (pin.version !== link.systemVersion) throw new Error(`${label} is linked at version "${link.systemVersion}" but system "${pin.name}" is pinned at "${pin.version}" — the registry barrel is stale; re-run \`uds pull\`.`);
1292
+ }
1281
1293
  #invalidate() {
1282
1294
  this.#derived = void 0;
1283
1295
  }
@@ -1334,7 +1346,7 @@ var Config = class Config {
1334
1346
  */
1335
1347
  validate() {
1336
1348
  const tokens = this.derived.tokens;
1337
- for (const [qualified, token] of tokens) visit(qualified, token, tokens, /* @__PURE__ */ new Set());
1349
+ for (const [qualified, token] of tokens) visit(qualified, token, (ref, context) => this.resolveTokenRef(ref, context), /* @__PURE__ */ new Set());
1338
1350
  for (const styleProp of this.styleProps.values()) for (const groupName of styleProp.consumedTokenGroups()) if (!this.tokenGroups.has(groupName)) throw new Error(`Style prop '${styleProp.name}' references unknown token group '${groupName}'`);
1339
1351
  for (const modifier of this.modifiers.values()) {
1340
1352
  const selector = modifier.selector;
@@ -1422,10 +1434,7 @@ var Config = class Config {
1422
1434
  out.canvasRoles = canvasRoles;
1423
1435
  }
1424
1436
  if (this.playgroundOptions.port !== void 0) out.playgroundOptions = { port: this.playgroundOptions.port };
1425
- if (this.linkedComponents.size > 0) out.linkedComponents = mapToRecord(this.linkedComponents, (entry) => ({
1426
- ...entry,
1427
- components: entry.components === "all" ? "all" : [...entry.components]
1428
- }));
1437
+ if (this.linkedSystems.size > 0) out.linkedSystems = mapToRecord(this.linkedSystems, (pin) => ({ ...pin }));
1429
1438
  return out;
1430
1439
  }
1431
1440
  /**
@@ -1444,12 +1453,12 @@ var Config = class Config {
1444
1453
  if (json.globalStyles) config.globalStyles = json.globalStyles;
1445
1454
  if (json.rawCss) config.rawCss = [...json.rawCss];
1446
1455
  if (json.modes) config.registerModes(json.modes);
1456
+ if (json.linkedSystems) config.registerLinkedSystems(json.linkedSystems);
1447
1457
  if (json.tokenGroups) config.registerTokenGroups(json.tokenGroups);
1448
1458
  if (json.styleProps) config.registerStyleProps(json.styleProps);
1449
1459
  if (json.modifiers) config.registerModifiers(json.modifiers);
1450
1460
  if (json.compositeStyles) config.registerComposites(json.compositeStyles);
1451
1461
  if (json.motion) config.registerMotion(json.motion);
1452
- if (json.linkedComponents) config.registerLinkedComponents(json.linkedComponents);
1453
1462
  const componentsByName = json.components ?? {};
1454
1463
  for (const def of Object.values(componentsByName)) if (def.sourceFilePath) attachSourcePath(def, def.sourceFilePath);
1455
1464
  const isSubcomponent = /* @__PURE__ */ new Set();
@@ -1569,20 +1578,15 @@ function parseRegistryKey(type) {
1569
1578
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1570
1579
  /** Exact `major.minor.patch` with optional prerelease/build — no ranges. */
1571
1580
  const EXACT_SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
1572
- function validateLinkedSystemEntry(systemId, entry, existing) {
1573
- const label = `Config.registerLinkedComponents: system id "${systemId}"`;
1574
- if (!UUID_RE.test(systemId)) throw new Error(`${label} must be a UUID — links are keyed by the source system's database id, not its slug or name.`);
1575
- if (existing.has(systemId)) throw new Error(`${label} is already linked ("${existing.get(systemId)?.name}") — update or remove the existing entry instead of re-linking.`);
1576
- if (entry.name.trim().length === 0) throw new Error(`${label}: name must be non-empty — it's the system's display-name snapshot at link time.`);
1577
- if (!EXACT_SEMVER_RE.test(entry.version)) throw new Error(`${label}: version "${entry.version}" must be an exact semver literal (e.g. "1.3.1") — ranges and tags are not supported.`);
1578
- if (entry.components === "all") return;
1579
- if (entry.components.length === 0) throw new Error(`${label}: at least one component name is required (or pass 'all' to link every component).`);
1580
- const seen = /* @__PURE__ */ new Set();
1581
- for (const name of entry.components) {
1582
- if (name.trim().length === 0) throw new Error(`${label}: component names must be non-empty strings.`);
1583
- if (seen.has(name)) throw new Error(`${label}: duplicate component "${name}".`);
1584
- seen.add(name);
1585
- }
1581
+ function validateLinkedSystemPin(systemId, pin, existing) {
1582
+ const label = `Config.registerLinkedSystems: system id "${systemId}"`;
1583
+ if (!UUID_RE.test(systemId)) throw new Error(`${label} must be a UUID — pins are keyed by the source system's database id, not its slug or name.`);
1584
+ if (existing.has(systemId)) {
1585
+ const prior = existing.get(systemId);
1586
+ throw new Error(`${label} is already pinned ("${prior?.name}" @ ${prior?.version}) — a config pins one version per system; update the existing pin instead of re-pinning.`);
1587
+ }
1588
+ if (pin.name.trim().length === 0) throw new Error(`${label}: name must be non-empty — it's the system's display-name snapshot at link time and feeds the registry folder slug.`);
1589
+ if (!EXACT_SEMVER_RE.test(pin.version)) throw new Error(`${label}: version "${pin.version}" must be an exact semver literal (e.g. "1.3.1") — ranges and tags are not supported.`);
1586
1590
  }
1587
1591
  function validateNamespace(value) {
1588
1592
  if (value.length === 0) throw new Error("Config.configure: namespace must be non-empty.");
@@ -1591,15 +1595,39 @@ function validateNamespace(value) {
1591
1595
  if (value === "foreign") throw new Error(`Config.configure: namespace "${FOREIGN_NAMESPACE}" is reserved for third-party leaf components (the \`${FOREIGN_NAMESPACE}:<Component>\` sentinel).`);
1592
1596
  return value;
1593
1597
  }
1594
- function visit(qualified, token, tokens, seen) {
1598
+ /**
1599
+ * Cross-reference walk for one token's alias chain, provenance-aware.
1600
+ * `resolve` is the config's `resolveTokenRef` — a ref on a linked token
1601
+ * resolves through the provenance index (source-system keys), a local ref
1602
+ * through consumer keys. A dangling ref on a linked token is a closure
1603
+ * violation (the source group it names was never linked), which gets its own
1604
+ * actionable error; a dangling local ref stays the classic unknown-token
1605
+ * error. Modifier refs are checked for linked tokens only — the closure rule
1606
+ * is new with linking, and local modifier behavior is unchanged.
1607
+ */
1608
+ function visit(qualified, token, resolve, seen) {
1595
1609
  if (seen.has(qualified)) throw new Error(`Token cycle detected at '${qualified}'`);
1596
- const value = token.value;
1597
- if (typeof value === "object" && value !== null && "ref" in value && typeof value.ref === "string") {
1598
- const next = tokens.get(value.ref);
1599
- if (!next) throw new Error(`Token '${qualified}' references unknown token '${value.ref}'`);
1600
- visit(value.ref, next, tokens, new Set(seen).add(qualified));
1610
+ const followRef = (ref) => {
1611
+ const next = resolve(ref, token.link);
1612
+ if (!next) {
1613
+ if (token.link) {
1614
+ const [sourceGroup] = ref.split("/");
1615
+ throw new Error(`Token '${qualified}' is linked from system "${token.link.systemId}" and references its token '${ref}', which nothing in this config provides — link the source system's "${sourceGroup}" group too (refs on linked tokens resolve by provenance, not by local group names).`);
1616
+ }
1617
+ throw new Error(`Token '${qualified}' references unknown token '${ref}'`);
1618
+ }
1619
+ visit(`${next.namespace}/${next.name}`, next, resolve, new Set(seen).add(qualified));
1620
+ };
1621
+ if (isTokenRefValue(token.value)) followRef(token.value.ref);
1622
+ if (token.link && token.modifiers) for (const value of Object.values(token.modifiers)) {
1623
+ const inner = typeof value === "object" && value !== null && "value" in value ? value.value : value;
1624
+ if (isTokenRefValue(inner)) followRef(inner.ref);
1601
1625
  }
1602
1626
  }
1627
+ /** A `token()` ref marker — `{ ref: 'ns/name' }` — in a value position. */
1628
+ function isTokenRefValue(value) {
1629
+ return typeof value === "object" && value !== null && "ref" in value && typeof value.ref === "string";
1630
+ }
1603
1631
  function stripName(entity) {
1604
1632
  const { name: _name, namespace: _namespace, ...rest } = entity.toJSON();
1605
1633
  return rest;
package/dist/Token.d.ts CHANGED
@@ -12,8 +12,15 @@ type ResolvedTokenLiteral = string | number | boolean | undefined;
12
12
  * Looks up another token by `namespace/name`. Injected by the parent
13
13
  * Config so `Token.derived.value` can chase `token()` aliases without
14
14
  * holding a back-ref to the whole config object.
15
+ *
16
+ * `context` is the provenance of the token the ref was authored on. A ref
17
+ * inside a linked token is written against the SOURCE system's group names
18
+ * (`token('base/carbon-5')` in a group linked from Yahoo OS means Yahoo OS's
19
+ * `base` group, whatever the consumer renamed it to), so the lookup resolves
20
+ * it through the link markers instead of the consumer's own keys. Local
21
+ * tokens pass no context and resolve against the consumer's keys directly.
15
22
  */
16
- type TokenSiblingLookup = (qualifiedName: string) => Token | undefined;
23
+ type TokenSiblingLookup = (qualifiedName: string, context?: SystemLink) => Token | undefined;
17
24
  declare class Token {
18
25
  #private;
19
26
  readonly name: string;
package/dist/Token.js CHANGED
@@ -43,6 +43,7 @@ var Token = class {
43
43
  const tokenValue = this.value;
44
44
  const tokenType = this.type;
45
45
  const modifiers = this.modifiers;
46
+ const link = this.link;
46
47
  const prefixGetter = this.#prefixGetter;
47
48
  const lookup = this.#lookup;
48
49
  const groupTypeGetter = this.#groupTypeGetter;
@@ -57,10 +58,10 @@ var Token = class {
57
58
  return `var(${buildCssVar(prefixGetter(), namespace, name)})`;
58
59
  },
59
60
  get value() {
60
- return chaseLiteral(tokenValue, lookup);
61
+ return chaseLiteral(tokenValue, lookup, link);
61
62
  },
62
63
  get cssValueType() {
63
- const literal = chaseLiteral(tokenValue, lookup);
64
+ const literal = chaseLiteral(tokenValue, lookup, link);
64
65
  return resolveTokenValueType({
65
66
  value: typeof literal === "string" ? literal : String(tokenValue),
66
67
  type: tokenType
@@ -68,7 +69,7 @@ var Token = class {
68
69
  },
69
70
  *modifierValues() {
70
71
  if (!modifiers) return;
71
- for (const [key, value] of Object.entries(modifiers)) yield [key, resolveModifierLiteral(value, lookup)];
72
+ for (const [key, value] of Object.entries(modifiers)) yield [key, resolveModifierLiteral(value, lookup, link)];
72
73
  }
73
74
  };
74
75
  }
@@ -99,19 +100,20 @@ function collectModifiers(definition) {
99
100
  }
100
101
  return count > 0 ? modifiers : void 0;
101
102
  }
102
- function chaseLiteral(value, lookup, seen = /* @__PURE__ */ new Set()) {
103
+ function chaseLiteral(value, lookup, context, seen = /* @__PURE__ */ new Set()) {
103
104
  if (isTokenRef(value)) {
104
- if (seen.has(value.ref)) return void 0;
105
- seen.add(value.ref);
106
- const next = lookup(value.ref);
105
+ const seenKey = `${context?.systemId ?? ""}:${value.ref}`;
106
+ if (seen.has(seenKey)) return void 0;
107
+ seen.add(seenKey);
108
+ const next = lookup(value.ref, context);
107
109
  if (!next) return void 0;
108
- return chaseLiteral(next.value, lookup, seen);
110
+ return chaseLiteral(next.value, lookup, next.link, seen);
109
111
  }
110
112
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
111
113
  }
112
- function resolveModifierLiteral(value, lookup) {
113
- if (typeof value === "object" && value !== null && "value" in value) return chaseLiteral(value.value, lookup);
114
- return chaseLiteral(value, lookup);
114
+ function resolveModifierLiteral(value, lookup, context) {
115
+ if (typeof value === "object" && value !== null && "value" in value) return chaseLiteral(value.value, lookup, context);
116
+ return chaseLiteral(value, lookup, context);
115
117
  }
116
118
  //#endregion
117
119
  export { Token };