@yahoo/uds-create-config 5.57.0 → 5.58.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.
@@ -17,6 +17,7 @@
17
17
  * The returned handle is a ref AND an `Authored`, so it composes into other components and
18
18
  * registers through the generic bridge.
19
19
  */
20
+ import { type PropsOrRules } from '../entities/system/prop-rules';
20
21
  import type { Authored } from '../framework/authoring';
21
22
  import { type Ref } from '../framework/utils/refs';
22
23
  /** A component handle — a ref (import/reference it), an `Authored` (register it), and a
@@ -69,7 +70,7 @@ interface Body<L, SP extends string, MOD extends string, P extends Record<string
69
70
  when: Record<string, string>;
70
71
  layers: LayerOverrides<L, SP, MOD>;
71
72
  }>;
72
- defaultProps?: Record<string, unknown>;
73
+ defaultProps?: PropsOrRules<Record<string, unknown>>;
73
74
  label?: string;
74
75
  description?: string;
75
76
  }
@@ -13,7 +13,8 @@ import { StoredDefaultCondition } from "../../framework/default-conditions.js";
13
13
  import { StoredPreviewCondition } from "../../framework/preview-store.js";
14
14
  import { RepeatSpec } from "../../framework/render-spec.js";
15
15
  import { ComponentSourceInput } from "./component-source-fields.js";
16
- import { AsInput, AuthoredDefaultsOf, LayerMap, PhantomCallProps, PhantomReadProps, PropMap, RenderArgs, StateMap, StyleRuleInput } from "./defineComponent.js";
16
+ import { PropsOrRules } from "./prop-rules.js";
17
+ import { AsInput, AuthoredDefaultsOf, LayerMap, PhantomCallProps, PhantomReadProps, PropMap, RenderArgs, StateMap, StyleRuleInput, UnconditionalDefaults } from "./defineComponent.js";
17
18
  import { listSlots, namedSlots, slotHostedBy, slotOf, slotsFor } from "../../spec/slot.js";
18
19
  import { cellAt } from "../../spec/cell.js";
19
20
  import { instanceAt } from "../../spec/instance.js";
@@ -871,7 +872,9 @@ declare const ComponentEntity: EntityClass<z.ZodObject<{
871
872
  }: ComputedInput) => string[];
872
873
  }, false, Record<never, never>, Record<never, never>>;
873
874
  /** Source extensions declare their structure through .render(). */
874
- type ComponentRefDelta = ComponentSourceInput<Partial<CreateInput<typeof componentFields>>>;
875
+ type ComponentRefDelta = Omit<ComponentSourceInput<Partial<CreateInput<typeof componentFields>>>, 'defaultProps'> & {
876
+ /** A record, or `.defaultProps`'s rule list; checked against the props by `ExtensionDefaults`. */readonly defaultProps?: PropsOrRules<Record<string, unknown>>;
877
+ };
875
878
  type ExactComponentDelta<Delta> = Delta & { [Key in Exclude<keyof Delta, keyof ComponentRefDelta>]: never };
876
879
  type ContractLayers<Contract> = Contract extends {
877
880
  layers: infer Layers;
@@ -908,7 +911,7 @@ type ResolvedDefaults<Contract, Delta> = Delta extends {
908
911
  readonly defaultProps: infer Defaults;
909
912
  } ? OverlayContract<Contract extends {
910
913
  defaultProps: infer Base;
911
- } ? Base : Record<never, never>, Defaults> : Contract extends {
914
+ } ? Base : Record<never, never>, UnconditionalDefaults<Defaults>> : Contract extends {
912
915
  defaultProps: infer Defaults;
913
916
  } ? Defaults : Record<never, never>;
914
917
  type ResolvedForwards<Contract, Delta> = Delta extends {
@@ -27,9 +27,9 @@ import { instanceAt } from "../../spec/instance.js";
27
27
  import { forwardOptions, forwardsWrite } from "./component-forwards.js";
28
28
  import { assertComponentSourceInput } from "./component-source-fields.js";
29
29
  import { checkStyleSet, styleSetEdits } from "./component-style-set.js";
30
- import { storedDefaults } from "./conditional-defaults.js";
31
30
  import { forwardRouteInvariants, refusedRoutes } from "./forward-route-invariants.js";
32
31
  import { iconDomainInvariants } from "./icon-domain-invariants.js";
32
+ import { storedDefaults } from "./prop-rules.js";
33
33
  import { rootLayerInvariants } from "./root-layer-invariants.js";
34
34
  import { Token } from "./Token.js";
35
35
  import { z } from "zod";
@@ -1199,7 +1199,7 @@ const Preview = defineSubEntity({
1199
1199
  });
1200
1200
  }
1201
1201
  });
