@yahoo/uds-create-config 5.20.0 → 5.21.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/LinkedSystem.d.ts +29 -4
- package/dist/entities/system/LinkedSystem.js +55 -4
- package/dist/framework/Config.d.ts +26 -1
- package/dist/framework/Config.js +82 -14
- package/dist/framework/schema-version.d.ts +1 -1
- package/dist/framework/schema-version.js +5 -5
- package/dist/migrations/20261009193004_link_override_ref.d.ts +9 -0
- package/dist/migrations/20261009193004_link_override_ref.js +10 -0
- package/dist/spec/links.d.ts +2 -0
- package/dist/spec/links.js +2 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -2
package/dist/configs/system.js
CHANGED
|
@@ -64,6 +64,7 @@ import { buildReactMigration } from "../migrations/20261008181849_build_react.js
|
|
|
64
64
|
import { cssMotionStatesMigration } from "../migrations/20261009000514_css_motion_states.js";
|
|
65
65
|
import { startingStyleModifierMigration } from "../migrations/20261009022722_starting_style_modifier.js";
|
|
66
66
|
import { cssMotionStrictMigration } from "../migrations/20261009181635_css_motion_strict.js";
|
|
67
|
+
import { linkOverrideRefMigration } from "../migrations/20261009193004_link_override_ref.js";
|
|
67
68
|
const System = defineConfig({
|
|
68
69
|
name: WEB_CONFIG_NAME,
|
|
69
70
|
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.",
|
|
@@ -139,5 +140,6 @@ if (!registeredSchemaMigrations().has(20261008181849)) registerSchemaMigrations(
|
|
|
139
140
|
if (!registeredSchemaMigrations().has(20261009000514)) registerSchemaMigrations(cssMotionStatesMigration);
|
|
140
141
|
if (!registeredSchemaMigrations().has(20261009022722)) registerSchemaMigrations(startingStyleModifierMigration);
|
|
141
142
|
if (!registeredSchemaMigrations().has(20261009181635)) registerSchemaMigrations(cssMotionStrictMigration);
|
|
143
|
+
if (!registeredSchemaMigrations().has(20261009193004)) registerSchemaMigrations(linkOverrideRefMigration);
|
|
142
144
|
//#endregion
|
|
143
145
|
export { System };
|
|
@@ -249,7 +249,7 @@ type PinOptionsInput<K extends string = string> = { [A in keyof RegisteredEntiti
|
|
|
249
249
|
/** A motion entry: an adjustment whose `runtime` is one the linked preset can be read as
|
|
250
250
|
* ({@link RegisteredMotionRuntimes}), so a swap the conversion would refuse doesn't type. */
|
|
251
251
|
type PinMotionAdjustment<E, P extends string> = {
|
|
252
|
-
override?: PinMotionBody<E, P>;
|
|
252
|
+
override?: PinMotionBody<E, P> | KindRefOf<E>;
|
|
253
253
|
extend?: PinMotionBody<E, P>;
|
|
254
254
|
hidden?: boolean;
|
|
255
255
|
};
|
|
@@ -259,8 +259,14 @@ type PinMotionBody<E, P extends string> = Extract<StoredBodyOf<PartialBodyOf<E>>
|
|
|
259
259
|
/** The linked presets registered at `path`, from whichever source. */
|
|
260
260
|
type LinkedMotionKey<P extends string> = Extract<keyof RegisteredMotionRuntimes, `${P}@${string}`>;
|
|
261
261
|
type MotionRuntimeOf<P extends string> = [LinkedMotionKey<P>] extends [never] ? 'css' | 'js' : RegisteredMotionRuntimes[LinkedMotionKey<P>];
|
|
262
|
+
/** A ref to another entity of `E`'s kind, which re-points the path to it. */
|
|
263
|
+
type KindRefOf<E> = E extends {
|
|
264
|
+
readonly kind: infer K extends string;
|
|
265
|
+
} ? {
|
|
266
|
+
readonly __ref: `${K}:${string}`;
|
|
267
|
+
} : never;
|
|
262
268
|
type PinComponentAdjustment<E, P extends string> = {
|
|
263
|
-
override?: PinComponentBody<E, P>;
|
|
269
|
+
override?: PinComponentBody<E, P> | KindRefOf<E>;
|
|
264
270
|
extend?: PinComponentBody<E, P>;
|
|
265
271
|
hidden?: boolean;
|
|
266
272
|
};
|
|
@@ -280,7 +286,7 @@ type LinkDeclaration = '*' | Record<string, '*' | readonly string[]>;
|
|
|
280
286
|
/** One `options` entry, checked against the kind its accessor addresses. The runtime floor — that an
|
|
281
287
|
* entry names at least one of the three — is the schema's `refine`, which reports the path. */
|
|
282
288
|
type PinAdjustment<E> = {
|
|
283
|
-
override?: StoredBodyOf<PartialBodyOf<E
|
|
289
|
+
override?: StoredBodyOf<PartialBodyOf<E>> | KindRefOf<E>;
|
|
284
290
|
extend?: StoredBodyOf<PartialBodyOf<E>>;
|
|
285
291
|
hidden?: boolean;
|
|
286
292
|
};
|
|
@@ -296,6 +302,7 @@ type StoredBodyOf<Body> = 'styles' extends keyof Body ? Omit<Body, 'styles'> & {
|
|
|
296
302
|
* the same entry, with nothing to check the bodies against yet.
|
|
297
303
|
*/
|
|
298
304
|
interface PinDirective {
|
|
305
|
+
/** A body, or a ref that re-points the path to another entity of the same kind. */
|
|
299
306
|
readonly override?: Record<string, unknown>;
|
|
300
307
|
readonly extend?: Record<string, unknown>;
|
|
301
308
|
readonly hidden?: boolean;
|
|
@@ -428,5 +435,23 @@ declare function suggestLinkSlug({
|
|
|
428
435
|
name: string;
|
|
429
436
|
taken: ReadonlySet<string>;
|
|
430
437
|
}): string;
|
|
438
|
+
/**
|
|
439
|
+
* A link's `options` entry whose `override` is a ref: the path it names reads as that entity
|
|
440
|
+
* (`'js/scale': { override: Motion.ref('css/scale') }`). `problem` says why it can't: the ref names
|
|
441
|
+
* another kind, it leads back to this path through other re-pointed paths, or, once the link's
|
|
442
|
+
* source is attached, it names nothing.
|
|
443
|
+
*/
|
|
444
|
+
interface LinkRedirect {
|
|
445
|
+
/** The link's slug. */
|
|
446
|
+
readonly link: string;
|
|
447
|
+
readonly accessor: string;
|
|
448
|
+
readonly path: string;
|
|
449
|
+
readonly target: string;
|
|
450
|
+
readonly problem?: 'kind' | 'cycle' | 'missing';
|
|
451
|
+
}
|
|
452
|
+
/** Every re-pointed path the config's links declare, with what stops each one, if anything. */
|
|
453
|
+
declare function linkRedirects(config: Config): LinkRedirect[];
|
|
454
|
+
/** One sentence for a re-pointed path that can't be read. */
|
|
455
|
+
declare function redirectSentence(redirect: LinkRedirect): string;
|
|
431
456
|
//#endregion
|
|
432
|
-
export { LINK_SLUG, LINK_SLUG_MESSAGE, LinkDeclaration, LinkedSystem, LinkedSystemClass, LinkedSystemCreateInput, LinkedSystemItemInput, LinkedSystemResolver, LinkedSystemState, PinDirective, PinOptions, PinOptionsField, PinOptionsInput, ResolvedSource, Source, SourceUnavailable, borrowedGroup, borrowedItem, isLinkSlug, resolvedSource, sourceOfGroup, sourceOfItem, sourceSlugFor, sourceUnavailable, suggestLinkSlug };
|
|
457
|
+
export { LINK_SLUG, LINK_SLUG_MESSAGE, LinkDeclaration, LinkRedirect, LinkedSystem, LinkedSystemClass, LinkedSystemCreateInput, LinkedSystemItemInput, LinkedSystemResolver, LinkedSystemState, PinDirective, PinOptions, PinOptionsField, PinOptionsInput, ResolvedSource, Source, SourceUnavailable, borrowedGroup, borrowedItem, isLinkSlug, linkRedirects, redirectSentence, resolvedSource, sourceOfGroup, sourceOfItem, sourceSlugFor, sourceUnavailable, suggestLinkSlug };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { rejectData } from "../../framework/rejection.js";
|
|
2
|
-
import { isRef } from "../../framework/utils/refs.js";
|
|
2
|
+
import { isRef, kindOf, pathOf, refSchema } from "../../framework/utils/refs.js";
|
|
3
3
|
import { isPlainObject } from "../../framework/utils/field-path.js";
|
|
4
4
|
import { pathInput } from "../../framework/schemas.js";
|
|
5
5
|
import { kindBorrowable } from "../../framework/registry.js";
|
|
@@ -262,7 +262,7 @@ function exampleBorrowable(config, slug) {
|
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
264
|
const pinOptions = z.record(z.string(), z.record(z.string(), z.strictObject({
|
|
265
|
-
override: z.record(z.string(), z.unknown()).optional(),
|
|
265
|
+
override: z.union([refSchema, z.record(z.string(), z.unknown())]).optional(),
|
|
266
266
|
extend: z.record(z.string(), z.unknown()).optional(),
|
|
267
267
|
hidden: z.boolean().optional()
|
|
268
268
|
}).refine((entry) => entry.override !== void 0 || entry.extend !== void 0 || entry.hidden !== void 0, { message: "An `options` entry needs `override`, `extend` or `hidden`." }))).optional().describe("Per-entity adjustments, keyed by accessor, then path, then verb.");
|
|
@@ -270,7 +270,7 @@ const LinkedSystemDefinition = defineEntity({
|
|
|
270
270
|
kind: "linked-system",
|
|
271
271
|
label: "Linked system",
|
|
272
272
|
labelPlural: "Linked systems",
|
|
273
|
-
invariants: (config) => config.list("linked-system").filter(({ path }) => !isLinkSlug(path)).map(({ path }) => `Link "${path}": ${LINK_SLUG_MESSAGE}`),
|
|
273
|
+
invariants: (config) => [...config.list("linked-system").filter(({ path }) => !isLinkSlug(path)).map(({ path }) => `Link "${path}": ${LINK_SLUG_MESSAGE}`), ...linkRedirects(config).filter((redirect) => redirect.problem !== void 0 && redirect.problem !== "missing").map(redirectSentence)],
|
|
274
274
|
schemas: {
|
|
275
275
|
create: { example: ({ config }) => {
|
|
276
276
|
const path = freeExampleSlug(config);
|
|
@@ -892,5 +892,56 @@ function suggestLinkSlug({ name, taken }) {
|
|
|
892
892
|
if (!taken.has(candidate)) return candidate;
|
|
893
893
|
}
|
|
894
894
|
}
|
|
895
|
+
/** Every re-pointed path the config's links declare, with what stops each one, if anything. */
|
|
896
|
+
function linkRedirects(config) {
|
|
897
|
+
const kindOfAccessor = (accessor) => config.accessors()[accessor]?.kind ?? accessor;
|
|
898
|
+
const declared = [];
|
|
899
|
+
for (const link of config.list("linked-system")) {
|
|
900
|
+
const { options } = link.toJSON();
|
|
901
|
+
for (const [accessor, entries] of Object.entries(options ?? {})) for (const [path, entry] of Object.entries(entries ?? {})) {
|
|
902
|
+
const override = isPlainObject(entry) ? entry.override : void 0;
|
|
903
|
+
if (!isRef(override)) continue;
|
|
904
|
+
declared.push({
|
|
905
|
+
link: link.path,
|
|
906
|
+
accessor,
|
|
907
|
+
path,
|
|
908
|
+
target: override.__ref
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
const next = /* @__PURE__ */ new Map();
|
|
913
|
+
for (const { accessor, path, target } of declared) next.set(`${kindOfAccessor(accessor)}:${path}`, `${kindOf(target)}:${pathOf(target)}`);
|
|
914
|
+
const loops = (start) => {
|
|
915
|
+
const seen = /* @__PURE__ */ new Set();
|
|
916
|
+
for (let at = start; at !== void 0; at = next.get(at)) {
|
|
917
|
+
if (seen.has(at)) return at === start;
|
|
918
|
+
seen.add(at);
|
|
919
|
+
}
|
|
920
|
+
return false;
|
|
921
|
+
};
|
|
922
|
+
return declared.map((redirect) => {
|
|
923
|
+
const kind = kindOfAccessor(redirect.accessor);
|
|
924
|
+
if (kindOf(redirect.target) !== kind) return {
|
|
925
|
+
...redirect,
|
|
926
|
+
problem: "kind"
|
|
927
|
+
};
|
|
928
|
+
if (loops(`${kind}:${redirect.path}`)) return {
|
|
929
|
+
...redirect,
|
|
930
|
+
problem: "cycle"
|
|
931
|
+
};
|
|
932
|
+
if (config.sourceFor(redirect.link) && !config.resolveRef(redirect.target)) return {
|
|
933
|
+
...redirect,
|
|
934
|
+
problem: "missing"
|
|
935
|
+
};
|
|
936
|
+
return redirect;
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
/** One sentence for a re-pointed path that can't be read. */
|
|
940
|
+
function redirectSentence(redirect) {
|
|
941
|
+
const at = `Link "${redirect.link}": options.${redirect.accessor}["${redirect.path}"] re-points to ${redirect.target}`;
|
|
942
|
+
if (redirect.problem === "kind") return `${at}, which is no ${redirect.accessor} entry. A path can only read as another entity of its own kind.`;
|
|
943
|
+
if (redirect.problem === "cycle") return `${at}, which leads back to it through other re-pointed paths.`;
|
|
944
|
+
return `${at}, which names nothing this config can read.`;
|
|
945
|
+
}
|
|
895
946
|
//#endregion
|
|
896
|
-
export { LINK_SLUG, LINK_SLUG_MESSAGE, LinkedSystem, borrowedGroup, borrowedItem, isLinkSlug, resolvedSource, sourceOfGroup, sourceOfItem, sourceSlugFor, sourceUnavailable, suggestLinkSlug };
|
|
947
|
+
export { LINK_SLUG, LINK_SLUG_MESSAGE, LinkedSystem, borrowedGroup, borrowedItem, isLinkSlug, linkRedirects, redirectSentence, resolvedSource, sourceOfGroup, sourceOfItem, sourceSlugFor, sourceUnavailable, suggestLinkSlug };
|
|
@@ -73,7 +73,7 @@ interface RecordedSource {
|
|
|
73
73
|
* The dense-counter `2` a short-lived scheme stamped before the chain existed reads as the mint
|
|
74
74
|
* that replaced it (see `detectedWireVersion`).
|
|
75
75
|
*/
|
|
76
|
-
declare const SERIALIZED_CONFIG_VERSION =
|
|
76
|
+
declare const SERIALIZED_CONFIG_VERSION = 20261009193004;
|
|
77
77
|
interface SerializedConfig {
|
|
78
78
|
/** See {@link SERIALIZED_CONFIG_VERSION}. Always written; a stored artifact may lack it. */
|
|
79
79
|
readonly version: number;
|
|
@@ -1328,6 +1328,31 @@ declare class Config {
|
|
|
1328
1328
|
* only as a borrow the picker offered and the read then refused.
|
|
1329
1329
|
*/
|
|
1330
1330
|
private extensionBody;
|
|
1331
|
+
/**
|
|
1332
|
+
* A source's body with a linked extension's `extend` laid over it, the one merge every read and
|
|
1333
|
+
* write of a borrowed row goes through. An `extend` whose `__ref` names another entity of the kind
|
|
1334
|
+
* (a link's `override: Motion.ref('css/scale')`) re-points the path: it reads as that entity, as
|
|
1335
|
+
* this config resolves it, with the rest of the `extend` over it. The kind's
|
|
1336
|
+
* {@link EntityKind.rebaseLinkedBody} then adapts the base before the merge.
|
|
1337
|
+
*
|
|
1338
|
+
* A target that resolves to nothing, names another kind, or leads back to this path through other
|
|
1339
|
+
* re-pointed paths leaves an empty body, which the kind's schema refuses, so the row reads as
|
|
1340
|
+
* nothing. The pin's invariants refuse the last two when they're written, and the build reports a
|
|
1341
|
+
* missing target (`brokenLinkRedirects`).
|
|
1342
|
+
*/
|
|
1343
|
+
laidLinkedBody({
|
|
1344
|
+
kind,
|
|
1345
|
+
path,
|
|
1346
|
+
body,
|
|
1347
|
+
extend
|
|
1348
|
+
}: {
|
|
1349
|
+
kind: string;
|
|
1350
|
+
path: string;
|
|
1351
|
+
body: Record<string, unknown>;
|
|
1352
|
+
extend: Record<string, unknown>;
|
|
1353
|
+
}): Record<string, unknown>;
|
|
1354
|
+
/** The body of the entity a re-pointed path names, or `undefined` when there is none to read. */
|
|
1355
|
+
private redirectedBody;
|
|
1331
1356
|
/**
|
|
1332
1357
|
* Resolve one locally extended linked item: the pinned source body, shallowly overlaid by the fields
|
|
1333
1358
|
* authored here, then parsed through this config's kind. Parsing on this side is what lets a React
|
package/dist/framework/Config.js
CHANGED
|
@@ -1800,7 +1800,12 @@ var Config = class Config {
|
|
|
1800
1800
|
kind: patch.kind,
|
|
1801
1801
|
path: pathOf(extension)
|
|
1802
1802
|
}) : void 0;
|
|
1803
|
-
const inherited = current ?
|
|
1803
|
+
const inherited = current ? this.at(snapshot).laidLinkedBody({
|
|
1804
|
+
kind: patch.kind,
|
|
1805
|
+
path: pathOf(extension),
|
|
1806
|
+
body: current.source,
|
|
1807
|
+
extend: current.overlay
|
|
1808
|
+
}) : resolved.toJSON();
|
|
1804
1809
|
const materialized = snapshot.withChanges([itemChange(entry, patch.kind, patch.path, inherited)]);
|
|
1805
1810
|
const changed = this.applySub(materialized, patch, materialized.get(patch.kind, patch.path) ?? resolved, mode);
|
|
1806
1811
|
const body = changed.get(patch.kind, patch.path)?.toJSON();
|
|
@@ -1884,24 +1889,24 @@ var Config = class Config {
|
|
|
1884
1889
|
kind: patch.kind,
|
|
1885
1890
|
path: pathOf(extension)
|
|
1886
1891
|
}) : void 0;
|
|
1887
|
-
const written = applyPathDelta(current ?
|
|
1888
|
-
|
|
1892
|
+
const written = applyPathDelta(current ? this.at(snapshot).laidLinkedBody({
|
|
1893
|
+
kind: patch.kind,
|
|
1889
1894
|
path: pathOf(extension),
|
|
1890
1895
|
body: current.source,
|
|
1891
1896
|
extend: current.overlay
|
|
1892
|
-
})
|
|
1897
|
+
}) : resolved.toJSON(), delta, isField);
|
|
1893
1898
|
const local = { ...current ? current.overlay : extension.extend };
|
|
1894
1899
|
for (const field of touched) if (Object.hasOwn(delta, field) && delta[field] === null) delete local[field];
|
|
1895
1900
|
else if (Object.hasOwn(written, field)) local[field] = written[field];
|
|
1896
1901
|
else delete local[field];
|
|
1897
1902
|
const sourceBody = this.configFor(extension)?.resolve(patch.kind, pathOf(extension))?.toJSON();
|
|
1898
1903
|
if (!sourceBody) reject("NOT_FOUND", `Cannot edit ${patch.kind} "${patch.path}" — its source "${extension.__ref}" is unavailable`);
|
|
1899
|
-
const candidate =
|
|
1900
|
-
|
|
1904
|
+
const candidate = this.at(snapshot).laidLinkedBody({
|
|
1905
|
+
kind: patch.kind,
|
|
1901
1906
|
path: pathOf(extension),
|
|
1902
1907
|
body: sourceBody,
|
|
1903
1908
|
extend: local
|
|
1904
|
-
})
|
|
1909
|
+
});
|
|
1905
1910
|
const createSchema = wholeBodySchema(entry, { config: this });
|
|
1906
1911
|
let merged = candidate;
|
|
1907
1912
|
if (createSchema) {
|
|
@@ -1920,8 +1925,12 @@ var Config = class Config {
|
|
|
1920
1925
|
if (!sourceKind || !write) reject("UNSUPPORTED", `Cannot edit ${patch.kind} "${patch.path}" — "${slug}" has no way to record the change`);
|
|
1921
1926
|
const accessor = view.accessorOf(patch.kind) ?? patch.kind;
|
|
1922
1927
|
const existing = view.sourceOverlay(slug, patch.kind, patch.path);
|
|
1928
|
+
const { target: redirect, rest } = redirectOf(local);
|
|
1923
1929
|
const overlay = {
|
|
1924
|
-
...classifyOverlay(local, sourceBody)
|
|
1930
|
+
...redirect === void 0 ? classifyOverlay(local, sourceBody) : {
|
|
1931
|
+
override: { __ref: redirect },
|
|
1932
|
+
...Object.keys(rest).length > 0 ? { extend: rest } : {}
|
|
1933
|
+
},
|
|
1925
1934
|
...existing?.hidden ? { hidden: true } : {}
|
|
1926
1935
|
};
|
|
1927
1936
|
const data = view.directiveWrite({
|
|
@@ -2642,13 +2651,12 @@ var Config = class Config {
|
|
|
2642
2651
|
owns,
|
|
2643
2652
|
namesItAlready: (target) => this.borrowsExactly(target, slug)
|
|
2644
2653
|
}) : base.toJSON();
|
|
2645
|
-
const
|
|
2646
|
-
|
|
2647
|
-
entry: this.entityOf(kind),
|
|
2654
|
+
const merged = this.laidLinkedBody({
|
|
2655
|
+
kind,
|
|
2648
2656
|
path: pathOf(extension),
|
|
2649
2657
|
body: inherited,
|
|
2650
|
-
extend
|
|
2651
|
-
})
|
|
2658
|
+
extend: extension.extend ?? {}
|
|
2659
|
+
});
|
|
2652
2660
|
return this.entityOf(kind)?.prepareLinkedBody?.({
|
|
2653
2661
|
path: pathOf(extension),
|
|
2654
2662
|
body: merged,
|
|
@@ -2657,6 +2665,46 @@ var Config = class Config {
|
|
|
2657
2665
|
}) ?? merged;
|
|
2658
2666
|
}
|
|
2659
2667
|
/**
|
|
2668
|
+
* A source's body with a linked extension's `extend` laid over it, the one merge every read and
|
|
2669
|
+
* write of a borrowed row goes through. An `extend` whose `__ref` names another entity of the kind
|
|
2670
|
+
* (a link's `override: Motion.ref('css/scale')`) re-points the path: it reads as that entity, as
|
|
2671
|
+
* this config resolves it, with the rest of the `extend` over it. The kind's
|
|
2672
|
+
* {@link EntityKind.rebaseLinkedBody} then adapts the base before the merge.
|
|
2673
|
+
*
|
|
2674
|
+
* A target that resolves to nothing, names another kind, or leads back to this path through other
|
|
2675
|
+
* re-pointed paths leaves an empty body, which the kind's schema refuses, so the row reads as
|
|
2676
|
+
* nothing. The pin's invariants refuse the last two when they're written, and the build reports a
|
|
2677
|
+
* missing target (`brokenLinkRedirects`).
|
|
2678
|
+
*/
|
|
2679
|
+
laidLinkedBody({ kind, path, body, extend }) {
|
|
2680
|
+
const { target, rest } = redirectOf(extend);
|
|
2681
|
+
const base = target === void 0 ? body : this.redirectedBody({
|
|
2682
|
+
kind,
|
|
2683
|
+
path,
|
|
2684
|
+
target
|
|
2685
|
+
}) ?? {};
|
|
2686
|
+
return extendBody(rebasedLinkedBody({
|
|
2687
|
+
entry: this.entityOf(kind),
|
|
2688
|
+
path,
|
|
2689
|
+
body: base,
|
|
2690
|
+
extend: rest
|
|
2691
|
+
}), rest);
|
|
2692
|
+
}
|
|
2693
|
+
/** The body of the entity a re-pointed path names, or `undefined` when there is none to read. */
|
|
2694
|
+
redirectedBody({ kind, path, target }) {
|
|
2695
|
+
if (kindOf(target) !== kind) return void 0;
|
|
2696
|
+
const reading = redirecting.get(this.state) ?? /* @__PURE__ */ new Set();
|
|
2697
|
+
const key = `${kind}:${path}`;
|
|
2698
|
+
if (reading.has(key)) return void 0;
|
|
2699
|
+
reading.add(key);
|
|
2700
|
+
redirecting.set(this.state, reading);
|
|
2701
|
+
try {
|
|
2702
|
+
return this.resolveRef(target)?.toJSON();
|
|
2703
|
+
} finally {
|
|
2704
|
+
reading.delete(key);
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
/**
|
|
2660
2708
|
* Resolve one locally extended linked item: the pinned source body, shallowly overlaid by the fields
|
|
2661
2709
|
* authored here, then parsed through this config's kind. Parsing on this side is what lets a React
|
|
2662
2710
|
* Native kind transform or reject a web value rather than trusting the source kind's rules.
|
|
@@ -3552,6 +3600,12 @@ var Config = class Config {
|
|
|
3552
3600
|
},
|
|
3553
3601
|
theirs
|
|
3554
3602
|
});
|
|
3603
|
+
if (laid && redirectOf(laid).target !== void 0) return this.laidLinkedBody({
|
|
3604
|
+
kind,
|
|
3605
|
+
path,
|
|
3606
|
+
body: theirs,
|
|
3607
|
+
extend: laid
|
|
3608
|
+
});
|
|
3555
3609
|
return laid ? {
|
|
3556
3610
|
...theirs,
|
|
3557
3611
|
...laid
|
|
@@ -5458,7 +5512,7 @@ var Config = class Config {
|
|
|
5458
5512
|
* links from the provided sources. A declared `linkedKind` with no provided source throws. */
|
|
5459
5513
|
hydrateFrom(stored, options) {
|
|
5460
5514
|
const claimed = schemaVersionOf(stored);
|
|
5461
|
-
if (claimed >
|
|
5515
|
+
if (claimed > 20261009193004) throw new SchemaVersionTooNew(claimed);
|
|
5462
5516
|
const wire = upgradeSerializedConfig(stored);
|
|
5463
5517
|
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\`.`);
|
|
5464
5518
|
const leftover = wire.options;
|
|
@@ -5623,6 +5677,20 @@ function extendBody(base, delta) {
|
|
|
5623
5677
|
* differently, and why the source's body is a parameter. `extend: { values: [Token.ref('surface')] }`
|
|
5624
5678
|
* means "alongside what they already accept", which has nothing to join without their list.
|
|
5625
5679
|
*/
|
|
5680
|
+
/** The paths each config is reading through a re-pointed link, for {@link Config.laidLinkedBody} to
|
|
5681
|
+
* find a loop by. Keyed on the config's state, which every read of one config shares. */
|
|
5682
|
+
const redirecting = /* @__PURE__ */ new WeakMap();
|
|
5683
|
+
/**
|
|
5684
|
+
* The entity an `extend` re-points its path to, and the rest of it: a link's `override` written as a
|
|
5685
|
+
* ref lands in the stored `extend` as its `__ref`, beside whatever the `extend` verb adds.
|
|
5686
|
+
*/
|
|
5687
|
+
function redirectOf(extend) {
|
|
5688
|
+
const { __ref: target, ...rest } = extend;
|
|
5689
|
+
return typeof target === "string" ? {
|
|
5690
|
+
target,
|
|
5691
|
+
rest
|
|
5692
|
+
} : { rest: extend };
|
|
5693
|
+
}
|
|
5626
5694
|
/** A source's body as an extension reads it before its `extend` merges in: the kind's
|
|
5627
5695
|
* {@link EntityKind.rebaseLinkedBody}, where it declares one. */
|
|
5628
5696
|
function rebasedLinkedBody({ entry, path, body, extend }) {
|
|
@@ -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 = 20261009193004;
|
|
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 = 20261009193004;
|
|
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 > 20261009193004) 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 ?? 20261009193004;
|
|
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 ?? 20261009193004;
|
|
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 ?? 20261009193004;
|
|
183
183
|
const entries = [];
|
|
184
184
|
const held = [];
|
|
185
185
|
const retired = [];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A link's `options` entry may give `override` as a ref to another entity of the same kind, which
|
|
3
|
+
* re-points the path to it (`'js/scale': { override: Motion.ref('css/scale') }`). Every stored
|
|
4
|
+
* `override` is a body, so the wire shape of every stored config is unchanged. Additive, so the
|
|
5
|
+
* envelope passes through as it is.
|
|
6
|
+
*/
|
|
7
|
+
import type { SchemaMigration } from '../framework/schema-version';
|
|
8
|
+
export declare const LINK_OVERRIDE_REF_VERSION = 20261009193004;
|
|
9
|
+
export declare const linkOverrideRefMigration: SchemaMigration;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/migrations/20261009193004_link_override_ref.ts
|
|
2
|
+
const LINK_OVERRIDE_REF_VERSION = 20261009193004;
|
|
3
|
+
const linkOverrideRefMigration = {
|
|
4
|
+
version: LINK_OVERRIDE_REF_VERSION,
|
|
5
|
+
up(json) {
|
|
6
|
+
return json;
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
//#endregion
|
|
10
|
+
export { LINK_OVERRIDE_REF_VERSION, linkOverrideRefMigration };
|