@yahoo/uds-create-config 5.57.0 → 5.57.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.
@@ -352,7 +352,7 @@ const LinkedSystemDefinition = defineEntity({
352
352
  input: unlinkInput,
353
353
  readOnly: false,
354
354
  label: "Unlink from",
355
- description: "Stop following a linked system's entities, in `link`'s shape or everything when `data` is omitted. The rows the declaration minted go with it, and so do the linked rows that point at them, since left linked they would bring them back; nothing of yours moves. A row of yours pointing at one of theirs refuses this — `copy` what is used first, or `break` with `keep: true`. `unlink-plan` names everything that goes.",
355
+ description: "Stop following a linked system's entities, in `link`'s shape or everything when `data` is omitted. The rows the declaration minted go with it, and so do the linked rows that point at them, since left linked they would bring them back, and the pin's `options` entries left with no row to adjust; nothing of yours moves. A row of yours pointing at one of theirs refuses this — `copy` what is used first, or `break` with `keep: true`. `unlink-plan` names everything that goes.",
356
356
  scope: "item",
357
357
  approval: "reference-change",
358
358
  title: (entity) => `Unlink from ${entity}`,
@@ -374,7 +374,7 @@ const LinkedSystemDefinition = defineEntity({
374
374
  input: unlinkInput,
375
375
  readOnly: true,
376
376
  label: "Plan an unlink",
377
- description: "What `unlink` with the same input removes — the minted rows the narrower declaration no longer reaches, with the linked rows taken along because they point at them named in `cascade` — and why the config would refuse it, a collision it would leave among them. Writes nothing.",
377
+ description: "What `unlink` with the same input removes — the minted rows the narrower declaration no longer reaches, with the linked rows taken along because they point at them named in `cascade`, and the `options` entries of the pin that go with them in `dropped` — and why the config would refuse it, a collision it would leave among them. Writes nothing.",
378
378
  scope: "item",
379
379
  title: (entity) => `Plan unlinking from ${entity}`,
380
380
  example: pinExample,
@@ -96,10 +96,20 @@ function planCopy({ config, input }) {
96
96
  const taking = [];
97
97
  const folders = [];
98
98
  const reused = [];
99
+ /** Folders this config draws through another pin, which a copied ref binds to as it does a reused row. */
100
+ const heldFolders = /* @__PURE__ */ new Set();
99
101
  /** Every `kind:path` `take` has answered, whichever way, so the walk below visits each once. */
100
102
  const seen = /* @__PURE__ */ new Set();
101
103
  const declared = declarationOf(config, slug);
102
104
  const folder = (kind, path) => {
105
+ const through = config.groupBorrowedFrom({
106
+ kind,
107
+ groupPath: path
108
+ });
109
+ if (through !== void 0 && through !== slug && input.onConflict !== "replace") {
110
+ heldFolders.add(`${kind}:${path}`);
111
+ return;
112
+ }
103
113
  if (!folders.some((f) => f.kind === kind && f.path === path)) folders.push({
104
114
  kind,
105
115
  path
@@ -258,6 +268,32 @@ function planCopy({ config, input }) {
258
268
  }
259
269
  }
260
270
  if (taking.length === 0 && wholesale) rejectData(`"${slug}" has nothing to copy.`);
271
+ /**
272
+ * The files of a sibling a copied render imports, when the sibling itself isn't copied — this config
273
+ * holds that path already, through another pin or as its own. The render imports the source's
274
+ * module, not whatever answers the path here, so that module lands without the component, and so do
275
+ * the files of the siblings it imports in turn. A copied sibling brings its own files.
276
+ */
277
+ const siblingFiles = ({ sibling, component }) => {
278
+ const key = `files:${sibling}`;
279
+ if (seen.has(key)) return;
280
+ seen.add(key);
281
+ if (taking.some((row) => row.kind === "component" && row.at === sibling)) return;
282
+ for (const path of renderSourceFiles({
283
+ source,
284
+ path: sibling
285
+ })) takeFile({
286
+ path,
287
+ component
288
+ });
289
+ for (const next of renderImportedComponents({
290
+ source,
291
+ path: sibling
292
+ })) siblingFiles({
293
+ sibling: next,
294
+ component
295
+ });
296
+ };
261
297
  /** Take what `body` points at by ref. */
262
298
  const follow = (body) => {
263
299
  for (const target of collectRefs(body)) {
@@ -318,12 +354,18 @@ function planCopy({ config, input }) {
318
354
  for (const sibling of renderImportedComponents({
319
355
  source,
320
356
  path: row.theirs
321
- })) take({
322
- kind: "component",
323
- theirs: sibling,
324
- at: sibling,
325
- named: false
326
- });
357
+ })) {
358
+ take({
359
+ kind: "component",
360
+ theirs: sibling,
361
+ at: sibling,
362
+ named: false
363
+ });
364
+ siblingFiles({
365
+ sibling,
366
+ component: row.theirs
367
+ });
368
+ }
327
369
  for (const path of renderSourceFiles({
328
370
  source,
329
371
  path: row.theirs
@@ -336,11 +378,17 @@ function planCopy({ config, input }) {
336
378
  const arriving = new Set([
337
379
  ...taking.map((row) => `${row.kind}:${row.at}`),
338
380
  ...reused.map((row) => `${row.kind}:${row.path}`),
381
+ ...heldFolders,
339
382
  ...folders.map((folder) => `${folder.kind}:${folder.path}`)
340
383
  ]);
341
384
  const patches = [];
342
385
  for (const { kind, path } of folders) {
343
- const meta = ownMeta(source.groups(kind).find((group) => group.path === path)?.meta ?? {});
386
+ const meta = ownMeta(config.copyOfGroup({
387
+ slug,
388
+ kind,
389
+ path,
390
+ alongside: arriving
391
+ }) ?? {});
344
392
  const pointed = config.groupBorrowedFrom({
345
393
  kind,
346
394
  groupPath: path
@@ -64,6 +64,9 @@ interface UnlinkPlan {
64
64
  /** Linked rows unlinked with the named ones because they point at them: left linked, they would
65
65
  * bring the named rows back on the next derive. */
66
66
  readonly cascade: readonly EntityAddress[];
67
+ /** The pin's `options` entries that go with the unlink: one on a path that stops deriving, and one
68
+ * on a path drawn from elsewhere that names a row that goes. Neither has a row to adjust. */
69
+ readonly dropped: readonly EntityAddress[];
67
70
  /** Why the config would refuse. `undefined` when it would go through. */
68
71
  readonly blocked?: string;
69
72
  /** The paths the unlink would leave two linked systems offering as different entities, when that
@@ -329,6 +329,7 @@ function planUnlink({ config, input }) {
329
329
  declared: void 0,
330
330
  removed: [],
331
331
  cascade: [],
332
+ dropped: [],
332
333
  patches: []
333
334
  };
334
335
  let declared;
@@ -397,16 +398,39 @@ function planUnlink({ config, input }) {
397
398
  }
398
399
  if (input.data !== void 0 && removed.length === 0) blocked = stillReached({ slug });
399
400
  }
401
+ const dropped = after ? deadDirectives({
402
+ config,
403
+ after,
404
+ slug,
405
+ removed
406
+ }) : [];
407
+ if (dropped.length > 0) {
408
+ const update = patches[patches.length - 1];
409
+ if (dropped.length === config.sourceDirectives(slug).length) update.data.options = null;
410
+ else for (const { accessor, path } of dropped) update.data[`options.${accessor}.${path}`] = null;
411
+ }
400
412
  return {
401
413
  declared,
402
414
  removed,
403
415
  cascade,
416
+ dropped: dropped.map(({ kind, path }) => ({
417
+ kind,
418
+ path
419
+ })),
404
420
  ...blocked === void 0 ? {} : { blocked },
405
421
  ...refused.collisions ? { collisions: refused.collisions } : {},
406
422
  patches
407
423
  };
408
424
  }
409
425
  /**
426
+ * The pin's directives an unlink leaves with nothing to do: on a path that no longer derives through
427
+ * the pin, and either on a row the unlink retires or naming one.
428
+ */
429
+ function deadDirectives({ config, after, slug, removed }) {
430
+ const gone = new Set(removed.map(({ kind, path }) => `${kind}:${path}`));
431
+ return config.sourceDirectives(slug).filter((directive) => directive.kind !== void 0 && after.resolve(directive.kind, directive.path)?.borrowedFrom !== slug && (gone.has(`${directive.kind}:${directive.path}`) || collectRefs(config.sourceOverlay(slug, directive.kind, directive.path)).some((target) => gone.has(target))));
432
+ }
433
+ /**
410
434
  * Leave paths of a linked system out, with a `!` entry each in the pin's `link`. Stronger than
411
435
  * `unlink`: nothing derives there however the link reaches it, a linked token naming it included,
412
436
  * and the linked tokens' overrides under an excluded modifier are left out of the build. `link` with
@@ -1942,6 +1942,42 @@ declare class Config {
1942
1942
  alongside?: ReadonlySet<string>;
1943
1943
  extend?: Record<string, unknown>;
1944
1944
  }): Record<string, unknown> | undefined;
1945
+ /**
1946
+ * One of a source's folders' meta as a copy stores it — its refs spelled from here as
1947
+ * {@link copyOf} spells a body's. The meta as the source holds it names its own pins by their slugs
1948
+ * there: the Bridge's `icons` folder names `package:@yahoo/uds-icons@yos`, and stored as it was that
1949
+ * names a pin this config doesn't have. `undefined` when the source has no folder there.
1950
+ */
1951
+ copyOfGroup({
1952
+ slug,
1953
+ kind,
1954
+ path,
1955
+ alongside
1956
+ }: {
1957
+ slug: string;
1958
+ kind: string;
1959
+ path: string; /** `kind:path` targets landing as this config's own in the same edit. */
1960
+ alongside?: ReadonlySet<string>;
1961
+ }): Record<string, unknown> | undefined;
1962
+ /**
1963
+ * `body` with each ref routed through another system's pin (`@yos-v3~yos`) respelled through this
1964
+ * config's own pin to the same system, where that pin's source holds the target too: a copy then
1965
+ * names what this config reaches directly rather than keeping the pin it came through referenced.
1966
+ * Bare when this config already holds the target as a pointer through that pin.
1967
+ */
1968
+ private throughDirectPins;
1969
+ /**
1970
+ * The pin of this config's own that reaches the system a routed source ends at — `yahoo-os` for
1971
+ * `yos-v3~yos`, when this config pins directly what `yos-v3` pins as `yos`. Matched on the pin
1972
+ * kind's `sourceOrigin`, so the two pins may name different versions. `undefined` for a source of
1973
+ * one hop, or one whose system no pin here reaches.
1974
+ *
1975
+ * The two pins match when they agree on every origin field both set, and share at least one. Not on
1976
+ * the whole key: a pin by path is stamped with its sibling's id when it's pushed, so the Bridge's
1977
+ * `yos` reads `configPath` and `systemId` where this config's pin to the same system names only the
1978
+ * id — and a `configPath` is relative to the config holding it, so it can't be compared across two.
1979
+ */
1980
+ private directRoute;
1945
1981
  /**
1946
1982
  * Whether a copied row no longer matches what its source offers at the path it was copied from.
1947
1983
  *
@@ -2255,6 +2291,16 @@ declare class Config {
2255
2291
  * that reaches derivation — a deleted extension whose source reached the row — says what it did.
2256
2292
  */
2257
2293
  private assertDerivedDropsUnreferenced;
2294
+ /**
2295
+ * Whether every directive of `pin` that names `target` sits on a path that no longer derives through
2296
+ * it — retired by this derivation, or drawn from another source already. Such a directive lays its
2297
+ * value over no row, so it goes with the link rather than being stranded by it: a pin's own
2298
+ * `options` never refuse removing its link. A directive on a row the pin still mints does, since
2299
+ * that row would be left pointing at nothing.
2300
+ *
2301
+ * `false` when `pin` is not a source, or names `target` somewhere other than a directive.
2302
+ */
2303
+ private directivesGoWith;
2258
2304
  /**
2259
2305
  * This config with a row for every entity it inherits — the build's output. A pin patch runs this
2260
2306
  * inside {@link apply}; a source attaching runs it through the session.
@@ -2513,6 +2559,8 @@ declare class Config {
2513
2559
  */
2514
2560
  sourceIdentityKey(kind: string, path: string): string | undefined;
2515
2561
  private sourceKey;
2562
+ /** Each of a pin's `of` fields as it reads, `''` where unset, in the kind's order. */
2563
+ private sourceFields;
2516
2564
  /**
2517
2565
  * Where what this config shows at `path` originates: the system it was first authored in, followed
2518
2566
  * through however many pins it took to get here. `undefined` for a row this config authored itself.
@@ -3935,25 +3935,85 @@ var Config = class Config {
3935
3935
  path,
3936
3936
  derives
3937
3937
  });
3938
- if (!theirs || !extend) return theirs;
3939
- const laid = overlayBody({
3938
+ if (!theirs) return void 0;
3939
+ const laid = extend ? overlayBody({
3940
3940
  overlay: {
3941
3941
  extend,
3942
3942
  hidden: false
3943
3943
  },
3944
3944
  theirs
3945
- });
3946
- if (laid && redirectOf(laid).target !== void 0) return this.laidLinkedBody({
3945
+ }) : void 0;
3946
+ return this.throughDirectPins(laid ? this.laidLinkedBody({
3947
3947
  kind,
3948
3948
  path,
3949
3949
  body: theirs,
3950
3950
  extend: laid,
3951
3951
  source: this.sourceFor(slug)
3952
- });
3953
- return laid ? {
3954
- ...theirs,
3955
- ...laid
3956
- } : theirs;
3952
+ }) : theirs);
3953
+ }
3954
+ /**
3955
+ * One of a source's folders' meta as a copy stores it — its refs spelled from here as
3956
+ * {@link copyOf} spells a body's. The meta as the source holds it names its own pins by their slugs
3957
+ * there: the Bridge's `icons` folder names `package:@yahoo/uds-icons@yos`, and stored as it was that
3958
+ * names a pin this config doesn't have. `undefined` when the source has no folder there.
3959
+ */
3960
+ copyOfGroup({ slug, kind, path, alongside }) {
3961
+ const source = this.sourceFor(slug);
3962
+ const meta = source?.groups(kind).find((group) => group.path === path)?.meta;
3963
+ if (!source || !meta) return void 0;
3964
+ return this.throughDirectPins(qualifyRefs({
3965
+ value: meta,
3966
+ source: slug,
3967
+ owns: (kindName) => source.ownedKinds().includes(kindName),
3968
+ namesItAlready: (target) => alongside?.has(`${kindOf(target)}:${pathOf(target)}`) === true || this.borrowsExactly(target, slug)
3969
+ }));
3970
+ }
3971
+ /**
3972
+ * `body` with each ref routed through another system's pin (`@yos-v3~yos`) respelled through this
3973
+ * config's own pin to the same system, where that pin's source holds the target too: a copy then
3974
+ * names what this config reaches directly rather than keeping the pin it came through referenced.
3975
+ * Bare when this config already holds the target as a pointer through that pin.
3976
+ */
3977
+ throughDirectPins(body) {
3978
+ let respelled = body;
3979
+ for (const target of new Set(collectRefs(body))) {
3980
+ const { kind, path, source } = splitRef(target);
3981
+ if (source === void 0) continue;
3982
+ const direct = this.directRoute(source);
3983
+ if (direct === void 0 || !this.sourceFor(direct)?.resolve(kind, path)) continue;
3984
+ const through = ref(kind, path, direct);
3985
+ respelled = rewriteRefs(respelled, `${kind}:${path}@${source}`, this.borrowsExactly(through, direct) ? `${kind}:${path}` : through.__ref);
3986
+ }
3987
+ return respelled;
3988
+ }
3989
+ /**
3990
+ * The pin of this config's own that reaches the system a routed source ends at — `yahoo-os` for
3991
+ * `yos-v3~yos`, when this config pins directly what `yos-v3` pins as `yos`. Matched on the pin
3992
+ * kind's `sourceOrigin`, so the two pins may name different versions. `undefined` for a source of
3993
+ * one hop, or one whose system no pin here reaches.
3994
+ *
3995
+ * The two pins match when they agree on every origin field both set, and share at least one. Not on
3996
+ * the whole key: a pin by path is stamped with its sibling's id when it's pushed, so the Bridge's
3997
+ * `yos` reads `configPath` and `systemId` where this config's pin to the same system names only the
3998
+ * id — and a `configPath` is relative to the config holding it, so it can't be compared across two.
3999
+ */
4000
+ directRoute(source) {
4001
+ const hops = sourceHops(source);
4002
+ const last = hops.pop();
4003
+ if (last === void 0 || hops.length === 0) return void 0;
4004
+ let through = this;
4005
+ for (const hop of hops) through = through?.sourceFor(hop);
4006
+ if (!through) return void 0;
4007
+ for (const sourceKind of through.sourceKinds()) {
4008
+ const theirs = through.sourceFields(sourceKind, last, "sourceOrigin");
4009
+ if (theirs === void 0) continue;
4010
+ const direct = this.sourceSlugs().find((slug) => {
4011
+ const mine = this.sourceFields(sourceKind, slug, "sourceOrigin");
4012
+ const shared = theirs.flatMap((value, field) => value !== "" && mine?.[field] ? [field] : []);
4013
+ return shared.length > 0 && shared.every((field) => theirs[field] === mine?.[field]);
4014
+ });
4015
+ if (direct !== void 0) return direct;
4016
+ }
3957
4017
  }
3958
4018
  /**
3959
4019
  * Whether a copied row no longer matches what its source offers at the path it was copied from.
@@ -4584,11 +4644,16 @@ var Config = class Config {
4584
4644
  assertDerivedDropsUnreferenced({ action = "remove the link", derivation = this.rowDerivation() } = {}) {
4585
4645
  const { snapshot } = this.state;
4586
4646
  const contested = new Set(derivation.collisions.map(({ kind, path }) => `${kind}:${path}`));
4647
+ const retired = new Set(derivation.changes.filter((change) => change.value === void 0).map(({ kind, path }) => `${kind}:${path}`));
4587
4648
  const stranded = [];
4588
4649
  let collides = false;
4589
4650
  for (const change of derivation.changes) {
4590
4651
  if (change.value !== void 0) continue;
4591
- const referrers = this.referrersOf(snapshot, change.kind, change.path).filter((referrer) => !isMintedAt(snapshot, referrer));
4652
+ const referrers = this.referrersOf(snapshot, change.kind, change.path).filter((referrer) => !isMintedAt(snapshot, referrer) && !this.directivesGoWith({
4653
+ pin: referrer,
4654
+ target: change,
4655
+ retired
4656
+ }));
4592
4657
  if (referrers.length === 0) continue;
4593
4658
  if (contested.has(`${change.kind}:${change.path}`)) collides = true;
4594
4659
  stranded.push(`${change.kind}:${change.path} (used by ${referrers.join(", ")})`);
@@ -4598,6 +4663,23 @@ var Config = class Config {
4598
4663
  reject("REFERRERS_BLOCK", `Cannot ${action} — it would take away ${stranded.join("; ")}. Copy what is used first (\`linked-system/copy\`), or break the link keeping its values (\`linked-system/break\` with \`keep: true\`).`);
4599
4664
  }
4600
4665
  /**
4666
+ * Whether every directive of `pin` that names `target` sits on a path that no longer derives through
4667
+ * it — retired by this derivation, or drawn from another source already. Such a directive lays its
4668
+ * value over no row, so it goes with the link rather than being stranded by it: a pin's own
4669
+ * `options` never refuse removing its link. A directive on a row the pin still mints does, since
4670
+ * that row would be left pointing at nothing.
4671
+ *
4672
+ * `false` when `pin` is not a source, or names `target` somewhere other than a directive.
4673
+ */
4674
+ directivesGoWith({ pin, target, retired }) {
4675
+ if (!this.entityOf(kindOf(pin))?.sourceIdentity) return false;
4676
+ const slug = pathOf(pin);
4677
+ const aliases = new Set(this.refAliases(target.kind, target.path));
4678
+ const naming = this.sourceDirectives(slug).filter((directive) => directive.kind !== void 0 && collectRefs(this.sourceOverlay(slug, directive.kind, directive.path)).some((ref) => aliases.has(ref)));
4679
+ if (naming.length === 0) return false;
4680
+ return naming.every(({ kind, path }) => retired.has(`${kind}:${path}`) || this.resolve(kind, path)?.borrowedFrom !== slug);
4681
+ }
4682
+ /**
4601
4683
  * This config with a row for every entity it inherits — the build's output. A pin patch runs this
4602
4684
  * inside {@link apply}; a source attaching runs it through the session.
4603
4685
  *
@@ -5415,6 +5497,10 @@ var Config = class Config {
5415
5497
  return this.sourceKey(kind, path, "sourceIdentity");
5416
5498
  }
5417
5499
  sourceKey(kind, path, of) {
5500
+ return this.sourceFields(kind, path, of)?.join("\0");
5501
+ }
5502
+ /** Each of a pin's `of` fields as it reads, `''` where unset, in the kind's order. */
5503
+ sourceFields(kind, path, of) {
5418
5504
  const entry = this.entityOf(kind);
5419
5505
  const fields = entry?.[of] ?? entry?.sourceIdentity;
5420
5506
  if (!fields) return void 0;
@@ -5423,7 +5509,7 @@ var Config = class Config {
5423
5509
  return fields.map((field) => {
5424
5510
  const value = body[field];
5425
5511
  return value === void 0 ? "" : String(value);
5426
- }).join("\0");
5512
+ });
5427
5513
  }
5428
5514
  /**
5429
5515
  * Where what this config shows at `path` originates: the system it was first authored in, followed