@yahoo/uds-create-config 5.64.1 → 5.64.2

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.
@@ -316,10 +316,11 @@ const LinkedSystemDefinition = defineEntity({
316
316
  title: (entity) => `Link something from ${entity}`,
317
317
  example: linkExample,
318
318
  handler: (input, config) => {
319
- const { plan: { linked, patches }, after } = planLinkApplied({
319
+ const { plan: { linked, patches, stranded, blocked }, after } = planLinkApplied({
320
320
  config,
321
321
  input: linkPlanInputOf(input)
322
322
  });
323
+ if (stranded && stranded.length > 0 && blocked) rejectData(blocked, ["onLinked"]);
323
324
  if (linked.length > 0 && input.onLinked === void 0) {
324
325
  const named = linked.map((at) => `${at.path} (from ${at.from})`);
325
326
  rejectData(`Linking through "${String(input.path)}" reaches what another linked system provides: ${named.join(", ")}. Say whether to take them (\`onLinked: 'take'\`) or keep them where they come from (\`onLinked: 'keep'\`).`, ["onLinked"]);
@@ -419,7 +419,8 @@ function linkedComponentModule(config, componentPathOrRef) {
419
419
  const owner = config.configFor(fileRef);
420
420
  const pin = publishedPin(config, slug);
421
421
  if (!owner || !pin) return void 0;
422
- const view = cachedView(config, owner, slug, askedSource !== void 0);
422
+ const shadowed = askedSource !== void 0;
423
+ const view = cachedView(config, (shadowed && owner === config.sourceFor(slug) ? config.drawnSourceFor(slug) : void 0) ?? owner, slug, shadowed);
423
424
  const moduleKey = componentModuleKey(view, filePath);
424
425
  if (!moduleKey) return void 0;
425
426
  const authored = Component.list(owner).find((candidate) => {
@@ -29,6 +29,12 @@ interface DroppedDirective extends EntityAddress {
29
29
  /** The accessor the entry sits under in that pin's `options`. */
30
30
  readonly accessor: string;
31
31
  }
32
+ /** An entry in another pin's `options` a take would leave naming something that pin's own copy can't
33
+ * be drawn with ({@link Config.pinCopyOf}). The take refuses rather than remove it. */
34
+ interface StrandedDirective extends DroppedDirective {
35
+ /** The refs the entry names that the pin's copy has no name for. */
36
+ readonly unreadable: readonly string[];
37
+ }
32
38
  /** One row a link mints. `named` when a path the call named covers it; otherwise it comes along
33
39
  * because something linked points at it. */
34
40
  interface LinkArrival extends EntityAddress {
@@ -51,6 +57,10 @@ interface LinkPlan {
51
57
  * rows their pin derives, and the row there follows this one now. Not carried onto the taken row,
52
58
  * since an entry corrects what that pin brings rather than stating a value of this config's own. */
53
59
  readonly dropped: readonly DroppedDirective[];
60
+ /** The other pins' entries at what the take moves that it can't carry: each lays over a copy that
61
+ * pin still builds, and names something the copy can't be drawn with. The take refuses while any
62
+ * remain, and `blocked` says what to change. */
63
+ readonly stranded?: readonly StrandedDirective[];
54
64
  /** Members already here from the source's own origin, left alone and kept out of the declaration. */
55
65
  readonly shared: readonly EntityAddress[];
56
66
  /** Paths another pin offers that a `hybrid` link reaches as a different entity. The other pin's
@@ -267,12 +267,13 @@ function planLinkApplied({ config, input }) {
267
267
  ];
268
268
  let result = planned.length > 0 ? applied(config, planned) : void 0;
269
269
  if (takes.length > 0 && result && "failed" in result) throw result.failed;
270
- const { dropped, patches: drops } = takes.length > 0 && result && "config" in result ? directivesTakenFrom({
270
+ const { dropped, stranded, patches: drops } = takes.length > 0 && result && "config" in result ? directivesTakenFrom({
271
271
  config,
272
272
  slug,
273
273
  after: result.config
274
274
  }) : {
275
275
  dropped: [],
276
+ stranded: [],
276
277
  patches: []
277
278
  };
278
279
  if (drops.length > 0 && result && "config" in result) result = applied(result.config, drops);
@@ -292,9 +293,13 @@ function planLinkApplied({ config, input }) {
292
293
  extended,
293
294
  dropped,
294
295
  shared,
295
- patches
296
+ patches,
297
+ ...stranded.length > 0 ? {
298
+ stranded,
299
+ blocked: strandedMessage(slug, stranded)
300
+ } : {}
296
301
  },
297
- after: result && "config" in result ? result.config : void 0
302
+ after: stranded.length === 0 && result && "config" in result ? result.config : void 0
298
303
  };
299
304
  }
300
305
  /**
@@ -314,14 +319,20 @@ function directivesTakenFrom({ config, slug, after }) {
314
319
  const key = (at) => `${at.slug}\u0000${at.accessor}\u0000${at.path}`;
315
320
  const already = new Set(danglingDirectives(config).map(key));
316
321
  const dropped = [];
322
+ const stranded = [];
317
323
  for (const at of danglingDirectives(after)) {
318
324
  if (at.missing !== "row" || at.heldBy !== slug || already.has(key(at))) continue;
319
- dropped.push({
325
+ const entry = {
320
326
  kind: config.accessors()[at.accessor]?.kind ?? at.accessor,
321
327
  path: at.path,
322
328
  from: at.slug,
323
329
  accessor: at.accessor
330
+ };
331
+ if (at.unreadable) stranded.push({
332
+ ...entry,
333
+ unreadable: at.unreadable
324
334
  });
335
+ else dropped.push(entry);
325
336
  }
326
337
  const edits = [];
327
338
  for (const pin of new Set(dropped.map((at) => at.from))) {
@@ -347,9 +358,16 @@ function directivesTakenFrom({ config, slug, after }) {
347
358
  }
348
359
  return {
349
360
  dropped,
361
+ stranded,
350
362
  patches: edits
351
363
  };
352
364
  }
365
+ /** Why a take refuses over the entries it can't carry ({@link StrandedDirective}). */
366
+ function strandedMessage(slug, stranded) {
367
+ const named = stranded.map((at) => `${at.from}'s ${at.accessor} entry at "${at.path}" names ${at.unreadable.join(", ")}`).join("; ");
368
+ const [one] = stranded;
369
+ return `Can't take through "${slug}": ${named}. ${stranded.length === 1 && one ? `${one.from} still builds its own ${one.path}, which its components render, and that entry applies to it` : "each pin still builds its own copy, which its components render, and the entry applies to it"}, but Create can draw that copy only with this system's own tokens, not yet with this system's own components, composites or motion presets. Point those values at something the pin links, or remove them from the entry, then take again.`;
370
+ }
353
371
  /** What a plan reports of a refusal: its message, and the collisions when that's what it names. */
354
372
  function refusal(error) {
355
373
  const collisions = error.issues.flatMap((issue) => issue.collisions ?? []);
@@ -2460,6 +2460,41 @@ declare class Config {
2460
2460
  kind: string;
2461
2461
  path: string;
2462
2462
  }): boolean;
2463
+ /**
2464
+ * The source at `slug` as this build draws the copies it still builds of its components that another
2465
+ * row stands in for here, each drawn the way the pin's own row was: with the entry the pin's
2466
+ * `options` lay at its path ({@link pinCopyOf}).
2467
+ *
2468
+ * A product that takes Yahoo OS's Button still builds the Bridge's, which the Bridge's Banner
2469
+ * renders, and the Bridge pin's entry for its Button is what that copy is drawn with. Everything else
2470
+ * is the source's own, so the source itself comes back when no copy carries an entry.
2471
+ */
2472
+ drawnSourceFor(slug: string): Config | undefined;
2473
+ /**
2474
+ * The body `slug`'s copy of `kind:path` is drawn with, spelled as the source spells it: the source's
2475
+ * entity with the entry the pin's `options` lay at the path, read as the pin's row would read it
2476
+ * here. `undefined` when this config holds the pin's row at the path (the row is what's drawn), holds
2477
+ * nothing there, or the pin lays nothing over it.
2478
+ *
2479
+ * A token it names that the source has no name for (one of this config's own, or one it takes
2480
+ * through another pin) is named under {@link CONSUMER_TOKENS} instead, and `tokens` gives each such
2481
+ * path the variable this config's stylesheet declares for it. `unreadable` names any other ref the
2482
+ * source has no name for: the copy can't be drawn with it, so the entry is reported instead.
2483
+ */
2484
+ pinCopyOf({
2485
+ slug,
2486
+ kind,
2487
+ path
2488
+ }: {
2489
+ slug: string;
2490
+ kind: string;
2491
+ path: string;
2492
+ }): {
2493
+ readonly body: Record<string, unknown>;
2494
+ readonly tokens: ReadonlyMap<string, string>;
2495
+ } | {
2496
+ readonly unreadable: readonly string[];
2497
+ } | undefined;
2463
2498
  /**
2464
2499
  * The group counterpart of {@link heldThroughAnotherPin}: this config holds the group at `path`
2465
2500
  * through another pin, and every member `slug` offers under it is held that way too. A group is a
@@ -9,7 +9,7 @@ import { isComposedValue } from "../entities/system/composed-values.js";
9
9
  import { borrowedSlug, classifyOverlay, uniteEntries } from "./source-integrity.js";
10
10
  import { kindBorrowable, titleCase } from "./registry.js";
11
11
  import { isGroupBody } from "./utils/group.js";
12
- import { authoredBag, pinBodyStyleNumbers, stylePropNumberUnit } from "./projections.js";
12
+ import { authoredBag, cssVar, declaresCssVariable, pinBodyStyleNumbers, stylePropNumberUnit } from "./projections.js";
13
13
  import { isAuthoringPhantom, phantomsIn } from "./utils/authoring-phantom.js";
14
14
  import { addressOf, editOp, inputOf, opVerb } from "./config-op.js";
15
15
  import { configSchemas } from "./config-ops.js";
@@ -696,6 +696,13 @@ function objectSchemaOf(schema) {
696
696
  * of {@link Config.sourceClosure}. Per config, since a source's files and components only change
697
697
  * with the source. */
698
698
  const implementedByCache = /* @__PURE__ */ new WeakMap();
699
+ /**
700
+ * The folder a source's drawn copy ({@link Config.drawnSourceFor}) names this config's own tokens
701
+ * under. Not a path a system authors, so it never stands for one of the source's.
702
+ */
703
+ const CONSUMER_TOKENS = "__consumer";
704
+ /** {@link Config.drawnSourceFor} per config and slug. */
705
+ const drawnSources = /* @__PURE__ */ new WeakMap();
699
706
  function implementedBy(config) {
700
707
  const cached = implementedByCache.get(config);
701
708
  if (cached) return cached;
@@ -5009,6 +5016,104 @@ var Config = class Config {
5009
5016
  });
5010
5017
  }
5011
5018
  /**
5019
+ * The source at `slug` as this build draws the copies it still builds of its components that another
5020
+ * row stands in for here, each drawn the way the pin's own row was: with the entry the pin's
5021
+ * `options` lay at its path ({@link pinCopyOf}).
5022
+ *
5023
+ * A product that takes Yahoo OS's Button still builds the Bridge's, which the Bridge's Banner
5024
+ * renders, and the Bridge pin's entry for its Button is what that copy is drawn with. Everything else
5025
+ * is the source's own, so the source itself comes back when no copy carries an entry.
5026
+ */
5027
+ drawnSourceFor(slug) {
5028
+ const source = this.sourceFor(slug);
5029
+ if (!source) return void 0;
5030
+ let bySlug = drawnSources.get(this);
5031
+ if (!bySlug) {
5032
+ bySlug = /* @__PURE__ */ new Map();
5033
+ drawnSources.set(this, bySlug);
5034
+ }
5035
+ const cached = bySlug.get(slug);
5036
+ if (cached) return cached;
5037
+ const changes = [];
5038
+ for (const { kind, path } of this.sourceDirectives(slug)) {
5039
+ const entry = kind === void 0 ? void 0 : this.entityOf(kind);
5040
+ if (kind !== "component" || !entry) continue;
5041
+ const copy = this.pinCopyOf({
5042
+ slug,
5043
+ kind,
5044
+ path
5045
+ });
5046
+ if (!copy || !("body" in copy)) continue;
5047
+ changes.push({
5048
+ kind,
5049
+ target: "item",
5050
+ path,
5051
+ value: new Entity(path, copy.body, entry.fieldKeys)
5052
+ });
5053
+ const tokenEntry = this.entityOf("token");
5054
+ for (const [at, cssVariable] of copy.tokens) {
5055
+ if (!tokenEntry) break;
5056
+ changes.push({
5057
+ kind: "token",
5058
+ target: "item",
5059
+ path: at,
5060
+ value: new Entity(at, { cssVariable }, tokenEntry.fieldKeys)
5061
+ });
5062
+ }
5063
+ }
5064
+ const drawn = changes.length === 0 ? source : source.derive({ snapshot: source.state.snapshot.withChanges(changes) });
5065
+ bySlug.set(slug, drawn);
5066
+ return drawn;
5067
+ }
5068
+ /**
5069
+ * The body `slug`'s copy of `kind:path` is drawn with, spelled as the source spells it: the source's
5070
+ * entity with the entry the pin's `options` lay at the path, read as the pin's row would read it
5071
+ * here. `undefined` when this config holds the pin's row at the path (the row is what's drawn), holds
5072
+ * nothing there, or the pin lays nothing over it.
5073
+ *
5074
+ * A token it names that the source has no name for (one of this config's own, or one it takes
5075
+ * through another pin) is named under {@link CONSUMER_TOKENS} instead, and `tokens` gives each such
5076
+ * path the variable this config's stylesheet declares for it. `unreadable` names any other ref the
5077
+ * source has no name for: the copy can't be drawn with it, so the entry is reported instead.
5078
+ */
5079
+ pinCopyOf({ slug, kind, path }) {
5080
+ const stored = this.storedBody(kind, path);
5081
+ if (stored === void 0 || isRef(stored) && sourceOf(stored) === slug) return;
5082
+ const overlay = this.sourceDirective(slug, kind, path).overlay;
5083
+ const source = this.sourceFor(slug);
5084
+ const theirs = this.theirBody({
5085
+ slug,
5086
+ kind,
5087
+ path
5088
+ });
5089
+ if (!overlay || !source || !theirs) return void 0;
5090
+ const read = this.derive({ snapshot: this.state.snapshot.withChanges([derivedItemChange(kind, path, ref(kind, path, slug).__ref, overlayBody({
5091
+ overlay,
5092
+ theirs
5093
+ }))]) }).resolve(kind, path)?.toJSON();
5094
+ if (!read) return void 0;
5095
+ const owned = new Set(source.ownedKinds());
5096
+ const unreadable = [...new Set(collectRefs(read).filter((target) => sourceOf(target) === void 0 && owned.has(kindOf(target)) && pathOf(target) !== "" && !this.borrowsExactly({ __ref: target }, slug)))];
5097
+ let body = read;
5098
+ const tokens = /* @__PURE__ */ new Map();
5099
+ const others = [];
5100
+ for (const target of unreadable) {
5101
+ const path = pathOf(target);
5102
+ if (kindOf(target) !== "token" || !declaresCssVariable(this, path)) {
5103
+ others.push(target);
5104
+ continue;
5105
+ }
5106
+ const at = `${CONSUMER_TOKENS}/${path}`;
5107
+ body = rewriteRefs(body, target, ref("token", at).__ref);
5108
+ tokens.set(at, cssVar(this, "token", path));
5109
+ }
5110
+ if (others.length > 0) return { unreadable: others };
5111
+ return {
5112
+ body: rewriteRefSource(body, slug, void 0),
5113
+ tokens
5114
+ };
5115
+ }
5116
+ /**
5012
5117
  * The group counterpart of {@link heldThroughAnotherPin}: this config holds the group at `path`
5013
5118
  * through another pin, and every member `slug` offers under it is held that way too. A group is a
5014
5119
  * folder over its members, so it is the same folder exactly when they are the same entities.
@@ -80,7 +80,7 @@ const sourceVarPrefix = (config, slug) => prefixed({
80
80
  */
81
81
  function linkedVarNaming(config, slug) {
82
82
  const pinned = sourceVarPrefix(config, slug);
83
- const source = config.sourceFor(slug);
83
+ const source = config.drawnSourceFor(slug);
84
84
  const named = /* @__PURE__ */ new Map();
85
85
  return (tokenPath) => {
86
86
  const hit = named.get(tokenPath);
@@ -249,6 +249,11 @@ interface DanglingDirective {
249
249
  missing: 'accessor' | 'path' | 'row';
250
250
  /** With `row`: the pin the row there follows instead, absent when it is this config's own. */
251
251
  heldBy?: string;
252
+ /**
253
+ * With `row`, for a component the pin still builds its own copy of beside that row: the refs the
254
+ * entry names that the source has no name for, so the copy can't be drawn with it.
255
+ */
256
+ unreadable?: readonly string[];
252
257
  }
253
258
  /**
254
259
  * Every directive aimed at nothing — an accessor this config has no kind for, a path the source
@@ -534,6 +534,15 @@ function heldElsewhere(config, { slug, kind, path }) {
534
534
  */
535
535
  function danglingDirectives(config) {
536
536
  const found = [];
537
+ const reached = /* @__PURE__ */ new Map();
538
+ const reachedComponents = (slug) => {
539
+ let paths = reached.get(slug);
540
+ if (!paths) {
541
+ paths = new Set(config.sourceClosure(slug, "component").map(pathOf));
542
+ reached.set(slug, paths);
543
+ }
544
+ return paths;
545
+ };
537
546
  for (const slug of config.sourceSlugs()) {
538
547
  const source = config.liveSourceFor(slug);
539
548
  if (!source) continue;
@@ -555,6 +564,25 @@ function danglingDirectives(config) {
555
564
  path
556
565
  });
557
566
  if (heldBy === void 0) continue;
567
+ if (kind === "component" && reachedComponents(slug).has(path)) {
568
+ const copy = config.pinCopyOf({
569
+ slug,
570
+ kind,
571
+ path
572
+ });
573
+ if (copy && "unreadable" in copy) {
574
+ found.push({
575
+ slug,
576
+ accessor,
577
+ path,
578
+ missing: "row",
579
+ ...heldBy === null ? {} : { heldBy },
580
+ unreadable: copy.unreadable
581
+ });
582
+ continue;
583
+ }
584
+ if (copy) continue;
585
+ }
558
586
  found.push({
559
587
  slug,
560
588
  accessor,