@yahoo/uds-create-config 4.40.0 → 4.41.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 +2 -0
- package/dist/entities/system/Component.d.ts +27 -3
- package/dist/entities/system/Component.js +66 -13
- package/dist/entities/system/defineComponent.d.ts +11 -4
- package/dist/entities/system/defineComponent.js +30 -10
- package/dist/entities/system/forward-route-invariants.d.ts +19 -0
- package/dist/entities/system/forward-route-invariants.js +58 -0
- package/dist/framework/Config.d.ts +1 -1
- package/dist/framework/Config.js +1 -1
- package/dist/framework/prop-surface.d.ts +15 -1
- package/dist/framework/prop-surface.js +19 -3
- package/dist/framework/render-spec.js +10 -8
- 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/index.js +2 -2
- package/dist/migrations/20260929161433_forward_routes.d.ts +12 -0
- package/dist/migrations/20260929161433_forward_routes.js +8 -0
- package/dist/spec/detach.js +9 -6
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
package/dist/configs/system.js
CHANGED
|
@@ -46,6 +46,7 @@ import { linkOnLinkedMigration } from "../migrations/20260928180801_link_on_link
|
|
|
46
46
|
import { compositePropLayerMigration } from "../migrations/20260928215509_composite_prop_layer.js";
|
|
47
47
|
import { shadowValueMigration } from "../migrations/20260929005554_shadow_value.js";
|
|
48
48
|
import { cssContentSafelistRefsMigration } from "../migrations/20260929150131_css_content_safelist_refs.js";
|
|
49
|
+
import { forwardRoutesMigration } from "../migrations/20260929161433_forward_routes.js";
|
|
49
50
|
const System = defineConfig({
|
|
50
51
|
name: WEB_CONFIG_NAME,
|
|
51
52
|
description: "A design system: the values, primitives and compositions an app is built from. A ref is written `{ \"__ref\": \"<kind>:<path>\" }`, so a token is `{ \"__ref\": \"token:<group>/<name>\" }` and a token group is `{ \"__ref\": \"token:<group>\" }`. Build it in this order. Tokens first, in groups by purpose (colors, spacing, radii, type sizes), since everything else names them. Then style properties, each with `values` holding one ref to the token group whose members it accepts (`{ \"__ref\": \"token:color\" }`), so a prop takes token names rather than raw CSS; add a string keyword only for a value no token holds. A style rule's bag holds a prop's values (`\"padding\": \"4\"`, `\"bg\": \"brand\"`), never raw CSS. Then a few primitive components, one layer each, whose props are style properties (a box for layout and surface, a text for type, a pressable for interaction). Then every other component composed from those primitives: its layers name a primitive or an element, its anatomy places them (a node renders a layer, a tag or `Slot` where a slot prop's content lands; `children` is the edge list; `visible` is `{ \"$state\": \"<prop>\" }`; a prop shown in a nested layer is a `forward` route, and `$state` never sits in a node's props), and its style rules set style-property values per layer, keyed by prop values when a variant changes them. Modes (a dark mode is `{ type: \"selector\", value: \".dark\" }`) change token values through a token's `overrides`, so a component never restyles for a mode; modifiers (hover, focus, disabled) name a state, and a style rule applies under one as a `_hover` block inside a layer bag. Composites bundle style values that recur. Last, register canvas roles: the canvas draws with them, one entity per tool (`frame` for the layout primitive, `text` for the text primitive, `image`) whose entries name the components that tool inserts, and a tool whose role is unregistered is disabled. Give each component a `preview` entry under `*` so it shows content, and never write a CSS property name where a style-property name belongs.",
|
|
@@ -103,5 +104,6 @@ if (!registeredSchemaMigrations().has(20260928180801)) registerSchemaMigrations(
|
|
|
103
104
|
if (!registeredSchemaMigrations().has(20260928215509)) registerSchemaMigrations(compositePropLayerMigration);
|
|
104
105
|
if (!registeredSchemaMigrations().has(20260929005554)) registerSchemaMigrations(shadowValueMigration);
|
|
105
106
|
if (!registeredSchemaMigrations().has(20260929150131)) registerSchemaMigrations(cssContentSafelistRefsMigration);
|
|
107
|
+
if (!registeredSchemaMigrations().has(20260929161433)) registerSchemaMigrations(forwardRoutesMigration);
|
|
106
108
|
//#endregion
|
|
107
109
|
export { System };
|
|
@@ -116,10 +116,18 @@ declare const propShapeDefinition: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
116
116
|
type: z.ZodLiteral<"forward">;
|
|
117
117
|
target: z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
118
118
|
__ref: z.ZodString;
|
|
119
|
-
}, z.core.$strip>]>;
|
|
119
|
+
}, z.core.$strip>, z.ZodArray<z.ZodString>]>;
|
|
120
120
|
into: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
121
121
|
__ref: z.ZodString;
|
|
122
122
|
}, z.core.$strip>]>>;
|
|
123
|
+
also: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
124
|
+
target: z.ZodObject<{
|
|
125
|
+
__ref: z.ZodString;
|
|
126
|
+
}, z.core.$strip>;
|
|
127
|
+
into: z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
128
|
+
__ref: z.ZodString;
|
|
129
|
+
}, z.core.$strip>]>;
|
|
130
|
+
}, z.core.$strict>>>;
|
|
123
131
|
accepts: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
|
|
124
132
|
__ref: z.ZodString;
|
|
125
133
|
}, z.core.$strip>, z.ZodArray<z.ZodObject<{
|
|
@@ -134,12 +142,20 @@ declare const propShapeDefinition: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
134
142
|
}, z.core.$strip>]>>]>>>;
|
|
135
143
|
}, z.core.$strict>, z.ZodTransform<{
|
|
136
144
|
type: "forward";
|
|
137
|
-
target: string | {
|
|
145
|
+
target: string | string[] | {
|
|
138
146
|
__ref: string;
|
|
139
147
|
};
|
|
140
148
|
into?: string | {
|
|
141
149
|
__ref: string;
|
|
142
150
|
} | undefined;
|
|
151
|
+
also?: {
|
|
152
|
+
target: {
|
|
153
|
+
__ref: string;
|
|
154
|
+
};
|
|
155
|
+
into: string | {
|
|
156
|
+
__ref: string;
|
|
157
|
+
};
|
|
158
|
+
}[] | undefined;
|
|
143
159
|
accepts?: {
|
|
144
160
|
__ref: string;
|
|
145
161
|
} | {
|
|
@@ -154,12 +170,20 @@ declare const propShapeDefinition: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
154
170
|
})[]> | undefined;
|
|
155
171
|
}, {
|
|
156
172
|
type: "forward";
|
|
157
|
-
target: string | {
|
|
173
|
+
target: string | string[] | {
|
|
158
174
|
__ref: string;
|
|
159
175
|
};
|
|
160
176
|
into?: string | {
|
|
161
177
|
__ref: string;
|
|
162
178
|
} | undefined;
|
|
179
|
+
also?: {
|
|
180
|
+
target: {
|
|
181
|
+
__ref: string;
|
|
182
|
+
};
|
|
183
|
+
into: string | {
|
|
184
|
+
__ref: string;
|
|
185
|
+
};
|
|
186
|
+
}[] | undefined;
|
|
163
187
|
accepts?: {
|
|
164
188
|
__ref: string;
|
|
165
189
|
} | {
|
|
@@ -20,6 +20,7 @@ import { listSlots, namedSlots, slotHostedBy, slotOf } from "../../spec/slot.js"
|
|
|
20
20
|
import { cellAt } from "../../spec/cell.js";
|
|
21
21
|
import { instanceAt } from "../../spec/instance.js";
|
|
22
22
|
import { File } from "./File.js";
|
|
23
|
+
import { forwardRouteInvariants, refusedRoutes } from "./forward-route-invariants.js";
|
|
23
24
|
import { iconDomainInvariants } from "./icon-domain-invariants.js";
|
|
24
25
|
import { rootLayerInvariants } from "./root-layer-invariants.js";
|
|
25
26
|
import { Token } from "./Token.js";
|
|
@@ -374,14 +375,35 @@ const layerPath = z.string().regex(/^[^/]+\/[^/]+$/, "expected `<layer>/<prop>`"
|
|
|
374
375
|
* Patch comes through a different door than one written in a config file, and both mean the same thing.
|
|
375
376
|
*/
|
|
376
377
|
function routedTarget(decl) {
|
|
377
|
-
if (!("target" in decl)
|
|
378
|
-
const
|
|
378
|
+
if (!("target" in decl)) return decl;
|
|
379
|
+
const { target } = decl;
|
|
380
|
+
if (Array.isArray(target)) {
|
|
381
|
+
const [first, ...rest] = target.map(splitLayerPath);
|
|
382
|
+
return {
|
|
383
|
+
...decl,
|
|
384
|
+
...first,
|
|
385
|
+
also: [...rest, ...decl.also ?? []]
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
if (typeof target !== "string") return decl;
|
|
379
389
|
return {
|
|
380
390
|
...decl,
|
|
381
|
-
|
|
382
|
-
|
|
391
|
+
...splitLayerPath(target)
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
/** `<layer>/<prop>` as the pair a route stores. */
|
|
395
|
+
function splitLayerPath(path) {
|
|
396
|
+
const separator = path.indexOf("/");
|
|
397
|
+
return {
|
|
398
|
+
target: layerRef(path.slice(0, separator)),
|
|
399
|
+
into: path.slice(separator + 1)
|
|
383
400
|
};
|
|
384
401
|
}
|
|
402
|
+
/** A forward's routes after its first — `also`, as the schema stores it. */
|
|
403
|
+
const alsoRoutes = z.array(z.strictObject({
|
|
404
|
+
target: refSchema,
|
|
405
|
+
into: z.union([z.string(), refSchema])
|
|
406
|
+
})).min(1);
|
|
385
407
|
/** A composite prop's authored `layer` name as the layer ref it is stored as — see {@link routedTarget}
|
|
386
408
|
* for why the coercion lives in the schema. */
|
|
387
409
|
function layerAsRef(decl) {
|
|
@@ -430,8 +452,13 @@ function propShapeWith(when) {
|
|
|
430
452
|
}),
|
|
431
453
|
z.strictObject({
|
|
432
454
|
type: z.literal("forward"),
|
|
433
|
-
target: z.union([
|
|
455
|
+
target: z.union([
|
|
456
|
+
layerPath,
|
|
457
|
+
refSchema,
|
|
458
|
+
z.array(layerPath).min(2)
|
|
459
|
+
]),
|
|
434
460
|
into: z.union([z.string(), refSchema]).optional(),
|
|
461
|
+
also: alsoRoutes.optional(),
|
|
435
462
|
accepts: z.union([refSchema, z.array(refSchema)]).optional(),
|
|
436
463
|
text: z.boolean().optional(),
|
|
437
464
|
required: z.boolean().optional(),
|
|
@@ -526,6 +553,18 @@ function propSchemaFor(config, owner) {
|
|
|
526
553
|
const layers = owner.layers;
|
|
527
554
|
return propShapeWith(ownerCondition(config, owner)).nullable().check((ctx) => {
|
|
528
555
|
const decl = ctx.value;
|
|
556
|
+
if (decl !== null && isPlainObject(decl) && decl.type === "forward" && Array.isArray(decl.also) && decl.also.length > 0) {
|
|
557
|
+
const refused = refusedRoutes(config, layers, decl);
|
|
558
|
+
if (refused) {
|
|
559
|
+
ctx.issues.push({
|
|
560
|
+
code: "custom",
|
|
561
|
+
input: decl,
|
|
562
|
+
path: ["target"],
|
|
563
|
+
message: refused
|
|
564
|
+
});
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
529
568
|
if (decl === null || !isPlainObject(decl) || decl.type !== "forward" || decl.accepts === void 0 || routesContent(config, layers, decl)) return;
|
|
530
569
|
const layer = isRef(decl.target) ? pathOf(decl.target) : String(decl.target ?? "?");
|
|
531
570
|
const prop = intoName(decl.into);
|
|
@@ -551,14 +590,24 @@ function propSchemaFor(config, owner) {
|
|
|
551
590
|
*/
|
|
552
591
|
function declaringInto(config, layers, decl) {
|
|
553
592
|
if (!isPlainObject(decl) || decl.type !== "forward" || !isRef(decl.target)) return decl;
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
593
|
+
const minted = (route) => {
|
|
594
|
+
if (!isRef(route.target)) return route;
|
|
595
|
+
const element = layers?.[pathOf(route.target)];
|
|
596
|
+
if (!isRef(element)) return route;
|
|
597
|
+
const name = intoName(route.into);
|
|
598
|
+
const owner = propOwner(config, pathOf(element), name);
|
|
599
|
+
if (!owner || isRef(route.into) && pathOf(route.into) === owner) return route;
|
|
600
|
+
return {
|
|
601
|
+
...route,
|
|
602
|
+
into: memberRef("component", owner, "props", name)
|
|
603
|
+
};
|
|
604
|
+
};
|
|
605
|
+
const first = minted(decl);
|
|
606
|
+
if (!Array.isArray(decl.also) || decl.also.length === 0) return first;
|
|
607
|
+
const also = decl.also.map((route) => isPlainObject(route) ? minted(route) : route);
|
|
559
608
|
return {
|
|
560
|
-
...
|
|
561
|
-
|
|
609
|
+
...first,
|
|
610
|
+
also
|
|
562
611
|
};
|
|
563
612
|
}
|
|
564
613
|
/**
|
|
@@ -2140,7 +2189,11 @@ subtree: z.boolean().default(true) }).optional()
|
|
|
2140
2189
|
handler: (input, config) => validateComponentProps(config, String(input.path), input.data.props ?? {})
|
|
2141
2190
|
}
|
|
2142
2191
|
},
|
|
2143
|
-
invariants: (config) => [
|
|
2192
|
+
invariants: (config) => [
|
|
2193
|
+
...iconDomainInvariants(config),
|
|
2194
|
+
...rootLayerInvariants(config),
|
|
2195
|
+
...forwardRouteInvariants(config)
|
|
2196
|
+
]
|
|
2144
2197
|
});
|
|
2145
2198
|
const baseComponentRef = ComponentEntity.ref.bind(ComponentEntity);
|
|
2146
2199
|
/** Style rules as a list, the form `defineComponent.styles` takes, in the stored form: keyed by each
|
|
@@ -145,11 +145,18 @@ type Forwarded = '*' | readonly string[];
|
|
|
145
145
|
*/
|
|
146
146
|
interface ForwardDecl<Path extends string = string> {
|
|
147
147
|
type: 'forward';
|
|
148
|
-
/** `<layer>/<prop>` as authored
|
|
149
|
-
|
|
148
|
+
/** `<layer>/<prop>` as authored, or several to route the one value to each
|
|
149
|
+
* (`['startIcon/variant', 'endIcon/variant']`); a `layerRef` once stored, with the prop moved to
|
|
150
|
+
* `into` and any routes after the first to `also`. */
|
|
151
|
+
target: Path | Ref$1 | readonly Path[];
|
|
150
152
|
/** STORED only — a member ref at the prop on that layer's element
|
|
151
153
|
* (`component:Input#props/disabled`). Minted at registration, when the layer's element has a path. */
|
|
152
154
|
into?: string | Ref$1;
|
|
155
|
+
/** STORED only — the routes after the first, each the same pair as `target` + `into`. */
|
|
156
|
+
also?: readonly {
|
|
157
|
+
readonly target: Ref$1;
|
|
158
|
+
readonly into: string | Ref$1;
|
|
159
|
+
}[];
|
|
153
160
|
/** Only where this route LANDS on a hole — it means exactly what {@link SlotDecl.accepts} means.
|
|
154
161
|
* That the target is a hole is computed from it (`routesContent`), so the rule is checked rather
|
|
155
162
|
* than trusted, and a route into a value prop is refused by name. */
|
|
@@ -367,7 +374,7 @@ type ValueOfDeclaration<D, Self, K extends PropertyKey, View extends SlotView> =
|
|
|
367
374
|
text: true;
|
|
368
375
|
} ? string : D extends {
|
|
369
376
|
type: 'forward';
|
|
370
|
-
target: `${string}/children
|
|
377
|
+
target: `${string}/children` | readonly [`${string}/children`, ...unknown[]];
|
|
371
378
|
} ? ReactNode : unknown;
|
|
372
379
|
/**
|
|
373
380
|
* A prop type's DECLARED props — everything but its index signature.
|
|
@@ -394,7 +401,7 @@ type RoutedValueOf<L extends LayerMap, D, Self = unknown, K extends PropertyKey
|
|
|
394
401
|
text: true;
|
|
395
402
|
} ? string : D extends {
|
|
396
403
|
type: 'forward';
|
|
397
|
-
target: `${infer Layer}/${infer Prop}
|
|
404
|
+
target: `${infer Layer}/${infer Prop}` | readonly [`${infer Layer}/${infer Prop}`, ...unknown[]];
|
|
398
405
|
} ? Prop extends 'children' ? ReactNode : Layer extends keyof L ? Prop extends keyof ElementPropsOf<L[Layer]> ? ElementPropsOf<L[Layer]>[Prop] : unknown : unknown : ValueOf<D, Self, K, View>;
|
|
399
406
|
/**
|
|
400
407
|
* The props the rendered ELEMENT contributes, `ref` included.
|
|
@@ -204,11 +204,12 @@ function storedProp(decl, layers) {
|
|
|
204
204
|
*/
|
|
205
205
|
function isAuthoredForward(decl) {
|
|
206
206
|
if (typeof decl !== "object" || decl === null || decl.type !== "forward") return false;
|
|
207
|
-
const { target, into } = decl;
|
|
208
|
-
return typeof target === "string" || typeof into === "string";
|
|
207
|
+
const { target, into, also } = decl;
|
|
208
|
+
return typeof target === "string" || Array.isArray(target) || typeof into === "string" || Array.isArray(also) && also.some((route) => typeof route?.into === "string");
|
|
209
209
|
}
|
|
210
210
|
/**
|
|
211
|
-
* A `bind()` declaration with
|
|
211
|
+
* A `bind()` declaration with each route's `into` resolved to a member ref at its layer's element —
|
|
212
|
+
* the first route's as `target` + `into`, the rest as `also`, the shape the schema stores.
|
|
212
213
|
*
|
|
213
214
|
* Left as authored when the layer's element is a tag or an unadopted handle: a tag's props are the
|
|
214
215
|
* DOM's and have no entity to point into, and minting a ref against an empty path would produce
|
|
@@ -216,15 +217,34 @@ function isAuthoredForward(decl) {
|
|
|
216
217
|
* non-ref `into`, so the failure is loud at registration rather than silent in the data.
|
|
217
218
|
*/
|
|
218
219
|
function storedForward(decl, layers) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
...decl,
|
|
220
|
+
const split = (path) => ({
|
|
221
|
+
target: layerRef(path.slice(0, path.indexOf("/"))),
|
|
222
|
+
into: path.slice(path.indexOf("/") + 1)
|
|
223
|
+
});
|
|
224
|
+
const [first, ...rest] = [...Array.isArray(decl.target) ? decl.target.map(split) : typeof decl.target === "string" ? [split(decl.target)] : [{
|
|
225
225
|
target: decl.target,
|
|
226
226
|
into: decl.into ?? "children"
|
|
227
|
-
},
|
|
227
|
+
}], ...decl.also ?? []];
|
|
228
|
+
if (!first) return decl;
|
|
229
|
+
const minted = (route) => typeof route.into === "string" ? storedRoutedForward({
|
|
230
|
+
type: "forward",
|
|
231
|
+
target: route.target,
|
|
232
|
+
into: route.into
|
|
233
|
+
}, layers) : route;
|
|
234
|
+
const primary = minted(first);
|
|
235
|
+
const { also: _also, ...body } = decl;
|
|
236
|
+
return {
|
|
237
|
+
...body,
|
|
238
|
+
target: primary.target,
|
|
239
|
+
into: primary.into,
|
|
240
|
+
...rest.length > 0 ? { also: rest.map((route) => {
|
|
241
|
+
const stored = minted(route);
|
|
242
|
+
return {
|
|
243
|
+
target: stored.target,
|
|
244
|
+
into: stored.into
|
|
245
|
+
};
|
|
246
|
+
}) } : {}
|
|
247
|
+
};
|
|
228
248
|
}
|
|
229
249
|
function storedRoutedForward(decl, layers) {
|
|
230
250
|
const layer = pathOf(decl.target);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Config } from '../../framework/Config';
|
|
2
|
+
/**
|
|
3
|
+
* Why a forward with several routes can't take them, or `undefined` when it can. Every route has to
|
|
4
|
+
* land on a layer the component has, and all of them on the same kind of prop with the same values:
|
|
5
|
+
* one value is written to each, so two Icon `variant`s agree and an Icon's `name` beside its
|
|
6
|
+
* `variant` does not. A route that carries content takes one layer only — the same content can't be
|
|
7
|
+
* placed twice.
|
|
8
|
+
*/
|
|
9
|
+
export declare function refusedRoutes(config: Config, layers: Readonly<Record<string, unknown>> | undefined, decl: {
|
|
10
|
+
target?: unknown;
|
|
11
|
+
into?: unknown;
|
|
12
|
+
also?: unknown;
|
|
13
|
+
}): string | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Every component's forwards that take several routes they can't. Held as an invariant rather than
|
|
16
|
+
* only in the prop's schema, because the schema's per-owner check runs on a prop op and a component
|
|
17
|
+
* written whole (`register`, an item create) reaches storage without it.
|
|
18
|
+
*/
|
|
19
|
+
export declare const forwardRouteInvariants: (config: Config) => string[];
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { isRef, pathOf } from "../../framework/utils/refs.js";
|
|
2
|
+
import { isPlainRecord } from "./style-bag.js";
|
|
3
|
+
import { createSliceMemo } from "../../framework/memo.js";
|
|
4
|
+
import { forwardRoutes, intoName, routedPropIn, routesContent } from "../../framework/prop-surface.js";
|
|
5
|
+
//#region src/entities/system/forward-route-invariants.ts
|
|
6
|
+
/**
|
|
7
|
+
* Why a forward with several routes can't take them, or `undefined` when it can. Every route has to
|
|
8
|
+
* land on a layer the component has, and all of them on the same kind of prop with the same values:
|
|
9
|
+
* one value is written to each, so two Icon `variant`s agree and an Icon's `name` beside its
|
|
10
|
+
* `variant` does not. A route that carries content takes one layer only — the same content can't be
|
|
11
|
+
* placed twice.
|
|
12
|
+
*/
|
|
13
|
+
function refusedRoutes(config, layers, decl) {
|
|
14
|
+
const routes = forwardRoutes(decl);
|
|
15
|
+
const refPath = (value) => isRef(value) ? pathOf(value.__ref) : void 0;
|
|
16
|
+
const spelled = (route) => `\`${refPath(route.target) ?? "?"}/${intoName(route.into)}\``;
|
|
17
|
+
for (const route of routes) {
|
|
18
|
+
const layer = refPath(route.target);
|
|
19
|
+
if (layers && (!layer || !(layer in layers))) return `${spelled(route)} names a layer this component doesn't have. Every route of a forward lands on one of its own layers.`;
|
|
20
|
+
}
|
|
21
|
+
if (routes.some((route) => routesContent(config, layers, route))) return `A forward that carries content takes one route: the same content can't fill ${routes.map(spelled).join(" and ")}. Route a value (a variant, a string) to several layers, or give each layer its own slot.`;
|
|
22
|
+
const landing = (route) => {
|
|
23
|
+
const routed = routedPropIn(config, layers, route);
|
|
24
|
+
return routed ? JSON.stringify({
|
|
25
|
+
type: routed.type,
|
|
26
|
+
value: routed.value
|
|
27
|
+
}) : "attribute";
|
|
28
|
+
};
|
|
29
|
+
const [first, ...rest] = routes;
|
|
30
|
+
if (!first) return void 0;
|
|
31
|
+
const expected = landing(first);
|
|
32
|
+
for (const route of rest) if (landing(route) !== expected) return `${spelled(route)} takes different values than ${spelled(first)}, and a forward writes one value to every route. Route it only to props that take the same values, or declare a second prop.`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Every component's forwards that take several routes they can't. Held as an invariant rather than
|
|
36
|
+
* only in the prop's schema, because the schema's per-owner check runs on a prop op and a component
|
|
37
|
+
* written whole (`register`, an item create) reaches storage without it.
|
|
38
|
+
*/
|
|
39
|
+
const forwardRouteInvariants = createSliceMemo({
|
|
40
|
+
kinds: ["component"],
|
|
41
|
+
compute: (config) => {
|
|
42
|
+
const problems = [];
|
|
43
|
+
for (const item of config.list("component")) {
|
|
44
|
+
const body = config.resolve("component", item.path)?.toJSON();
|
|
45
|
+
if (!body || !isPlainRecord(body.props)) continue;
|
|
46
|
+
const layers = isPlainRecord(body.layers) ? body.layers : void 0;
|
|
47
|
+
for (const [name, decl] of Object.entries(body.props)) {
|
|
48
|
+
if (!isPlainRecord(decl) || decl.type !== "forward") continue;
|
|
49
|
+
if (!Array.isArray(decl.also) || decl.also.length === 0) continue;
|
|
50
|
+
const refused = refusedRoutes(config, layers, decl);
|
|
51
|
+
if (refused) problems.push(`Component "${item.path}" prop "${name}": ${refused}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return problems;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
//#endregion
|
|
58
|
+
export { forwardRouteInvariants, refusedRoutes };
|
|
@@ -72,7 +72,7 @@ interface RecordedSource {
|
|
|
72
72
|
* The dense-counter `2` a short-lived scheme stamped before the chain existed reads as the mint
|
|
73
73
|
* that replaced it (see `detectedWireVersion`).
|
|
74
74
|
*/
|
|
75
|
-
declare const SERIALIZED_CONFIG_VERSION =
|
|
75
|
+
declare const SERIALIZED_CONFIG_VERSION = 20260929161433;
|
|
76
76
|
interface SerializedConfig {
|
|
77
77
|
/** See {@link SERIALIZED_CONFIG_VERSION}. Always written; a stored artifact may lack it. */
|
|
78
78
|
readonly version: number;
|
package/dist/framework/Config.js
CHANGED
|
@@ -5172,7 +5172,7 @@ var Config = class Config {
|
|
|
5172
5172
|
* links from the provided sources. A declared `linkedKind` with no provided source throws. */
|
|
5173
5173
|
hydrateFrom(stored, options) {
|
|
5174
5174
|
const claimed = schemaVersionOf(stored);
|
|
5175
|
-
if (claimed >
|
|
5175
|
+
if (claimed > 20260929161433) throw new SchemaVersionTooNew(claimed);
|
|
5176
5176
|
const wire = upgradeSerializedConfig(stored);
|
|
5177
5177
|
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, or port the repo once with \`uds migrate\`.`);
|
|
5178
5178
|
const leftover = wire.options;
|
|
@@ -126,6 +126,20 @@ declare function propApplies(prop: {
|
|
|
126
126
|
* graph. Readers want the name either way, and `'children'` is the default a content route means.
|
|
127
127
|
*/
|
|
128
128
|
declare function intoName(into: unknown): string;
|
|
129
|
+
/**
|
|
130
|
+
* Every route a forward takes, the first one first: its own `target` + `into`, then each of `also`
|
|
131
|
+
* (`iconVariant` into both `startIcon/variant` and `endIcon/variant`). Registration refuses routes
|
|
132
|
+
* that disagree about what the value is, so the first answers for the prop's type and domain, and
|
|
133
|
+
* anything that WRITES the value or marks a layer bound walks all of them.
|
|
134
|
+
*/
|
|
135
|
+
declare function forwardRoutes(decl: {
|
|
136
|
+
target?: unknown;
|
|
137
|
+
into?: unknown;
|
|
138
|
+
also?: unknown;
|
|
139
|
+
}): {
|
|
140
|
+
target: unknown;
|
|
141
|
+
into: unknown;
|
|
142
|
+
}[];
|
|
129
143
|
/**
|
|
130
144
|
* The prop a route ROUTES INTO — the declaration at `<layer>/<prop>`, reached through the layer's
|
|
131
145
|
* element.
|
|
@@ -214,4 +228,4 @@ interface PropRoute {
|
|
|
214
228
|
*/
|
|
215
229
|
declare function renderBindingsOf(config: Config, componentPath: string): PropRoute[];
|
|
216
230
|
//#endregion
|
|
217
|
-
export { BindingKind, CodeSlot, ForwardClaims, PropRoute, SurfaceCompositeProp, SurfaceProp, SurfacePropCondition, SurfacePropKind, SurfaceScalarProp, SurfaceSlotProp, SurfaceStylePropertyProp, SurfaceVariantProp, codeSlotsOf, forwardClaims, forwardedLayers, intoName, isValueSlot, layerPropRoutes, propApplies, propRegistryRef, renderBindingsOf, routedProp, routedPropIn, routesContent, slotCodeForms, standsInForCode, surfaceProp, surfaceProps };
|
|
231
|
+
export { BindingKind, CodeSlot, ForwardClaims, PropRoute, SurfaceCompositeProp, SurfaceProp, SurfacePropCondition, SurfacePropKind, SurfaceScalarProp, SurfaceSlotProp, SurfaceStylePropertyProp, SurfaceVariantProp, codeSlotsOf, forwardClaims, forwardRoutes, forwardedLayers, intoName, isValueSlot, layerPropRoutes, propApplies, propRegistryRef, renderBindingsOf, routedProp, routedPropIn, routesContent, slotCodeForms, standsInForCode, surfaceProp, surfaceProps };
|
|
@@ -130,6 +130,22 @@ function intoName(into) {
|
|
|
130
130
|
return member.slice(member.indexOf("/") + 1);
|
|
131
131
|
}
|
|
132
132
|
/**
|
|
133
|
+
* Every route a forward takes, the first one first: its own `target` + `into`, then each of `also`
|
|
134
|
+
* (`iconVariant` into both `startIcon/variant` and `endIcon/variant`). Registration refuses routes
|
|
135
|
+
* that disagree about what the value is, so the first answers for the prop's type and domain, and
|
|
136
|
+
* anything that WRITES the value or marks a layer bound walks all of them.
|
|
137
|
+
*/
|
|
138
|
+
function forwardRoutes(decl) {
|
|
139
|
+
const also = Array.isArray(decl.also) ? decl.also : [];
|
|
140
|
+
return [{
|
|
141
|
+
target: decl.target,
|
|
142
|
+
into: decl.into
|
|
143
|
+
}, ...also.map((route) => ({
|
|
144
|
+
target: route?.target,
|
|
145
|
+
into: route?.into
|
|
146
|
+
}))];
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
133
149
|
* The prop a route ROUTES INTO — the declaration at `<layer>/<prop>`, reached through the layer's
|
|
134
150
|
* element.
|
|
135
151
|
*
|
|
@@ -455,9 +471,9 @@ function layerPropRoutes(config, componentPath, layer) {
|
|
|
455
471
|
if (!body) return routes;
|
|
456
472
|
const declared = resolveComponentProps(config, componentPath);
|
|
457
473
|
for (const [name, decl] of Object.entries(declared)) {
|
|
458
|
-
if (decl.type !== "forward"
|
|
474
|
+
if (decl.type !== "forward") continue;
|
|
459
475
|
if (routesContent(config, body.layers, decl)) continue;
|
|
460
|
-
routes.set(intoName(
|
|
476
|
+
for (const route of forwardRoutes(decl)) if (refPath(route.target) === layer) routes.set(intoName(route.into), name);
|
|
461
477
|
}
|
|
462
478
|
const { forwarded } = computeForwardClaims(config, componentPath);
|
|
463
479
|
for (const [name, claimedBy] of forwarded) if (claimedBy === layer && !routes.has(name)) routes.set(name, name);
|
|
@@ -502,4 +518,4 @@ function renderBoundProp(value) {
|
|
|
502
518
|
return name.length > 0 && !name.includes("/") ? name : void 0;
|
|
503
519
|
}
|
|
504
520
|
//#endregion
|
|
505
|
-
export { carriesContent, codeSlotsOf, forwardClaims, forwardedLayers, intoName, isValueSlot, layerPropRoutes, propApplies, propRegistryRef, renderBindingsOf, renderBoundProp, routedProp, routedPropIn, routesContent, routesIntoText, slotCodeForms, standsInForCode, surfaceProp, surfaceProps };
|
|
521
|
+
export { carriesContent, codeSlotsOf, forwardClaims, forwardRoutes, forwardedLayers, intoName, isValueSlot, layerPropRoutes, propApplies, propRegistryRef, renderBindingsOf, renderBoundProp, routedProp, routedPropIn, routesContent, routesIntoText, slotCodeForms, standsInForCode, surfaceProp, surfaceProps };
|
|
@@ -4,7 +4,7 @@ import { createSliceMemo } from "./memo.js";
|
|
|
4
4
|
import { unconditionalPreviewProps } from "./preview-store.js";
|
|
5
5
|
import { authoredBag, authoredValue, declaredPropDefaults, memberFromLeaf, refListOf, resolveComponentProps, stylePropertyPathFor } from "./projections.js";
|
|
6
6
|
import { slotHostLayer } from "./slot-host.js";
|
|
7
|
-
import { forwardedLayers, intoName, isValueSlot, renderBindingsOf, routesContent } from "./prop-surface.js";
|
|
7
|
+
import { forwardRoutes, forwardedLayers, intoName, isValueSlot, renderBindingsOf, routesContent } from "./prop-surface.js";
|
|
8
8
|
import { graftPreviewElements } from "../spec/preview-elements.js";
|
|
9
9
|
import { componentLayerClass, componentPropClasses, componentRuleClasses, layerRoutedProps, splitStyleProps, styleRuleMotionClasses } from "./class-names.js";
|
|
10
10
|
//#region src/framework/render-spec.ts
|
|
@@ -1060,13 +1060,15 @@ function valueForwards(config, body) {
|
|
|
1060
1060
|
for (const [from, decl] of Object.entries(body.props ?? {})) {
|
|
1061
1061
|
if (decl?.type !== "forward") continue;
|
|
1062
1062
|
if (routesContent(config, body.layers, decl)) continue;
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1063
|
+
for (const route of forwardRoutes(decl)) {
|
|
1064
|
+
const target = refOf(route.target);
|
|
1065
|
+
if (!target) continue;
|
|
1066
|
+
routes.push({
|
|
1067
|
+
layer: pathOf(target),
|
|
1068
|
+
from,
|
|
1069
|
+
into: intoName(route.into)
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1070
1072
|
}
|
|
1071
1073
|
return routes;
|
|
1072
1074
|
}
|
|
@@ -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 = 20260929161433;
|
|
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 = 20260929161433;
|
|
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 > 20260929161433) 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 ?? 20260929161433;
|
|
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 ?? 20260929161433;
|
|
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 ?? 20260929161433;
|
|
183
183
|
const entries = [];
|
|
184
184
|
const held = [];
|
|
185
185
|
const retired = [];
|