1202
- const DEFAULT_CONDITIONS_DESCRIPTION = "Defaults that hold only under a condition on this component's other props, laid over `defaultProps` the way a style rule is over the unconditional one: a list of entries, each identified by its condition. `{ \"when\": { \"modal\": false }, \"props\": { \"includeScrim\": false } }` defaults `includeScrim` to `false` while `modal` is `false`, and to its `defaultProps` value otherwise. When several entries hold, the later one wins on the prop it sets. Both sides name the component's own variant, string, number or boolean props, a prop set here is never part of a condition, and a value the call site passes always wins. Authored as `conditionalDefault(fallback, [{ when, value }])` inside `.defaultProps`.";
1202
+ const DEFAULT_CONDITIONS_DESCRIPTION = "Defaults that hold only under a condition on this component's other props, laid over `defaultProps` the way a style rule is over the unconditional one: a list of entries, each identified by its condition. `{ \"when\": { \"modal\": false }, \"props\": { \"includeScrim\": false } }` defaults `includeScrim` to `false` while `modal` is `false`, and to its `defaultProps` value otherwise. When several entries hold, the later one wins on the prop it sets. Both sides name the component's own variant, string, number or boolean props, a prop set here is never part of a condition, and a value the call site passes always wins. Authored as a rule in `.defaultProps([{ when, props }])`.";
1203
1203
  const defaultConditionShapeError = "a conditional default is `{ \"when\": { \"<prop>\": <value> }, \"props\": { \"<prop>\": <value> } }`: both name this component's own variant, string, number or boolean props (not `as`), `when` at one of their values.";
