@yahoo/uds-create-config 5.69.0 → 5.71.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.
- package/dist/configs/system.js +4 -0
- package/dist/entities/system/Component.d.ts +10 -4
- package/dist/entities/system/Component.js +22 -12
- package/dist/entities/system/LinkedSystem.d.ts +2 -0
- package/dist/entities/system/LinkedSystem.js +14 -4
- package/dist/entities/system/link-plan.d.ts +34 -1
- package/dist/entities/system/link-plan.js +157 -2
- package/dist/framework/Config.d.ts +1 -1
- package/dist/framework/Config.js +1 -1
- package/dist/framework/component-api-changes.d.ts +14 -1
- package/dist/framework/component-api-changes.js +22 -6
- package/dist/framework/schema-version.d.ts +1 -1
- package/dist/framework/schema-version.js +5 -5
- package/dist/index.d.ts +2 -2
- package/dist/migrations/20261102185610_link_replace.d.ts +7 -0
- package/dist/migrations/20261102185610_link_replace.js +8 -0
- package/dist/migrations/20261102193912_map_props_from.d.ts +12 -0
- package/dist/migrations/20261102193912_map_props_from.js +10 -0
- package/dist/spec/index.d.ts +2 -2
- package/dist/spec/index.js +2 -2
- package/dist/spec/prop-mapping.d.ts +34 -2
- package/dist/spec/prop-mapping.js +47 -10
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
package/dist/configs/system.js
CHANGED
|
@@ -36,6 +36,8 @@ import { lintIgnoreMigration } from "../migrations/20261027205147_lint_ignore.js
|
|
|
36
36
|
import { componentMapPropsMigration } from "../migrations/20261030134149_component_map_props.js";
|
|
37
37
|
import { layerPrefixMigration } from "../migrations/20261030163028_layer_prefix.js";
|
|
38
38
|
import { defaultColorsMigration } from "../migrations/20261102143821_default_colors.js";
|
|
39
|
+
import { linkReplaceMigration } from "../migrations/20261102185610_link_replace.js";
|
|
40
|
+
import { mapPropsFromMigration } from "../migrations/20261102193912_map_props_from.js";
|
|
39
41
|
import { CanvasRole } from "../entities/system/CanvasRole.js";
|
|
40
42
|
import { GlobalStyle } from "../entities/system/GlobalStyle.js";
|
|
41
43
|
import { Guidance } from "../entities/system/Guidance.js";
|
|
@@ -205,5 +207,7 @@ if (!registeredSchemaMigrations().has(20261027205147)) registerSchemaMigrations(
|
|
|
205
207
|
if (!registeredSchemaMigrations().has(20261030134149)) registerSchemaMigrations(componentMapPropsMigration);
|
|
206
208
|
if (!registeredSchemaMigrations().has(20261030163028)) registerSchemaMigrations(layerPrefixMigration);
|
|
207
209
|
if (!registeredSchemaMigrations().has(20261102143821)) registerSchemaMigrations(defaultColorsMigration);
|
|
210
|
+
if (!registeredSchemaMigrations().has(20261102185610)) registerSchemaMigrations(linkReplaceMigration);
|
|
211
|
+
if (!registeredSchemaMigrations().has(20261102193912)) registerSchemaMigrations(mapPropsFromMigration);
|
|
208
212
|
//#endregion
|
|
209
213
|
export { System };
|
|
@@ -873,11 +873,16 @@ declare const ComponentEntity: EntityClass<z.ZodObject<{
|
|
|
873
873
|
readonly input: z.ZodObject<{
|
|
874
874
|
path: z.ZodType<string, string | undefined, z.core.$ZodTypeInternals<string, string | undefined>>;
|
|
875
875
|
data: z.ZodObject<{
|
|
876
|
-
|
|
876
|
+
from: z.ZodOptional<z.ZodString>;
|
|
877
|
+
only: z.ZodOptional<z.ZodObject<{
|
|
878
|
+
canvas: z.ZodString;
|
|
879
|
+
nodes: z.ZodArray<z.ZodString>;
|
|
880
|
+
}, z.core.$strip>>;
|
|
881
|
+
rules: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
877
882
|
when: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
|
|
878
883
|
set: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
879
884
|
unset: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
880
|
-
}, z.core.$strip
|
|
885
|
+
}, z.core.$strip>>>;
|
|
881
886
|
}, z.core.$strip>;
|
|
882
887
|
}, z.core.$strip>;
|
|
883
888
|
};
|
|
@@ -885,11 +890,12 @@ declare const ComponentEntity: EntityClass<z.ZodObject<{
|
|
|
885
890
|
readonly input: z.ZodObject<{
|
|
886
891
|
path: z.ZodType<string, string | undefined, z.core.$ZodTypeInternals<string, string | undefined>>;
|
|
887
892
|
data: z.ZodObject<{
|
|
888
|
-
|
|
893
|
+
from: z.ZodOptional<z.ZodString>;
|
|
894
|
+
rules: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
889
895
|
when: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
|
|
890
896
|
set: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
891
897
|
unset: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
892
|
-
}, z.core.$strip
|
|
898
|
+
}, z.core.$strip>>>;
|
|
893
899
|
instances: z.ZodDefault<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
|
|
894
900
|
}, z.core.$strip>;
|
|
895
901
|
}, z.core.$strip>;
|
|
@@ -24,7 +24,7 @@ import { slotAnatomyPropOf, slotTargetsOf, withoutTruthyTerm } from "../../frame
|
|
|
24
24
|
import { listSlots, namedSlots, slotHostedBy, slotOf, slotsFor } from "../../spec/slot.js";
|
|
25
25
|
import { cellAt } from "../../spec/cell.js";
|
|
26
26
|
import { instanceAt } from "../../spec/instance.js";
|
|
27
|
-
import { mapComponentProps,
|
|
27
|
+
import { mapComponentProps, mapPropsRulesSchema, mappingFromSchema, mappingScopeSchema, propMappingErrors } from "../../spec/prop-mapping.js";
|
|
28
28
|
import { forwardOptions, forwardsWrite } from "./component-forwards.js";
|
|
29
29
|
import { assertComponentSourceInput } from "./component-source-fields.js";
|
|
30
30
|
import { checkStyleSet, styleSetEdits } from "./component-style-set.js";
|
|
@@ -2947,12 +2947,16 @@ element: refSchema })
|
|
|
2947
2947
|
"map-props": {
|
|
2948
2948
|
input: z.object({
|
|
2949
2949
|
path: pathInput("component"),
|
|
2950
|
-
data: z.object({
|
|
2950
|
+
data: z.object({
|
|
2951
|
+
from: mappingFromSchema.optional(),
|
|
2952
|
+
only: mappingScopeSchema.optional(),
|
|
2953
|
+
rules: mapPropsRulesSchema
|
|
2954
|
+
})
|
|
2951
2955
|
}),
|
|
2952
2956
|
readOnly: false,
|
|
2953
2957
|
approval: "reference-change",
|
|
2954
2958
|
label: "Map props",
|
|
2955
|
-
description: "Move a component's instances onto its props as they are now, after its API changed: `variant: 'primary'` becomes `color: 'brand', prominence: 'solid'`. It changes nothing in the config. Where it runs decides what it moves: in Create, the instances in the branch's design files; in a checkout (`uds config edit`, the local UDS MCP), the call sites in the project's source. Refuses a rule that writes a prop or value the component doesn't take. Run it with `plan: true` first: the plan says what it would move, and writes nothing.",
|
|
2959
|
+
description: "Move a component's instances onto its props as they are now, after its API changed: `variant: 'primary'` becomes `color: 'brand', prominence: 'solid'`. It changes nothing in the config. Where it runs decides what it moves: in Create, the instances in the branch's design files; in a checkout (`uds config edit`, the local UDS MCP), the call sites in the project's source. With `from`, it swaps the component too: instances of `from`, a component the system may no longer have, become this component's, and the rules move their props onto it (`when: {}` reaches every swapped instance). Refuses a rule that writes a prop or value the component doesn't take. Run it with `plan: true` first: the plan says what it would move, and writes nothing.",
|
|
2956
2960
|
scope: "item",
|
|
2957
2961
|
title: (entity) => `Map ${entity} props`,
|
|
2958
2962
|
example: ({ config, path }) => {
|
|
@@ -2963,10 +2967,7 @@ element: refSchema })
|
|
|
2963
2967
|
};
|
|
2964
2968
|
},
|
|
2965
2969
|
handler: (input, config) => {
|
|
2966
|
-
const errors = propMappingErrors(config,
|
|
2967
|
-
component: String(input.path),
|
|
2968
|
-
rules: input.data.rules
|
|
2969
|
-
});
|
|
2970
|
+
const errors = propMappingErrors(config, mappingOf(String(input.path), input.data));
|
|
2970
2971
|
if (errors.length > 0) rejectData(errors.join(" "));
|
|
2971
2972
|
return config;
|
|
2972
2973
|
}
|
|
@@ -2975,7 +2976,8 @@ element: refSchema })
|
|
|
2975
2976
|
input: z.object({
|
|
2976
2977
|
path: pathInput("component"),
|
|
2977
2978
|
data: z.object({
|
|
2978
|
-
|
|
2979
|
+
from: mappingFromSchema.optional(),
|
|
2980
|
+
rules: mapPropsRulesSchema,
|
|
2979
2981
|
instances: z.array(z.record(z.string(), z.unknown())).default([]).describe("Instances' props as they are stored or written: each comes back with the rules applied.")
|
|
2980
2982
|
})
|
|
2981
2983
|
}),
|
|
@@ -2987,10 +2989,7 @@ element: refSchema })
|
|
|
2987
2989
|
example: ({ config, path }) => exampleMapping(config, path),
|
|
2988
2990
|
handler: (input, config) => {
|
|
2989
2991
|
const data = input.data;
|
|
2990
|
-
return mapComponentProps(config,
|
|
2991
|
-
component: String(input.path),
|
|
2992
|
-
rules: data.rules
|
|
2993
|
-
}, data.instances ?? []);
|
|
2992
|
+
return mapComponentProps(config, mappingOf(String(input.path), input.data), data.instances ?? []);
|
|
2994
2993
|
}
|
|
2995
2994
|
}
|
|
2996
2995
|
},
|
|
@@ -3140,6 +3139,17 @@ function exampleProps(config, componentPath) {
|
|
|
3140
3139
|
}
|
|
3141
3140
|
return {};
|
|
3142
3141
|
}
|
|
3142
|
+
/** The mapping a `map-props` op's input names, refused when a mapping without `from` has no rule. */
|
|
3143
|
+
function mappingOf(component, data) {
|
|
3144
|
+
const { from, only, rules = [] } = data ?? {};
|
|
3145
|
+
if (from === void 0 && rules.length === 0) rejectData("Name at least one rule in `rules`, or `from` to swap another component's instances onto this one.");
|
|
3146
|
+
return {
|
|
3147
|
+
component,
|
|
3148
|
+
...from === void 0 ? {} : { from: from.replace(/^component:/, "") },
|
|
3149
|
+
...only === void 0 ? {} : { only },
|
|
3150
|
+
rules
|
|
3151
|
+
};
|
|
3152
|
+
}
|
|
3143
3153
|
/** A runnable `map-props-plan` example: an instance holding the first variant prop's first value, and
|
|
3144
3154
|
* a rule moving it onto the prop's second value (or the same one, for a prop with a single value).
|
|
3145
3155
|
* A component with no variant gets the other common case, dropping a prop it no longer has.
|
|
@@ -123,6 +123,7 @@ declare const LinkedSystemDefinition: EntityClass<z.ZodObject<{
|
|
|
123
123
|
keep: "keep";
|
|
124
124
|
take: "take";
|
|
125
125
|
}>>;
|
|
126
|
+
replace: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>>>;
|
|
126
127
|
}, z.core.$strip>;
|
|
127
128
|
};
|
|
128
129
|
readonly 'link-plan': {
|
|
@@ -133,6 +134,7 @@ declare const LinkedSystemDefinition: EntityClass<z.ZodObject<{
|
|
|
133
134
|
keep: "keep";
|
|
134
135
|
take: "take";
|
|
135
136
|
}>>;
|
|
137
|
+
replace: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>>>;
|
|
136
138
|
}, z.core.$strip>;
|
|
137
139
|
};
|
|
138
140
|
readonly unlink: {
|
|
@@ -171,7 +171,16 @@ const linkInput = z.object({
|
|
|
171
171
|
data: z.union([z.literal("*"), z.record(z.string(), copyPaths)]),
|
|
172
172
|
/** What to do at a path another linked system provides: `'take'` makes it follow this source,
|
|
173
173
|
* `'keep'` leaves it where it comes from. Required when there is one; `link-plan` lists them. */
|
|
174
|
-
onLinked: z.enum(["keep", "take"]).optional()
|
|
174
|
+
onLinked: z.enum(["keep", "take"]).optional(),
|
|
175
|
+
/**
|
|
176
|
+
* A component this config holds under one name, replaced with the source's of another name, keyed
|
|
177
|
+
* by accessor: `{ components: { SelectContent: 'SelectMenu' } }` replaces the `SelectContent` this
|
|
178
|
+
* config holds with the source's `SelectMenu`. Every ref to `SelectContent` is renamed to
|
|
179
|
+
* `SelectMenu`, so design files and code follow the way they follow a rename, and the system
|
|
180
|
+
* `SelectContent` came through stops deriving it unless its own rows still use it. Each source path
|
|
181
|
+
* is one `data` names; `link-plan` lists each in `replaced`. A take at the same name is `onLinked`.
|
|
182
|
+
*/
|
|
183
|
+
replace: z.record(z.string(), z.record(z.string().min(1), z.string().min(1))).optional()
|
|
175
184
|
});
|
|
176
185
|
const unlinkInput = z.object({
|
|
177
186
|
path: pathInput("linked-system"),
|
|
@@ -216,7 +225,8 @@ function linkPlanInputOf(input) {
|
|
|
216
225
|
return {
|
|
217
226
|
path: String(input.path),
|
|
218
227
|
data: input.data,
|
|
219
|
-
...input.onLinked === void 0 ? {} : { onLinked: input.onLinked }
|
|
228
|
+
...input.onLinked === void 0 ? {} : { onLinked: input.onLinked },
|
|
229
|
+
...input.replace === void 0 ? {} : { replace: input.replace }
|
|
220
230
|
};
|
|
221
231
|
}
|
|
222
232
|
/** A `link` or `unlink` example: one entity the source offers, under the accessor this config files it by. */
|
|
@@ -310,7 +320,7 @@ const LinkedSystemDefinition = defineEntity({
|
|
|
310
320
|
input: linkInput,
|
|
311
321
|
readOnly: false,
|
|
312
322
|
label: "Link from",
|
|
313
|
-
description: "Draw entities from a linked system, following the source: `data` names them by accessor — `{ tokens: ['color/brand', 'color'] }` takes an entity and a folder's members, `{ tokens: '*' }` every token, and `'*'` the whole system. What this config already holds at those paths is replaced by theirs, so run `linked-system/link-plan` with the same input first to see what that is. To hold their values as your own instead, `copy`.",
|
|
323
|
+
description: "Draw entities from a linked system, following the source: `data` names them by accessor — `{ tokens: ['color/brand', 'color'] }` takes an entity and a folder's members, `{ tokens: '*' }` every token, and `'*'` the whole system. What this config already holds at those paths is replaced by theirs, so run `linked-system/link-plan` with the same input first to see what that is. To replace something this config holds under another name (the `SelectContent` it has with the source's `SelectMenu`), name the pair in `replace` (`{ components: { SelectContent: 'SelectMenu' } }`): every reference to the old name moves to the new one, in design files and code too. To hold their values as your own instead, `copy`.",
|
|
314
324
|
scope: "item",
|
|
315
325
|
approval: "reference-change",
|
|
316
326
|
title: (entity) => `Link something from ${entity}`,
|
|
@@ -333,7 +343,7 @@ const LinkedSystemDefinition = defineEntity({
|
|
|
333
343
|
input: linkInput,
|
|
334
344
|
readOnly: true,
|
|
335
345
|
label: "Plan a link",
|
|
336
|
-
description: "What `link` with the same input replaces — rows of yours deleted so the source shows through, every path another linked system provides (`linked`, turned into extensions when `onLinked` says take), the `options` entries another linked system holds at what a take moves, which it removes since they would apply to nothing (`dropped`), members left alone because they are already here from the same origin, paths another linked system declares that the link reaches as a different entity (`settled`), whose row stays, and why the config would refuse the link (`blocked`), a collision it would leave among them. Writes nothing.",
|
|
346
|
+
description: "What `link` with the same input replaces — rows of yours deleted so the source shows through, every path another linked system provides (`linked`, turned into extensions when `onLinked` says take), the `options` entries another linked system holds at what a take moves, which it removes since they would apply to nothing (`dropped`), members left alone because they are already here from the same origin, paths another linked system declares that the link reaches as a different entity (`settled`), whose row stays, what `replace` replaces under another name (`replaced`: each path, what replaces it, the pin it came through, and whether it is left out of that pin or kept because rows of that pin still use it), and why the config would refuse the link (`blocked`), a collision it would leave among them. Writes nothing.",
|
|
337
347
|
scope: "item",
|
|
338
348
|
title: (entity) => `Plan a link from ${entity}`,
|
|
339
349
|
example: linkExample,
|
|
@@ -9,12 +9,22 @@ type LinkPaths = '*' | Readonly<Record<string, '*' | readonly string[]>>;
|
|
|
9
9
|
/** What a link does at a path held through another linked system: `'take'` makes the row follow this
|
|
10
10
|
* source, `'keep'` leaves it with the system it comes from. */
|
|
11
11
|
type OnLinked = 'keep' | 'take';
|
|
12
|
+
/** Per accessor, a path this config holds → the source's path that replaces it under another name. */
|
|
13
|
+
type LinkReplacements = Readonly<Record<string, Readonly<Record<string, string>>>>;
|
|
12
14
|
interface LinkPlanInput {
|
|
13
15
|
/** The pin's slug. */
|
|
14
16
|
readonly path: string;
|
|
15
17
|
readonly data: LinkPaths;
|
|
16
18
|
/** Planned as `'take'` when omitted; the `link` op refuses without it while `linked` is not empty. */
|
|
17
19
|
readonly onLinked?: OnLinked;
|
|
20
|
+
/**
|
|
21
|
+
* What this config holds under one name that the source's entity replaces under another:
|
|
22
|
+
* `{ components: { SelectContent: 'SelectMenu' } }` replaces the `SelectContent` this config holds
|
|
23
|
+
* with the source's `SelectMenu`, and everything that pointed at `SelectContent` points at
|
|
24
|
+
* `SelectMenu` after it. Each source path is one `data` names. A take at the same path is
|
|
25
|
+
* `onLinked: 'take'`, not this.
|
|
26
|
+
*/
|
|
27
|
+
readonly replace?: LinkReplacements;
|
|
18
28
|
}
|
|
19
29
|
/** A path the link reaches that another linked system provides, or declares under a row of this
|
|
20
30
|
* config's own. */
|
|
@@ -40,6 +50,22 @@ interface StrandedDirective extends DroppedDirective {
|
|
|
40
50
|
interface LinkArrival extends EntityAddress {
|
|
41
51
|
readonly named: boolean;
|
|
42
52
|
}
|
|
53
|
+
/** A row this config held that a `replace` entry replaces with the source's entity of another name.
|
|
54
|
+
* `path` is the row's; every ref to it is renamed to `with`. */
|
|
55
|
+
interface LinkReplacement extends EntityAddress {
|
|
56
|
+
/** The source's path that replaces it, which is where the row lands. */
|
|
57
|
+
readonly with: string;
|
|
58
|
+
/** The slug of the pin the replaced row came through; absent for a row of this config's own. */
|
|
59
|
+
readonly from?: string;
|
|
60
|
+
/** Whether the replaced path was left out of that pin, so it no longer derives here. */
|
|
61
|
+
readonly excluded: boolean;
|
|
62
|
+
/** That pin's own rows still drawing on the replaced path, which keep it held. */
|
|
63
|
+
readonly keptFor?: readonly EntityAddress[];
|
|
64
|
+
/** Why the replaced path couldn't be left out of that pin, when it couldn't. */
|
|
65
|
+
readonly keptBecause?: string;
|
|
66
|
+
/** That pin's `options` entries at the replaced path, removed with it. */
|
|
67
|
+
readonly dropped: readonly EntityAddress[];
|
|
68
|
+
}
|
|
43
69
|
interface LinkPlan {
|
|
44
70
|
/** The pin's `link` once the op has run. */
|
|
45
71
|
readonly declared: LinkDeclaration | undefined;
|
|
@@ -53,6 +79,8 @@ interface LinkPlan {
|
|
|
53
79
|
readonly linked: readonly LinkedElsewhere[];
|
|
54
80
|
/** Rows held through another pin, turned into extensions through this one. */
|
|
55
81
|
readonly extended: readonly EntityAddress[];
|
|
82
|
+
/** What `replace` replaces, each with the source's entity it's replaced with. */
|
|
83
|
+
readonly replaced: readonly LinkReplacement[];
|
|
56
84
|
/** The other pins' `options` entries at what the take moves, removed with it: they adjust only the
|
|
57
85
|
* rows their pin derives, and the row there follows this one now. Not carried onto the taken row,
|
|
58
86
|
* since an entry corrects what that pin brings rather than stating a value of this config's own. */
|
|
@@ -156,6 +184,11 @@ interface SourceRowState {
|
|
|
156
184
|
* that leaves a referrer dangling until the declaration covers the path. A member
|
|
157
185
|
* already here from the source's own origin is left alone and kept out of the declaration, so a
|
|
158
186
|
* folder that holds one is declared by its other members.
|
|
187
|
+
*
|
|
188
|
+
* A `replace` entry replaces a row this config holds with the source's entity of another name: that
|
|
189
|
+
* row becomes an extension of the source's, is renamed to the source's path so every ref follows
|
|
190
|
+
* (`replacementTakes`), and is left out of the pin it came through unless that pin's own rows still
|
|
191
|
+
* draw on it.
|
|
159
192
|
*/
|
|
160
193
|
declare function planLink({
|
|
161
194
|
config,
|
|
@@ -214,4 +247,4 @@ declare function sourceRowStates({
|
|
|
214
247
|
readonly slug: string;
|
|
215
248
|
}): SourceRowState[];
|
|
216
249
|
//#endregion
|
|
217
|
-
export { BreakOutcome, BreakPlan, BreakPlanInput, BreakPlanRow, BreakReach, DroppedDirective, LinkArrival, LinkPaths, LinkPlan, LinkPlanInput, LinkedElsewhere, OnLinked, SourceRowState, UnlinkPlan, UnlinkPlanInput, planBreak, planLink, planUnlink, sourceRowStates };
|
|
250
|
+
export { BreakOutcome, BreakPlan, BreakPlanInput, BreakPlanRow, BreakReach, DroppedDirective, LinkArrival, LinkPaths, LinkPlan, LinkPlanInput, LinkReplacement, LinkReplacements, LinkedElsewhere, OnLinked, SourceRowState, UnlinkPlan, UnlinkPlanInput, planBreak, planLink, planUnlink, sourceRowStates };
|
|
@@ -71,6 +71,11 @@ function declaredElsewhere({ config, slug, kind, path }) {
|
|
|
71
71
|
* that leaves a referrer dangling until the declaration covers the path. A member
|
|
72
72
|
* already here from the source's own origin is left alone and kept out of the declaration, so a
|
|
73
73
|
* folder that holds one is declared by its other members.
|
|
74
|
+
*
|
|
75
|
+
* A `replace` entry replaces a row this config holds with the source's entity of another name: that
|
|
76
|
+
* row becomes an extension of the source's, is renamed to the source's path so every ref follows
|
|
77
|
+
* (`replacementTakes`), and is left out of the pin it came through unless that pin's own rows still
|
|
78
|
+
* draw on it.
|
|
74
79
|
*/
|
|
75
80
|
function planLink({ config, input }) {
|
|
76
81
|
return planLinkApplied({
|
|
@@ -116,7 +121,15 @@ function planLinkApplied({ config, input }) {
|
|
|
116
121
|
/** Per accessor, what the declaration gains: the kind whole, or paths. */
|
|
117
122
|
const gains = /* @__PURE__ */ new Map();
|
|
118
123
|
let leftAnywhere = false;
|
|
124
|
+
const replacements = replacementTakes({
|
|
125
|
+
config,
|
|
126
|
+
slug,
|
|
127
|
+
source,
|
|
128
|
+
input
|
|
129
|
+
});
|
|
130
|
+
const landing = new Set(replacements.entries.map(({ kind, path }) => `${kind}:${path}`));
|
|
119
131
|
const shadow = (kind, path) => {
|
|
132
|
+
if (landing.has(`${kind}:${path}`)) return true;
|
|
120
133
|
const held = config.resolve(kind, path);
|
|
121
134
|
if (!held || held.borrowedFrom === slug) return true;
|
|
122
135
|
if (sharedWithOrigin({
|
|
@@ -261,12 +274,13 @@ function planLinkApplied({ config, input }) {
|
|
|
261
274
|
declared = Object.keys(next).length === 0 ? current : next;
|
|
262
275
|
}
|
|
263
276
|
const planned = [
|
|
277
|
+
...replacements.patches,
|
|
264
278
|
...takes,
|
|
265
279
|
...declares,
|
|
266
280
|
...shadows
|
|
267
281
|
];
|
|
268
282
|
let result = planned.length > 0 ? applied(config, planned) : void 0;
|
|
269
|
-
if (takes.length > 0 && result && "failed" in result) throw result.failed;
|
|
283
|
+
if ((takes.length > 0 || replacements.patches.length > 0) && result && "failed" in result) throw result.failed;
|
|
270
284
|
const { dropped, stranded, patches: drops } = takes.length > 0 && result && "config" in result ? directivesTakenFrom({
|
|
271
285
|
config,
|
|
272
286
|
slug,
|
|
@@ -277,7 +291,74 @@ function planLinkApplied({ config, input }) {
|
|
|
277
291
|
patches: []
|
|
278
292
|
};
|
|
279
293
|
if (drops.length > 0 && result && "config" in result) result = applied(result.config, drops);
|
|
280
|
-
const
|
|
294
|
+
const taken = [];
|
|
295
|
+
const leaves = [];
|
|
296
|
+
for (const entry of replacements.entries) {
|
|
297
|
+
const { kind, accessor, path, replaces, from } = entry;
|
|
298
|
+
if (from === void 0 || !result || !("config" in result)) {
|
|
299
|
+
taken.push({
|
|
300
|
+
kind,
|
|
301
|
+
path: replaces,
|
|
302
|
+
with: path,
|
|
303
|
+
...from ? { from } : {},
|
|
304
|
+
excluded: false,
|
|
305
|
+
dropped: []
|
|
306
|
+
});
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
const leftOut = { [accessor]: [replaces] };
|
|
310
|
+
const keptFor = linkedDependents({
|
|
311
|
+
config: result.config,
|
|
312
|
+
slug: from,
|
|
313
|
+
data: leftOut
|
|
314
|
+
});
|
|
315
|
+
if (keptFor.length > 0) {
|
|
316
|
+
taken.push({
|
|
317
|
+
kind,
|
|
318
|
+
path: replaces,
|
|
319
|
+
with: path,
|
|
320
|
+
from,
|
|
321
|
+
excluded: false,
|
|
322
|
+
keptFor,
|
|
323
|
+
dropped: []
|
|
324
|
+
});
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
const exclusion = planExclude({
|
|
328
|
+
config: result.config,
|
|
329
|
+
input: {
|
|
330
|
+
path: from,
|
|
331
|
+
data: leftOut
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
if (exclusion.blocked !== void 0) {
|
|
335
|
+
taken.push({
|
|
336
|
+
kind,
|
|
337
|
+
path: replaces,
|
|
338
|
+
with: path,
|
|
339
|
+
from,
|
|
340
|
+
excluded: false,
|
|
341
|
+
keptBecause: exclusion.blocked,
|
|
342
|
+
dropped: []
|
|
343
|
+
});
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
leaves.push(...exclusion.patches);
|
|
347
|
+
result = applied(result.config, exclusion.patches);
|
|
348
|
+
taken.push({
|
|
349
|
+
kind,
|
|
350
|
+
path: replaces,
|
|
351
|
+
with: path,
|
|
352
|
+
from,
|
|
353
|
+
excluded: true,
|
|
354
|
+
dropped: exclusion.dropped
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
const patches = [
|
|
358
|
+
...planned,
|
|
359
|
+
...drops,
|
|
360
|
+
...leaves
|
|
361
|
+
];
|
|
281
362
|
return {
|
|
282
363
|
plan: {
|
|
283
364
|
declared,
|
|
@@ -291,6 +372,7 @@ function planLinkApplied({ config, input }) {
|
|
|
291
372
|
addresses,
|
|
292
373
|
linked,
|
|
293
374
|
extended,
|
|
375
|
+
replaced: taken,
|
|
294
376
|
dropped,
|
|
295
377
|
shared,
|
|
296
378
|
patches,
|
|
@@ -303,6 +385,79 @@ function planLinkApplied({ config, input }) {
|
|
|
303
385
|
};
|
|
304
386
|
}
|
|
305
387
|
/**
|
|
388
|
+
* The edits that take each `replace` entry: the replaced row becomes this config's own extension of
|
|
389
|
+
* the source's entity, still at the old path, and is then renamed to the source's path. A rename is
|
|
390
|
+
* the edit that moves every ref, and the one design files and code follow, so the replaced name's
|
|
391
|
+
* instances arrive on the taken entity the way a rename's would. The config refuses to rename a row
|
|
392
|
+
* held under its source's own name, so the extension comes first: what it holds is then a name this
|
|
393
|
+
* config gave.
|
|
394
|
+
*/
|
|
395
|
+
function replacementTakes({ config, slug, source, input }) {
|
|
396
|
+
const entries = [];
|
|
397
|
+
const patches = [];
|
|
398
|
+
const accessors = config.accessors();
|
|
399
|
+
const landing = /* @__PURE__ */ new Map();
|
|
400
|
+
for (const [accessor, pairs] of Object.entries(input.replace ?? {})) {
|
|
401
|
+
const kind = accessors[accessor]?.kind;
|
|
402
|
+
if (!kind) rejectData(`Cannot replace anything through "${slug}" — this system has no "${accessor}".`, ["replace", accessor]);
|
|
403
|
+
for (const [replaces, path] of Object.entries(pairs)) {
|
|
404
|
+
const at = [
|
|
405
|
+
"replace",
|
|
406
|
+
accessor,
|
|
407
|
+
replaces
|
|
408
|
+
];
|
|
409
|
+
if (input.data !== "*" && !covers({
|
|
410
|
+
declared: input.data,
|
|
411
|
+
accessor,
|
|
412
|
+
path
|
|
413
|
+
})) rejectData(`Cannot replace "${replaces}" with "${path}" — \`data\` doesn't link "${path}" through "${slug}". Name it there too.`, at);
|
|
414
|
+
if (!source.resolve(kind, path)) rejectData(`Cannot replace "${replaces}" with "${path}" — "${slug}" has no ${kind} "${path}".`, at);
|
|
415
|
+
if (path === replaces) rejectData(`Cannot replace "${path}" with itself — taking the source's "${path}" over it is \`onLinked: 'take'\`.`, at);
|
|
416
|
+
const twice = landing.get(`${kind}:${path}`);
|
|
417
|
+
if (twice !== void 0) rejectData(`Cannot replace both "${twice}" and "${replaces}" with "${path}".`, at);
|
|
418
|
+
const held = config.resolve(kind, replaces);
|
|
419
|
+
if (!held) rejectData(`Cannot replace "${replaces}" — this system has no ${kind} "${replaces}".`, at);
|
|
420
|
+
if (held.borrowedFrom === slug) rejectData(`Cannot replace "${replaces}" with "${path}" — "${replaces}" already comes from "${slug}".`, at);
|
|
421
|
+
const occupied = config.storedBody(kind, path);
|
|
422
|
+
if (occupied !== void 0 && !isMintedRow(occupied)) rejectData(`Cannot replace "${replaces}" with "${path}" — this system already has a ${kind} of its own at "${path}".`, at);
|
|
423
|
+
landing.set(`${kind}:${path}`, replaces);
|
|
424
|
+
const from = held.borrowedFrom ?? declaredElsewhere({
|
|
425
|
+
config,
|
|
426
|
+
slug,
|
|
427
|
+
kind,
|
|
428
|
+
path: replaces
|
|
429
|
+
});
|
|
430
|
+
const stored = config.storedBody(kind, replaces);
|
|
431
|
+
const minted = stored !== void 0 && isMintedRow(stored);
|
|
432
|
+
entries.push({
|
|
433
|
+
kind,
|
|
434
|
+
accessor,
|
|
435
|
+
path,
|
|
436
|
+
replaces,
|
|
437
|
+
...from ? { from } : {}
|
|
438
|
+
});
|
|
439
|
+
patches.push({
|
|
440
|
+
kind,
|
|
441
|
+
operation: minted ? "create" : "replace",
|
|
442
|
+
path: replaces,
|
|
443
|
+
data: {
|
|
444
|
+
...ref(kind, path, slug),
|
|
445
|
+
extend: {}
|
|
446
|
+
}
|
|
447
|
+
}, {
|
|
448
|
+
kind,
|
|
449
|
+
operation: "rename",
|
|
450
|
+
path: replaces,
|
|
451
|
+
to: path
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return {
|
|
456
|
+
entries,
|
|
457
|
+
patches
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
306
461
|
* The other pins' `options` entries a take leaves applying to nothing, and the pin edits that remove
|
|
307
462
|
* them.
|
|
308
463
|
*
|
|
@@ -74,7 +74,7 @@ interface RecordedSource {
|
|
|
74
74
|
* The dense-counter `2` a short-lived scheme stamped before the chain existed reads as the mint
|
|
75
75
|
* that replaced it (see `detectedWireVersion`).
|
|
76
76
|
*/
|
|
77
|
-
declare const SERIALIZED_CONFIG_VERSION =
|
|
77
|
+
declare const SERIALIZED_CONFIG_VERSION = 20261102193912;
|
|
78
78
|
interface SerializedConfig {
|
|
79
79
|
/** See {@link SERIALIZED_CONFIG_VERSION}. Always written; a stored artifact may lack it. */
|
|
80
80
|
readonly version: number;
|
package/dist/framework/Config.js
CHANGED
|
@@ -6391,7 +6391,7 @@ var Config = class Config {
|
|
|
6391
6391
|
* links from the provided sources. A declared `linkedKind` with no provided source throws. */
|
|
6392
6392
|
hydrateFrom(stored, options) {
|
|
6393
6393
|
const claimed = schemaVersionOf(stored);
|
|
6394
|
-
if (claimed >
|
|
6394
|
+
if (claimed > 20261102193912) throw new SchemaVersionTooNew(claimed);
|
|
6395
6395
|
const wire = upgradeSerializedConfig(stored);
|
|
6396
6396
|
if (!Array.isArray(wire.ownedKinds) || typeof wire.items !== "object" || wire.items === null) throw new ConfigFormatError(`Config "${stored.name ?? "system-config"}" cannot be hydrated: this is not a serialized config-v2 config (no "ownedKinds"/"items"). A pre-cutover build artifact converts through the registered cutover migration — import the config type's module (e.g. \`configs/system\`) before hydrating.`);
|
|
6397
6397
|
const leftover = wire.options;
|
|
@@ -25,6 +25,11 @@ interface ComponentApiChange {
|
|
|
25
25
|
readonly component: string;
|
|
26
26
|
/** `true` when `after` has no such component: every instance of it names nothing. */
|
|
27
27
|
readonly removed: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* The path `after` holds the component at, when a rename moved it: its instances follow the rename,
|
|
30
|
+
* and `breaks` are what its props lose against the component they land on.
|
|
31
|
+
*/
|
|
32
|
+
readonly renamedTo?: string;
|
|
28
33
|
readonly breaks: readonly ComponentApiBreak[];
|
|
29
34
|
/** Props `after` exposes that `before` didn't, in declaration order. */
|
|
30
35
|
readonly added: readonly string[];
|
|
@@ -33,7 +38,15 @@ interface ComponentApiChange {
|
|
|
33
38
|
* The components whose API `after` broke for instances stored against `before`, in `before`'s
|
|
34
39
|
* order. A component that only gained props isn't listed. `components` narrows the read to those
|
|
35
40
|
* paths, for a caller that already knows what a change touched.
|
|
41
|
+
*
|
|
42
|
+
* `renames` are the component renames between the two, oldest first (`documentRenamesFromPatches`
|
|
43
|
+
* reads them off a draft). A renamed component's instances follow the rename, so it isn't removed:
|
|
44
|
+
* its props are compared with the component it lands on, which is what replacing a component with
|
|
45
|
+
* a source's of another name breaks, and a rename that keeps every prop breaks nothing.
|
|
36
46
|
*/
|
|
37
|
-
declare function componentApiChanges(before: Config, after: Config, components?: readonly string[]
|
|
47
|
+
declare function componentApiChanges(before: Config, after: Config, components?: readonly string[], renames?: readonly {
|
|
48
|
+
readonly from: string;
|
|
49
|
+
readonly to: string;
|
|
50
|
+
}[]): ComponentApiChange[];
|
|
38
51
|
//#endregion
|
|
39
52
|
export { ComponentApiBreak, ComponentApiChange, componentApiChanges };
|
|
@@ -43,8 +43,8 @@ function breaksOf(configs, component, before, after) {
|
|
|
43
43
|
});
|
|
44
44
|
continue;
|
|
45
45
|
}
|
|
46
|
-
const wasValues = valuesOf(configs.before, component, was);
|
|
47
|
-
const isValues = valuesOf(configs.after, component, is);
|
|
46
|
+
const wasValues = valuesOf(configs.before, component.before, was);
|
|
47
|
+
const isValues = valuesOf(configs.after, component.after, is);
|
|
48
48
|
if (wasValues && isValues) {
|
|
49
49
|
const kept = new Set(isValues);
|
|
50
50
|
const gone = wasValues.filter((value) => !kept.has(value));
|
|
@@ -65,14 +65,26 @@ function breaksOf(configs, component, before, after) {
|
|
|
65
65
|
* The components whose API `after` broke for instances stored against `before`, in `before`'s
|
|
66
66
|
* order. A component that only gained props isn't listed. `components` narrows the read to those
|
|
67
67
|
* paths, for a caller that already knows what a change touched.
|
|
68
|
+
*
|
|
69
|
+
* `renames` are the component renames between the two, oldest first (`documentRenamesFromPatches`
|
|
70
|
+
* reads them off a draft). A renamed component's instances follow the rename, so it isn't removed:
|
|
71
|
+
* its props are compared with the component it lands on, which is what replacing a component with
|
|
72
|
+
* a source's of another name breaks, and a rename that keeps every prop breaks nothing.
|
|
68
73
|
*/
|
|
69
|
-
function componentApiChanges(before, after, components) {
|
|
74
|
+
function componentApiChanges(before, after, components, renames = []) {
|
|
70
75
|
const paths = components ?? before.list("component").map((entity) => entity.path);
|
|
71
76
|
const changes = [];
|
|
77
|
+
const landsAt = (path) => {
|
|
78
|
+
let at = path;
|
|
79
|
+
for (const rename of renames) if (rename.from === at) at = rename.to;
|
|
80
|
+
return at;
|
|
81
|
+
};
|
|
72
82
|
for (const component of paths) {
|
|
73
83
|
if (!before.resolve("component", component)) continue;
|
|
74
84
|
const was = surfaceProps(before, component);
|
|
75
|
-
|
|
85
|
+
const target = landsAt(component);
|
|
86
|
+
const renamedTo = target === component ? void 0 : target;
|
|
87
|
+
if (!after.resolve("component", target)) {
|
|
76
88
|
changes.push({
|
|
77
89
|
component,
|
|
78
90
|
removed: true,
|
|
@@ -81,16 +93,20 @@ function componentApiChanges(before, after, components) {
|
|
|
81
93
|
});
|
|
82
94
|
continue;
|
|
83
95
|
}
|
|
84
|
-
const is = surfaceProps(after,
|
|
96
|
+
const is = surfaceProps(after, target);
|
|
85
97
|
const breaks = breaksOf({
|
|
86
98
|
before,
|
|
87
99
|
after
|
|
88
|
-
},
|
|
100
|
+
}, {
|
|
101
|
+
before: component,
|
|
102
|
+
after: target
|
|
103
|
+
}, was, is);
|
|
89
104
|
if (breaks.length === 0) continue;
|
|
90
105
|
const had = new Set(was.map((prop) => prop.name));
|
|
91
106
|
changes.push({
|
|
92
107
|
component,
|
|
93
108
|
removed: false,
|
|
109
|
+
...renamedTo ? { renamedTo } : {},
|
|
94
110
|
breaks,
|
|
95
111
|
added: is.filter((prop) => !had.has(prop.name)).map((prop) => prop.name)
|
|
96
112
|
});
|
|
@@ -15,7 +15,7 @@ import { Patch, SerializedConfig } from "./Config.js";
|
|
|
15
15
|
* sets this constant to its own older timestamp; the merge ref then carries a registered migration
|
|
16
16
|
* newer than the constant, and the assertion fails until the author re-mints.
|
|
17
17
|
*/
|
|
18
|
-
declare const CURRENT_SCHEMA_VERSION =
|
|
18
|
+
declare const CURRENT_SCHEMA_VERSION = 20261102193912;
|
|
19
19
|
/** What the walkers need to see of an envelope — loose on purpose, so a typed `SerializedConfig`
|
|
20
20
|
* and a raw old-shape record both flow in without casts. */
|
|
21
21
|
interface VersionStamped {
|
|
@@ -13,7 +13,7 @@ import { ConfigFormatError } from "./rejection.js";
|
|
|
13
13
|
* sets this constant to its own older timestamp; the merge ref then carries a registered migration
|
|
14
14
|
* newer than the constant, and the assertion fails until the author re-mints.
|
|
15
15
|
*/
|
|
16
|
-
const CURRENT_SCHEMA_VERSION =
|
|
16
|
+
const CURRENT_SCHEMA_VERSION = 20261102193912;
|
|
17
17
|
/** The mint that moved `options` into the `settings` kind — restated here, rather than imported
|
|
18
18
|
* from the migration that owns it, because that migration imports this module. */
|
|
19
19
|
const SETTINGS_AS_KIND_VERSION = 20260908171448;
|
|
@@ -45,7 +45,7 @@ function registerSchemaMigrations(...migrations) {
|
|
|
45
45
|
for (const migration of migrations) {
|
|
46
46
|
if (migration.version !== 1 && !isMintTimestamp(migration.version)) throw new Error(`Schema migration version ${migration.version} is not a mint timestamp — mint one with \`date +%Y%m%d%H%M%S\`, like a supabase migration.`);
|
|
47
47
|
if (REGISTERED.has(migration.version)) throw new Error(`A schema migration at version ${migration.version} is already registered.`);
|
|
48
|
-
if (migration.version >
|
|
48
|
+
if (migration.version > 20261102193912) throw new Error(`Schema migration ${migration.version} is newer than CURRENT_SCHEMA_VERSION (${CURRENT_SCHEMA_VERSION}) — a stale branch merged, or the mint forgot to bump the constant. Re-mint: set CURRENT_SCHEMA_VERSION to the newest migration version.`);
|
|
49
49
|
REGISTERED.set(migration.version, migration);
|
|
50
50
|
}
|
|
51
51
|
}
|
|
@@ -102,7 +102,7 @@ function pendingFor(version, target, options) {
|
|
|
102
102
|
* Already-current input passes through untouched — same reference, so hot paths pay nothing.
|
|
103
103
|
*/
|
|
104
104
|
function upgradeSerializedConfig(json, options) {
|
|
105
|
-
const target = options?.target ??
|
|
105
|
+
const target = options?.target ?? 20261102193912;
|
|
106
106
|
const version = detectedWireVersion(json);
|
|
107
107
|
if (version > target) throw new SchemaVersionTooNew(version, target);
|
|
108
108
|
const raw = json;
|
|
@@ -160,7 +160,7 @@ function upgradedSources(json, options) {
|
|
|
160
160
|
* meaning under the current schema and replay skips it — reported by the caller, not swallowed here.
|
|
161
161
|
*/
|
|
162
162
|
function upgradePatch(patch, writtenAt, options) {
|
|
163
|
-
const target = options?.target ??
|
|
163
|
+
const target = options?.target ?? 20261102193912;
|
|
164
164
|
if (writtenAt > target) throw new SchemaVersionTooNew(writtenAt, target);
|
|
165
165
|
let current = patch;
|
|
166
166
|
for (const migration of pendingFor(writtenAt, target, options)) {
|
|
@@ -179,7 +179,7 @@ function upgradePatch(patch, writtenAt, options) {
|
|
|
179
179
|
* stamp would cause.
|
|
180
180
|
*/
|
|
181
181
|
function upgradeDraftEntries(draft, options) {
|
|
182
|
-
const target = options?.target ??
|
|
182
|
+
const target = options?.target ?? 20261102193912;
|
|
183
183
|
const entries = [];
|
|
184
184
|
const held = [];
|
|
185
185
|
const retired = [];
|