1204
1204
  const defaultConditionBase = z.object({
1205
1205
  when: z.record(z.string(), z.unknown()),
@@ -1811,9 +1811,9 @@ const componentFields = z.strictObject({
1811
1811
  anatomy: AnatomyNode.optional().describe("The tree of what this component renders, keyed by node. Each node names what it renders (a layer, for anything a style rule reaches) and lists its `children`; the node keyed `root` is the top. A one-layer component may omit it. Anatomy read from an extension's own source file replaces the inherited tree; edits to a borrowed render merge into its tree."),
1812
1812
  defaultProps: Style.optional(),
1813
1813
  /** The defaults that hold only under a condition on the component's other props — see
1814
- * `conditionalDefault`. */
1814
+ * `.defaultProps([{ when, props }])`. */
1815
1815
  defaultPropsWhen: orList(DefaultCondition).optional().describe(DEFAULT_CONDITIONS_DESCRIPTION),
1816
- /** What the tile and the matrix render with — see `preview({ defaultProps, when })`. Read off
1816
+ /** What the tile and the matrix render with — see `.preview({ defaultProps })`. Read off
1817
1817
  * `.preview()` at build, and written per cell from the matrix. */
1818
1818
  preview: orList(Preview).optional().describe(PREVIEW_DESCRIPTION),
1819
1819
  /**
@@ -2989,7 +2989,7 @@ function componentRef(path) {
2989
2989
  assertComponentSourceInput(delta);
2990
2990
  const extended = base.extend({
2991
2991
  ...delta,
2992
- ...storedDefaults(delta)
2992
+ ...delta.defaultProps === void 0 ? {} : storedDefaults(delta.defaultProps)
2993
2993
  });
2994
2994
  Object.defineProperty(extended, "render", {
2995
2995
  enumerable: false,
@@ -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
@@ -3,7 +3,7 @@ import { RegisteredComponentHandles, RegisteredGroupPaths, RegisteredModifiers,
3
3
  import { Authored } from "../../framework/authoring.js";
4
4
  import { StyleValue } from "./style-bag.js";
5
5
  import { ComponentSourceInput } from "./component-source-fields.js";
6
- import { ConditionalDefault } from "./conditional-defaults.js";
6
+ import { PropsOrRules } from "./prop-rules.js";
7
7
  import { VariantValues } from "./Component.js";
8
8
  import { AllHTMLAttributes, ComponentPropsWithRef, ComponentType, ElementType, JSX, ReactElement, ReactNode, Ref } from "react";
9
9
 
@@ -664,11 +664,17 @@ type ForwardedPropsOf<L extends LayerMap, F extends string> = [NonRootForward<L,
664
664
  type DefaultsOf<P extends PropMap> = Partial<DeclaredProps<P>> & {
665
665
  [key: string]: unknown;
666
666
  };
667
- /** {@link DefaultsOf} as `.defaultProps` takes it: a declared prop other than `as` may also default
668
- * through {@link conditionalDefault}. */
669
- type AuthoredDefaultsOf<P extends PropMap> = { [K in keyof DeclaredProps<P>]?: K extends 'as' ? DeclaredProps<P>[K] : DeclaredProps<P>[K] | ConditionalDefault<DeclaredProps<P>[K]> } & {
670
- [key: string]: unknown;
671
- };
667
+ /** {@link DefaultsOf} as `.defaultProps` and `.extend({ defaultProps })` take it: the defaults, or
668
+ * a list of rules giving them under conditions. */
669
+ type AuthoredDefaultsOf<P extends PropMap> = PropsOrRules<DefaultsOf<P>>;
670
+ /** The defaults a component applies with nothing set: a record as given, or its unconditional
671
+ * rules folded together. */
672
+ type UnconditionalDefaults<D> = D extends readonly (infer Rule)[] ? [UnconditionalProps<Rule>] extends [never] ? Record<never, never> : UnionToIntersection<UnconditionalProps<Rule>> : D;
673
+ type UnconditionalProps<Rule> = Rule extends {
674
+ when: Readonly<Record<string, unknown>>;
675
+ } ? never : Rule extends {
676
+ props: infer Props;
677
+ } ? Props : never;
672
678
  /**
673
679
  * `T` without the keys in `K` — `Omit` that leaves an index signature alone.
674
680
  *
@@ -785,10 +791,11 @@ F extends string = never, /** The states `.states()` declared — what the rende
785
791
  * `const`, so an `as` keeps its literal type: without it `{ as: 'input' }` widens to `string` and
786
792
  * there's no tag left to resolve the element's props against.
787
793
  *
788
- * A default that depends on another prop is a `conditionalDefault`:
789
- * `includeScrim: conditionalDefault(true, [{ when: { modal: false }, value: false }])`.
794
+ * Defaults that depend on other props are rules, the way style rules are: a rule without `when`
795
+ * holds always, and a later rule wins on the props it sets.
796
+ * `[{ props: { includeScrim: true } }, { when: { modal: false }, props: { includeScrim: false } }]`.
790
797
  */
791
- defaultProps<const D2 extends AuthoredDefaultsOf<P>>(defaults: D2): ComponentBuilder<L, P, E, D2, F, S>;
798
+ defaultProps<const D2 extends AuthoredDefaultsOf<P>>(defaults: D2): ComponentBuilder<L, P, E, UnconditionalDefaults<D2>, F, S>;
792
799
  /**
793
800
  * The layers whose element props this component ALSO exposes, keyed by layer.
794
801
  *
@@ -832,34 +839,23 @@ F extends string = never, /** The states `.states()` declared — what the rende
832
839
  */
833
840
  renderCanvas<E2 extends E = E>(fn: (args: Without<RenderArgs<L, P, S>, keyof E2> & E2) => unknown): ComponentBuilder<L, P, E, D, F, S>;
834
841
  /**
835
- * What a preview of this component STARTS from — the props a single tile is rendered with.
842
+ * What a preview tile renders with. `defaultProps` takes what `.defaultProps` does: a record, or a
843
+ * list of rules.
836
844
  *
837
- * Only `defaultProps`, because the grid itself is derived: every axis a matrix could sweep is
838
- * already declared in `styles`, as the conditions the rules apply under, and the modifier columns
839
- * are the `_hover`/`_active` blocks those rules contain. An authored matrix restated all of it and
840
- * could disagree with it — a variant added to `styles` and forgotten in the matrix simply never
841
- * appeared. See `previewMatrix`.
845
+ * Only content, because the grid itself is derived: every axis a matrix could sweep is declared in
846
+ * `styles`, as the conditions the rules apply under, and the modifier columns are the
847
+ * `_hover`/`_active` blocks those rules contain. See `previewMatrix`.
848
+ *
849
+ * A rule with `when` gives the content a cell needs at one point on an axis:
850
+ * `{ when: { type: 'image' }, props: { src } }` gives the image kind a photo. A condition naming a
851
+ * prop no rule styles makes that prop an axis too, with the value named.
842
852
  */
843
853
  preview(spec: {
844
- defaultProps?: DefaultsOf<P>;
845
- /**
846
- * Preview props that hold only under a condition — the same `when` grammar a style rule uses.
847
- * The grid is derived from the styles; what it cannot derive is the CONTENT a cell needs at a
848
- * given point on an axis: `{ when: { type: 'image' }, props: { src } }` gives the image kind a
849
- * photo, `{ when: { invalid: true }, props: { selected: true } }` shows the invalid state checked.
850
- * A condition naming a prop no rule styles makes that prop an axis too, with the value named —
851
- * so a preview can say a boolean is worth a column the way a rule would.
852
- */
853
- when?: readonly PreviewCondition<P>[];
854
+ defaultProps?: PropsOrRules<DefaultsOf<P>>;
854
855
  }): ComponentBuilder<L, P, E, D, F, S>;
855
856
  toStored(): Record<string, unknown>;
856
857
  assignPath(path: string): void;
857
858
  }
858
- /** Preview props that hold under a condition on the cell's props — see `preview({ when })`. */
859
- interface PreviewCondition<P extends PropMap = PropMap> {
860
- readonly when: Partial<Record<keyof P & string, unknown>>;
861
- readonly props: DefaultsOf<P>;
862
- }
863
859
  /** A component under authorship, with its declarations forgotten — what a registry holds. */
864
860
  type AuthoredComponent = ComponentBuilder<LayerMap, PropMap, unknown, unknown>;
865
861
  /**
@@ -901,4 +897,4 @@ interface DefineComponent {
901
897
  declare function definePropsFirst<const P extends PropMap<LayerPath>>(props: P & NoInfer<{ [K in keyof P]: Exact<P[K], AuthoredProp<LayerPath>> }>): ComponentBuilder<LayerMap, P>;
902
898
  declare const defineComponent: DefineComponent;
903
899
  //#endregion
904
- export { AsInput, AuthoredComponent, AuthoredDefaultsOf, AuthoredProp, ComponentBuilder, ComponentPropsOf, GlyphComponent, GlyphValue, LayerElement, LayerMap, LinkedHandle, ModifierProps, PhantomCallProps, PhantomReadProps, PropCondition, PropDecl, PropMap, RegisteredMembers, RenderArgs, SlotCodeForm, SlotDecl, StateMap, StyleProps, StyleRuleInput, defineComponent };
900
+ export { AsInput, AuthoredComponent, AuthoredDefaultsOf, AuthoredProp, ComponentBuilder, ComponentPropsOf, GlyphComponent, GlyphValue, LayerElement, LayerMap, LinkedHandle, ModifierProps, PhantomCallProps, PhantomReadProps, PropCondition, PropDecl, PropMap, RegisteredMembers, RenderArgs, SlotCodeForm, SlotDecl, StateMap, StyleProps, StyleRuleInput, UnconditionalDefaults, defineComponent };
@@ -1,8 +1,7 @@
1
- import { canonicalCondition, isRef, kindOf, memberRef, pathOf } from "../../framework/utils/refs.js";
1
+ import { isRef, kindOf, memberRef, pathOf } from "../../framework/utils/refs.js";
2
2
  import { glyphIdentityOf, glyphPackageRefOf, isComponentType } from "./glyph-identity.js";
3
- import "../../framework/preview-store.js";
4
3
  import { assertComponentSourceInput } from "./component-source-fields.js";
5
- import { isConditionalDefault, storedDefaults } from "./conditional-defaults.js";
4
+ import { keyedPropRules, storedDefaults } from "./prop-rules.js";
6
5
  import { Component, layerRef, storedStyleRules } from "./Component.js";
7
6
  //#region src/entities/system/defineComponent.ts
8
7
  function build(body) {
@@ -25,10 +24,7 @@ function build(body) {
25
24
  metadata: (meta) => next(meta),
26
25
  render: () => handle,
27
26
  renderCanvas: () => handle,
28
- preview: (spec) => {
29
- if (spec?.defaultProps === void 0 && !spec?.when?.length) return handle;
30
- return next({ preview: spec });
31
- },
27
+ preview: (spec) => spec?.defaultProps === void 0 ? handle : next({ preview: spec.defaultProps }),
32
28
  toStored: () => toStored(body),
33
29
  assignPath(path) {
34
30
  handle.path = path;
@@ -146,7 +142,6 @@ function storableElement(value) {
146
142
  */
147
143
  function storableValue(value) {
148
144
  if (value === null) return null;
149
- if (isConditionalDefault(value)) throw new Error("`conditionalDefault` is a default, so it goes in `.defaultProps` or `.extend({ defaultProps })`. A preview conditions its props with `.preview({ when: [{ when, props }] })`.");
150
145
  const type = typeof value;
151
146
  if (type === "string" || type === "number" || type === "boolean") return value;
152
147
  if (isComponentType(value)) return storableComponentRef(value);
@@ -376,21 +371,19 @@ function toStored(body) {
376
371
  assertComponentSourceInput(body);
377
372
  const stored = { ...body };
378
373
  if (body.preview !== void 0) {
379
- const authored = body.preview;
380
374
  const keyed = {};
381
- const defaults = authored.defaultProps !== void 0 ? storableProps(authored.defaultProps) : void 0;
382
- if (defaults !== void 0) keyed["*"] = {
383
- when: null,
384
- props: defaults
385
- };
386
- for (const { when, props } of authored.when ?? []) keyed[canonicalCondition(when)] = {
387
- when,
388
- props: storableProps(props) ?? {}
389
- };
375
+ for (const [key, { when, props }] of keyedPropRules(body.preview)) {
376
+ const storable = storableProps(props);
377
+ if (storable === void 0 && when === null) continue;
378
+ keyed[key] = {
379
+ when,
380
+ props: storable ?? {}
381
+ };
382
+ }
390
383
  if (Object.keys(keyed).length === 0) delete stored.preview;
391
384
  else stored.preview = keyed;
392
385
  }
393
- Object.assign(stored, storedDefaults(body));
386
+ if (body.defaultProps !== void 0) Object.assign(stored, storedDefaults(body.defaultProps));
394
387
  if (body.props && typeof body.props === "object") {
395
388
  const props = {};
396
389
  for (const [name, decl] of Object.entries(body.props)) props[name] = storedProp(decl, body.layers);
@@ -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
@@ -0,0 +1,10 @@
1
+ //#region src/entities/system/prop-rules.d.ts
2
+ /** One rule: `props` hold while every prop `when` names has its value, or always with no `when`. */
3
+ interface PropRule<Props = Record<string, unknown>> {
4
+ readonly when?: Readonly<Record<string, unknown>> | null;
5
+ readonly props: Props;
6
+ }
7
+ /** The props given unconditionally, or a list of rules. */
8
+ type PropsOrRules<Props> = Props | readonly PropRule<Props>[];
9
+ //#endregion
10
+ export { PropRule, PropsOrRules };
@@ -0,0 +1,42 @@
1
+ import { canonicalCondition } from "../../framework/utils/refs.js";
2
+ //#region src/entities/system/prop-rules.ts
3
+ /**
4
+ * Rules keyed by condition, `*` for the unconditional one. Two rules with the same condition are
5
+ * one: their props merge, the later value winning, and the merged rule takes the later one's place.
6
+ */
7
+ function keyedPropRules(authored) {
8
+ const rules = Array.isArray(authored) ? authored : [{ props: authored }];
9
+ const keyed = /* @__PURE__ */ new Map();
10
+ for (const rule of rules) {
11
+ const key = canonicalCondition(rule.when);
12
+ const prior = keyed.get(key);
13
+ keyed.delete(key);
14
+ keyed.set(key, {
15
+ when: key === "*" ? null : { ...rule.when },
16
+ props: {
17
+ ...prior?.props,
18
+ ...rule.props
19
+ }
20
+ });
21
+ }
22
+ return keyed;
23
+ }
24
+ /**
25
+ * Authored defaults as stored: the unconditional rule in `defaultProps`, each other in
26
+ * `defaultPropsWhen`. A plain record stores as it was written, with no `defaultPropsWhen`.
27
+ */
28
+ function storedDefaults(authored) {
29
+ if (!Array.isArray(authored)) return { defaultProps: { ...authored } };
30
+ const keyed = keyedPropRules(authored);
31
+ const defaultPropsWhen = {};
32
+ for (const [key, { when, props }] of keyed) if (when) defaultPropsWhen[key] = {
33
+ when,
34
+ props
35
+ };
36
+ return {
37
+ defaultProps: keyed.get("*")?.props ?? {},
38
+ ...Object.keys(defaultPropsWhen).length > 0 ? { defaultPropsWhen } : {}
39
+ };
40
+ }
41
+ //#endregion
42
+ export { keyedPropRules, storedDefaults };
@@ -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.