@yahoo/uds-create-config 5.27.1 → 5.28.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 +12 -0
- package/dist/entities/system/LinkedSystem.js +114 -18
- package/dist/entities/system/link-declaration.d.ts +43 -2
- package/dist/entities/system/link-declaration.js +110 -15
- package/dist/entities/system/link-plan.js +109 -4
- package/dist/framework/Config.d.ts +6 -1
- package/dist/framework/Config.js +28 -6
- package/dist/framework/defineEntity.d.ts +4 -3
- package/dist/framework/defineEntity.js +1 -0
- package/dist/framework/registry.d.ts +5 -0
- package/dist/framework/registry.js +2 -1
- package/dist/framework/rejection.d.ts +4 -2
- package/dist/framework/schema-version.d.ts +1 -1
- package/dist/framework/schema-version.js +5 -5
- package/dist/framework/source-integrity.d.ts +9 -2
- package/dist/framework/source-integrity.js +21 -2
- package/dist/framework/utils/refs.d.ts +14 -2
- package/dist/framework/utils/refs.js +22 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/migrations/20261013182150_link_exclude_op.d.ts +13 -0
- package/dist/migrations/20261013182150_link_exclude_op.js +10 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
package/dist/configs/system.js
CHANGED
|
@@ -68,6 +68,7 @@ import { linkOverrideRefMigration } from "../migrations/20261009193004_link_over
|
|
|
68
68
|
import { linkedComponentModulesMigration } from "../migrations/20261012200606_linked_component_modules.js";
|
|
69
69
|
import { componentStatesMigration } from "../migrations/20261013154037_component_states.js";
|
|
70
70
|
import { tokenOverridesListMigration } from "../migrations/20261013164300_token_overrides_list.js";
|
|
71
|
+
import { linkExcludeOpMigration } from "../migrations/20261013182150_link_exclude_op.js";
|
|
71
72
|
const System = defineConfig({
|
|
72
73
|
name: WEB_CONFIG_NAME,
|
|
73
74
|
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.",
|
|
@@ -147,5 +148,6 @@ if (!registeredSchemaMigrations().has(20261009193004)) registerSchemaMigrations(
|
|
|
147
148
|
if (!registeredSchemaMigrations().has(20261012200606)) registerSchemaMigrations(linkedComponentModulesMigration);
|
|
148
149
|
if (!registeredSchemaMigrations().has(20261013154037)) registerSchemaMigrations(componentStatesMigration);
|
|
149
150
|
if (!registeredSchemaMigrations().has(20261013164300)) registerSchemaMigrations(tokenOverridesListMigration);
|
|
151
|
+
if (!registeredSchemaMigrations().has(20261013182150)) registerSchemaMigrations(linkExcludeOpMigration);
|
|
150
152
|
//#endregion
|
|
151
153
|
export { System };
|
|
@@ -143,6 +143,18 @@ declare const LinkedSystemDefinition: EntityClass<z.ZodObject<{
|
|
|
143
143
|
data: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"*">, z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodLiteral<"*">, z.ZodArray<z.ZodString>]>>]>>;
|
|
144
144
|
}, z.core.$strip>;
|
|
145
145
|
};
|
|
146
|
+
readonly exclude: {
|
|
147
|
+
readonly input: z.ZodObject<{
|
|
148
|
+
path: z.ZodType<string, string | undefined, z.core.$ZodTypeInternals<string, string | undefined>>;
|
|
149
|
+
data: z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>;
|
|
150
|
+
}, z.core.$strip>;
|
|
151
|
+
};
|
|
152
|
+
readonly 'exclude-plan': {
|
|
153
|
+
readonly input: z.ZodObject<{
|
|
154
|
+
path: z.ZodType<string, string | undefined, z.core.$ZodTypeInternals<string, string | undefined>>;
|
|
155
|
+
data: z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>;
|
|
156
|
+
}, z.core.$strip>;
|
|
157
|
+
};
|
|
146
158
|
readonly break: {
|
|
147
159
|
readonly input: z.ZodObject<{
|
|
148
160
|
path: z.ZodType<string, string | undefined, z.core.$ZodTypeInternals<string, string | undefined>>;
|
|
@@ -6,8 +6,9 @@ import { kindBorrowable } from "../../framework/registry.js";
|
|
|
6
6
|
import { defineEntity } from "../../framework/defineEntity.js";
|
|
7
7
|
import { storedStyleRules } from "./Component.js";
|
|
8
8
|
import { sourceUnreachable } from "../../framework/sources.js";
|
|
9
|
+
import { covers, excludes } from "./link-declaration.js";
|
|
9
10
|
import { planCopy } from "./copy-plan.js";
|
|
10
|
-
import { planBreak, planLink, planUnlink, sourceRowStates } from "./link-plan.js";
|
|
11
|
+
import { planBreak, planExclude, planLink, planUnlink, sourceRowStates } from "./link-plan.js";
|
|
11
12
|
import { z } from "zod";
|
|
12
13
|
//#region src/entities/system/LinkedSystem.ts
|
|
13
14
|
/**
|
|
@@ -177,6 +178,12 @@ const unlinkInput = z.object({
|
|
|
177
178
|
/** What to stop drawing, in `link`'s shape; everything when omitted. */
|
|
178
179
|
data: z.union([z.literal("*"), z.record(z.string(), copyPaths)]).optional()
|
|
179
180
|
});
|
|
181
|
+
const excludeInput = z.object({
|
|
182
|
+
path: pathInput("linked-system"),
|
|
183
|
+
/** What to leave out, keyed by accessor the way the pin's `link` is: the source's paths, where a
|
|
184
|
+
* folder leaves out its members. */
|
|
185
|
+
data: z.record(z.string(), z.array(z.string().min(1)).min(1))
|
|
186
|
+
});
|
|
180
187
|
const breakInput = z.object({
|
|
181
188
|
path: pathInput("linked-system"),
|
|
182
189
|
/** Whether every value drawn through the pin becomes this config's own first. */
|
|
@@ -197,6 +204,13 @@ function copyInputOf(input) {
|
|
|
197
204
|
...input.onConflict === void 0 ? {} : { onConflict: input.onConflict }
|
|
198
205
|
};
|
|
199
206
|
}
|
|
207
|
+
/** The parsed `exclude` input as the plan takes it. */
|
|
208
|
+
function excludePlanInputOf(input) {
|
|
209
|
+
return {
|
|
210
|
+
path: String(input.path),
|
|
211
|
+
data: input.data
|
|
212
|
+
};
|
|
213
|
+
}
|
|
200
214
|
/** The parsed `link` input as the plan takes it. */
|
|
201
215
|
function linkPlanInputOf(input) {
|
|
202
216
|
return {
|
|
@@ -270,7 +284,11 @@ const LinkedSystemDefinition = defineEntity({
|
|
|
270
284
|
kind: "linked-system",
|
|
271
285
|
label: "Linked system",
|
|
272
286
|
labelPlural: "Linked systems",
|
|
273
|
-
invariants: (config) => [
|
|
287
|
+
invariants: (config) => [
|
|
288
|
+
...config.list("linked-system").filter(({ path }) => !isLinkSlug(path)).map(({ path }) => `Link "${path}": ${LINK_SLUG_MESSAGE}`),
|
|
289
|
+
...linkRedirects(config).filter((redirect) => redirect.problem !== void 0 && redirect.problem !== "missing").map(redirectSentence),
|
|
290
|
+
...excludedDirectives(config)
|
|
291
|
+
],
|
|
274
292
|
schemas: {
|
|
275
293
|
create: { example: ({ config }) => {
|
|
276
294
|
const path = freeExampleSlug(config);
|
|
@@ -372,6 +390,45 @@ const LinkedSystemDefinition = defineEntity({
|
|
|
372
390
|
}
|
|
373
391
|
},
|
|
374
392
|
/**
|
|
393
|
+
* Leave part of a linked system out for good: a `!` entry per path in the pin's `link`. Unlike
|
|
394
|
+
* `unlink`, nothing derives there however the link reaches it, and the linked tokens' overrides
|
|
395
|
+
* under an excluded modifier are left out of the build. `link` with the same path lifts it.
|
|
396
|
+
*/
|
|
397
|
+
exclude: {
|
|
398
|
+
input: excludeInput,
|
|
399
|
+
readOnly: false,
|
|
400
|
+
label: "Exclude from",
|
|
401
|
+
description: "Leave paths of a linked system out, keyed by accessor the way `link` is: `{ modifiers: ['text'] }` leaves out the `text` folder's members. Stronger than `unlink`: nothing derives at an excluded path however the link reaches it, even when a linked token's override names it, and the source's token overrides under an excluded modifier are left out of the build. The pin's `options` entries at those paths go with them. `exclude-plan` names what goes; `link` with the same path brings it back.",
|
|
402
|
+
scope: "item",
|
|
403
|
+
approval: "reference-change",
|
|
404
|
+
title: (entity) => `Exclude something from ${entity}`,
|
|
405
|
+
example: linkExample,
|
|
406
|
+
handler: (input, config) => {
|
|
407
|
+
const { patches, blocked } = planExclude({
|
|
408
|
+
config,
|
|
409
|
+
input: excludePlanInputOf(input)
|
|
410
|
+
});
|
|
411
|
+
if (blocked !== void 0) rejectData(blocked);
|
|
412
|
+
return config.applyAll(patches);
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
"exclude-plan": {
|
|
416
|
+
input: excludeInput,
|
|
417
|
+
readOnly: true,
|
|
418
|
+
label: "Plan an exclusion",
|
|
419
|
+
description: "What `exclude` with the same input leaves out — the minted rows that stop deriving, the linked tokens' overrides under an excluded modifier (`leftOut`), and the pin's `options` entries that go with the paths (`dropped`) — and why the config would refuse it. Writes nothing.",
|
|
420
|
+
scope: "item",
|
|
421
|
+
title: (entity) => `Plan an exclusion from ${entity}`,
|
|
422
|
+
example: linkExample,
|
|
423
|
+
handler: (input, config) => {
|
|
424
|
+
const { patches: _patches, ...plan } = planExclude({
|
|
425
|
+
config,
|
|
426
|
+
input: excludePlanInputOf(input)
|
|
427
|
+
});
|
|
428
|
+
return plan;
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
/**
|
|
375
432
|
* Break the link: the pin goes, and everything drawn through it either becomes this config's own
|
|
376
433
|
* or goes with it.
|
|
377
434
|
*/
|
|
@@ -541,6 +598,7 @@ const LinkedSystemDefinition = defineEntity({
|
|
|
541
598
|
* link: { tokens: '*', icons: '*' } // those kinds whole
|
|
542
599
|
* link: { tokens: ['color/brand', 'color/paper'] } // those entities
|
|
543
600
|
* link: { components: ['actions'] } // a folder: its members
|
|
601
|
+
* link: { '*': '*', modifiers: ['*', '!text'] } // everything but one folder
|
|
544
602
|
* ```
|
|
545
603
|
* `Config.withDerivedRows` reads it and makes a pointer row per entity it covers — on every build,
|
|
546
604
|
* and inside the apply of any patch to this pin — so a link's cost in authored source is this one
|
|
@@ -566,14 +624,38 @@ const LinkedSystemDefinition = defineEntity({
|
|
|
566
624
|
* build would make it again on the next pass, and a delete that comes back is worse than one that
|
|
567
625
|
* was refused. To own one of their entities, `copy` it; to stop drawing it, `unlink` it.
|
|
568
626
|
*
|
|
569
|
-
* The gestures that write this are the kind's own ops — `link`, `unlink`, `break` —
|
|
570
|
-
* `-plan` read beside it, so a surface never assembles the declaration by hand.
|
|
627
|
+
* The gestures that write this are the kind's own ops — `link`, `unlink`, `exclude`, `break` —
|
|
628
|
+
* each with a `-plan` read beside it, so a surface never assembles the declaration by hand.
|
|
571
629
|
*
|
|
572
630
|
* `'*'` means every kind that may be drawn on, which is every kind that may be borrowed — each
|
|
573
631
|
* kind says which it is (`EntityKind.linkable`). Nothing is subtracted: a pin and a `file` aren't
|
|
574
632
|
* borrowable, so no borrow of one exists for this to cover.
|
|
633
|
+
*
|
|
634
|
+
* **Leaving part of a kind out.** A record's `'*'` key stands for every accessor it doesn't name,
|
|
635
|
+
* and a list entry `'*'` for every path, so everything but one folder is spelled
|
|
636
|
+
* ```ts
|
|
637
|
+
* link: { '*': '*', modifiers: ['*', '!text'] }
|
|
638
|
+
* ```
|
|
639
|
+
* A `!` entry leaves that path out, or a folder's members, whatever else in the list reaches it.
|
|
640
|
+
* Unlike a path a list doesn't name, an excluded path derives no row however the link reaches it —
|
|
641
|
+
* a linked token whose override is under a `text` modifier names that modifier, and it still
|
|
642
|
+
* stays out — so the name is free for this config to author, and the source's entity stays
|
|
643
|
+
* reachable qualified. A token's overrides under an excluded modifier are left out of the build
|
|
644
|
+
* rather than refused (`unstatableConditions`), which is what a consumer without one of the
|
|
645
|
+
* source's mode axes means. A linked component's rule keyed on one still reaches the source's
|
|
646
|
+
* modifier, as a rule on any modifier the link doesn't take does. See `covers` and `excludes`.
|
|
647
|
+
*
|
|
648
|
+
* Exclusions win wherever they sit, so `['!text', '*']` is `['*', '!text']`, and a list of them
|
|
649
|
+
* alone (`['!text']`) takes nothing by name and still keeps `text` out. An `options` entry at an
|
|
650
|
+
* excluded path is refused (an invariant), since no row derives there for it to adjust. `exclude`
|
|
651
|
+
* writes one (and drops such entries with it), and `link` lifts one over what it takes. `unlink`
|
|
652
|
+
* keeps them and writes none, because a `!` also drops the overrides under the path, which
|
|
653
|
+
* unlinking never has.
|
|
654
|
+
*
|
|
655
|
+
* No entity path can be misread here: `entityPathProblem` refuses a segment that starts with `!`,
|
|
656
|
+
* a segment that is `*`, and an empty segment, wherever a path becomes an identity.
|
|
575
657
|
*/
|
|
576
|
-
link: z.union([z.literal("*"), z.record(z.string(), z.union([z.literal("*"), z.array(z.string().min(1)).min(1)]))]).optional().describe("What this system draws from the source, standing: `'*'` for everything, or a record by kind."),
|
|
658
|
+
link: z.union([z.literal("*"), z.record(z.string(), z.union([z.literal("*"), z.array(z.string().min(1)).min(1)]))]).optional().describe("What this system draws from the source, standing: `'*'` for everything, or a record by kind (`'*'` keys every kind it doesn't name). A list's `'!path'` entry leaves that path out."),
|
|
577
659
|
/**
|
|
578
660
|
* What this system does to individual entities of the source, keyed by accessor, then by the path
|
|
579
661
|
* the source spells, then by verb.
|
|
@@ -715,20 +797,22 @@ displayName: ({ member, path }) => typeof member.name === "string" && member.nam
|
|
|
715
797
|
* question and the delete gate's, answered here because `link` is this kind's field and its shape
|
|
716
798
|
* is this kind's to interpret.
|
|
717
799
|
*
|
|
718
|
-
* `'*'` at either level covers everything under it
|
|
719
|
-
*
|
|
720
|
-
*
|
|
721
|
-
*
|
|
800
|
+
* `'*'` at either level covers everything under it, and a record's `'*'` key stands for every
|
|
801
|
+
* accessor it doesn't name; a list covers the paths it names, as the source spells them, and a named
|
|
802
|
+
* folder covers its members, less what its `!` entries leave out. Nothing else excludes a kind here
|
|
803
|
+
* — a kind a whole-source take does not reach is one no borrow of it exists for
|
|
804
|
+
* (`EntityKind.linkable`), so the claim is unreachable rather than filtered.
|
|
722
805
|
*/
|
|
723
|
-
bulkAdopts: ({ body, kind, accessor, path }) => {
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
806
|
+
bulkAdopts: ({ body, kind, accessor, path }) => covers({
|
|
807
|
+
declared: body.link,
|
|
808
|
+
accessor: accessor ?? kind,
|
|
809
|
+
path
|
|
810
|
+
}),
|
|
811
|
+
bulkExcludes: ({ body, kind, accessor, path }) => excludes({
|
|
812
|
+
declared: body.link,
|
|
813
|
+
accessor: accessor ?? kind,
|
|
814
|
+
path
|
|
815
|
+
}),
|
|
732
816
|
bulkOverlay: ({ body, kind, accessor, path }) => {
|
|
733
817
|
const directive = directiveAt(body, accessor ?? kind, path);
|
|
734
818
|
if (!directive) return void 0;
|
|
@@ -794,6 +878,18 @@ function linkedSystemItem(def) {
|
|
|
794
878
|
};
|
|
795
879
|
}
|
|
796
880
|
const LinkedSystem = Object.assign(LinkedSystemDefinition, { item: linkedSystemItem });
|
|
881
|
+
/** Each `options` entry at a path its pin's `link` excludes, which derives no row for it to adjust. */
|
|
882
|
+
function excludedDirectives(config) {
|
|
883
|
+
return config.list("linked-system").flatMap(({ path: slug }) => {
|
|
884
|
+
const body = config.storedBody("linked-system", slug);
|
|
885
|
+
if (!body?.options) return [];
|
|
886
|
+
return Object.entries(body.options).flatMap(([accessor, entries]) => isPlainObject(entries) ? Object.keys(entries).filter((path) => excludes({
|
|
887
|
+
declared: body.link,
|
|
888
|
+
accessor,
|
|
889
|
+
path
|
|
890
|
+
})).map((path) => `Link "${slug}": options.${accessor}["${path}"] adjusts a path its \`link\` excludes, which derives no row. Drop the entry, or the exclusion.`) : []);
|
|
891
|
+
});
|
|
892
|
+
}
|
|
797
893
|
/** The `options` entry for one path, or `undefined` where the pin says nothing about it. */
|
|
798
894
|
function directiveAt(body, accessor, path) {
|
|
799
895
|
return body.options?.[accessor]?.[path];
|
|
@@ -3,19 +3,60 @@ import type { LinkDeclaration } from './LinkedSystem';
|
|
|
3
3
|
export declare const PIN_KIND = "linked-system";
|
|
4
4
|
/** The pin's `link` as stored. */
|
|
5
5
|
export declare function declarationOf(config: Config, slug: string): LinkDeclaration | undefined;
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* What a declaration says about one accessor: its own entry, or the record's `'*'` entry, which stands
|
|
8
|
+
* for every accessor the record doesn't name (`{ '*': '*', modifiers: ['*', '!text'] }`).
|
|
9
|
+
*/
|
|
10
|
+
export declare function entriesFor(declared: LinkDeclaration | undefined, accessor: string): '*' | readonly string[] | undefined;
|
|
11
|
+
/** The paths a list leaves out, without their `!`. */
|
|
12
|
+
export declare function excludedIn(entries: '*' | readonly string[] | undefined): string[];
|
|
13
|
+
/**
|
|
14
|
+
* Whether a declaration leaves the accessor's path out on purpose: a `!` entry names it or a folder
|
|
15
|
+
* above it. An excluded path derives no row however the link reaches it, a ref pulling it in included.
|
|
16
|
+
*/
|
|
17
|
+
export declare function excludes({ declared, accessor, path, }: {
|
|
18
|
+
declared: LinkDeclaration | undefined;
|
|
19
|
+
accessor: string;
|
|
20
|
+
path: string;
|
|
21
|
+
}): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Whether a declaration covers the accessor's path: the whole source, the kind, the path, or a folder
|
|
24
|
+
* above it, and no `!` entry leaves it out. An exclusion wins whatever else in the list reaches the
|
|
25
|
+
* path, so `['*', '!text']` is every path but `text`'s.
|
|
26
|
+
*/
|
|
7
27
|
export declare function covers({ declared, accessor, path, }: {
|
|
8
28
|
declared: LinkDeclaration | undefined;
|
|
9
29
|
accessor: string;
|
|
10
30
|
path: string;
|
|
11
31
|
}): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* A list with `paths` taken into it: each is added unless a path the list takes already reaches it,
|
|
34
|
+
* and an exclusion that would leave one out is lifted. One naming the path or a folder under it goes;
|
|
35
|
+
* one naming a folder above it is spelled out as the folder's other members, so they stay out.
|
|
36
|
+
*/
|
|
37
|
+
export declare function includePaths({ config, slug, kind, entries, paths, }: {
|
|
38
|
+
config: Config;
|
|
39
|
+
slug: string;
|
|
40
|
+
kind: string;
|
|
41
|
+
entries: readonly string[];
|
|
42
|
+
paths: readonly string[];
|
|
43
|
+
}): string[];
|
|
44
|
+
/**
|
|
45
|
+
* A list with `paths` left out: a `!` entry for each the list doesn't exclude already. A path the
|
|
46
|
+
* list names under one of them goes, since the exclusion now says it, and so does a narrower
|
|
47
|
+
* exclusion a new one reaches. A folder named above an excluded path stays, less that path. With
|
|
48
|
+
* nothing listed, the list is the exclusions alone, which still keep the paths out when a linked row
|
|
49
|
+
* reaches them.
|
|
50
|
+
*/
|
|
51
|
+
export declare function excludePaths(entries: '*' | readonly string[] | undefined, paths: readonly string[]): string[];
|
|
12
52
|
/**
|
|
13
53
|
* The declaration with `removal` taken out of it, or `undefined` when nothing stays declared.
|
|
14
54
|
*
|
|
15
55
|
* A declaration of everything is spelled out first, one kind per accessor the source has rows for, so
|
|
16
56
|
* one kind can be taken out of it; a kind taken whole is spelled out as the source's top-level paths
|
|
17
57
|
* so some can go; a declared folder above a removed path is spelled out as its other members, so the
|
|
18
|
-
* rest of it stays covered.
|
|
58
|
+
* rest of it stays covered. A list that takes every path (`['*', …]`) is spelled out the same way.
|
|
59
|
+
* Its `!` entries stay, a list of them alone included, and narrowing writes none.
|
|
19
60
|
*/
|
|
20
61
|
export declare function narrowDeclaration({ config, slug, current, removal, verb, }: {
|
|
21
62
|
config: Config;
|
|
@@ -7,14 +7,90 @@ function declarationOf(config, slug) {
|
|
|
7
7
|
if (!pin) rejectData(`No linked system "${slug}".`);
|
|
8
8
|
return pin.link;
|
|
9
9
|
}
|
|
10
|
-
/**
|
|
10
|
+
/** An entry that leaves a path out: `'!text'`. */
|
|
11
|
+
const EXCLUDE = "!";
|
|
12
|
+
/** Whether a declared entry names `path` or a folder above it. */
|
|
13
|
+
const reaches = (entry, path) => entry === "*" || entry === path || path.startsWith(`${entry}/`);
|
|
14
|
+
/**
|
|
15
|
+
* What a declaration says about one accessor: its own entry, or the record's `'*'` entry, which stands
|
|
16
|
+
* for every accessor the record doesn't name (`{ '*': '*', modifiers: ['*', '!text'] }`).
|
|
17
|
+
*/
|
|
18
|
+
function entriesFor(declared, accessor) {
|
|
19
|
+
if (declared === void 0 || declared === "*") return declared;
|
|
20
|
+
return declared[accessor] ?? declared["*"];
|
|
21
|
+
}
|
|
22
|
+
/** The paths a list leaves out, without their `!`. */
|
|
23
|
+
function excludedIn(entries) {
|
|
24
|
+
if (entries === void 0 || entries === "*") return [];
|
|
25
|
+
return entries.filter((entry) => entry.startsWith(EXCLUDE)).map((entry) => entry.slice(1));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Whether a declaration leaves the accessor's path out on purpose: a `!` entry names it or a folder
|
|
29
|
+
* above it. An excluded path derives no row however the link reaches it, a ref pulling it in included.
|
|
30
|
+
*/
|
|
31
|
+
function excludes({ declared, accessor, path }) {
|
|
32
|
+
return excludedIn(entriesFor(declared, accessor)).some((excluded) => reaches(excluded, path));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Whether a declaration covers the accessor's path: the whole source, the kind, the path, or a folder
|
|
36
|
+
* above it, and no `!` entry leaves it out. An exclusion wins whatever else in the list reaches the
|
|
37
|
+
* path, so `['*', '!text']` is every path but `text`'s.
|
|
38
|
+
*/
|
|
11
39
|
function covers({ declared, accessor, path }) {
|
|
12
|
-
|
|
13
|
-
if (
|
|
14
|
-
|
|
15
|
-
if (
|
|
16
|
-
|
|
17
|
-
|
|
40
|
+
const entries = entriesFor(declared, accessor);
|
|
41
|
+
if (entries === void 0) return false;
|
|
42
|
+
if (entries === "*") return true;
|
|
43
|
+
if (excludes({
|
|
44
|
+
declared,
|
|
45
|
+
accessor,
|
|
46
|
+
path
|
|
47
|
+
})) return false;
|
|
48
|
+
return entries.some((entry) => !entry.startsWith(EXCLUDE) && reaches(entry, path));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A list with `paths` taken into it: each is added unless a path the list takes already reaches it,
|
|
52
|
+
* and an exclusion that would leave one out is lifted. One naming the path or a folder under it goes;
|
|
53
|
+
* one naming a folder above it is spelled out as the folder's other members, so they stay out.
|
|
54
|
+
*/
|
|
55
|
+
function includePaths({ config, slug, kind, entries, paths }) {
|
|
56
|
+
const source = config.sourceFor(slug);
|
|
57
|
+
const included = [];
|
|
58
|
+
const exclusions = [];
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
if (!entry.startsWith(EXCLUDE)) {
|
|
61
|
+
included.push(entry);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const excluded = entry.slice(1);
|
|
65
|
+
if (paths.some((path) => reaches(path, excluded))) continue;
|
|
66
|
+
if (!paths.some((path) => reaches(excluded, path))) {
|
|
67
|
+
exclusions.push(entry);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
for (const member of source?.members(kind, excluded) ?? []) if (!paths.some((path) => reaches(path, member.path))) exclusions.push(`${EXCLUDE}${member.path}`);
|
|
71
|
+
}
|
|
72
|
+
for (const path of paths) {
|
|
73
|
+
if (included.some((entry) => reaches(entry, path))) continue;
|
|
74
|
+
included.push(path);
|
|
75
|
+
}
|
|
76
|
+
return [...included, ...exclusions];
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* A list with `paths` left out: a `!` entry for each the list doesn't exclude already. A path the
|
|
80
|
+
* list names under one of them goes, since the exclusion now says it, and so does a narrower
|
|
81
|
+
* exclusion a new one reaches. A folder named above an excluded path stays, less that path. With
|
|
82
|
+
* nothing listed, the list is the exclusions alone, which still keep the paths out when a linked row
|
|
83
|
+
* reaches them.
|
|
84
|
+
*/
|
|
85
|
+
function excludePaths(entries, paths) {
|
|
86
|
+
const list = entries === void 0 ? [] : entries === "*" ? ["*"] : entries;
|
|
87
|
+
const excluded = excludedIn(list);
|
|
88
|
+
const added = [...new Set(paths)].filter((path) => !excluded.some((already) => reaches(already, path)));
|
|
89
|
+
return [...list.filter((entry) => {
|
|
90
|
+
if (entry === "*") return true;
|
|
91
|
+
const path = entry.startsWith(EXCLUDE) ? entry.slice(1) : entry;
|
|
92
|
+
return !added.some((adding) => reaches(adding, path));
|
|
93
|
+
}), ...added.map((path) => `${EXCLUDE}${path}`)];
|
|
18
94
|
}
|
|
19
95
|
/**
|
|
20
96
|
* The declaration with `removal` taken out of it, or `undefined` when nothing stays declared.
|
|
@@ -22,7 +98,8 @@ function covers({ declared, accessor, path }) {
|
|
|
22
98
|
* A declaration of everything is spelled out first, one kind per accessor the source has rows for, so
|
|
23
99
|
* one kind can be taken out of it; a kind taken whole is spelled out as the source's top-level paths
|
|
24
100
|
* so some can go; a declared folder above a removed path is spelled out as its other members, so the
|
|
25
|
-
* rest of it stays covered.
|
|
101
|
+
* rest of it stays covered. A list that takes every path (`['*', …]`) is spelled out the same way.
|
|
102
|
+
* Its `!` entries stay, a list of them alone included, and narrowing writes none.
|
|
26
103
|
*/
|
|
27
104
|
function narrowDeclaration({ config, slug, current, removal, verb }) {
|
|
28
105
|
const source = config.sourceFor(slug);
|
|
@@ -31,7 +108,18 @@ function narrowDeclaration({ config, slug, current, removal, verb }) {
|
|
|
31
108
|
if (source && source.list(kind).length === 0) continue;
|
|
32
109
|
next[config.accessorOf(kind) ?? kind] = "*";
|
|
33
110
|
}
|
|
34
|
-
else
|
|
111
|
+
else {
|
|
112
|
+
Object.assign(next, current);
|
|
113
|
+
const fallback = current["*"];
|
|
114
|
+
if (fallback !== void 0) {
|
|
115
|
+
delete next["*"];
|
|
116
|
+
for (const kind of config.borrowableKinds(slug)) {
|
|
117
|
+
if (source && source.list(kind).length === 0) continue;
|
|
118
|
+
const accessor = config.accessorOf(kind) ?? kind;
|
|
119
|
+
if (!(accessor in current)) next[accessor] = fallback;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
35
123
|
for (const [accessor, paths] of removal) {
|
|
36
124
|
const before = next[accessor];
|
|
37
125
|
if (before === void 0) continue;
|
|
@@ -40,25 +128,32 @@ function narrowDeclaration({ config, slug, current, removal, verb }) {
|
|
|
40
128
|
continue;
|
|
41
129
|
}
|
|
42
130
|
const kind = config.accessors()[accessor]?.kind ?? accessor;
|
|
131
|
+
const excluded = excludedIn(before);
|
|
43
132
|
let declaredPaths;
|
|
44
|
-
if (before === "*") {
|
|
133
|
+
if (before === "*" || before.includes("*")) {
|
|
45
134
|
if (!source) rejectData(`Cannot ${verb} ${paths.join(", ")} through "${slug}" — the declaration takes every "${accessor}", and nothing has resolved that link to spell them out. Unlink the kind (\`{ "${accessor}": "*" }\`) instead.`, ["data", accessor]);
|
|
46
135
|
declaredPaths = source.tree(kind).map((node) => node.path);
|
|
47
|
-
} else declaredPaths = before;
|
|
136
|
+
} else declaredPaths = before.filter((entry) => !entry.startsWith(EXCLUDE));
|
|
48
137
|
const removed = (candidate) => paths.some((path) => path === candidate || candidate.startsWith(`${path}/`));
|
|
49
138
|
const kept = [];
|
|
50
139
|
for (const declaredPath of declaredPaths) {
|
|
51
140
|
if (removed(declaredPath)) continue;
|
|
141
|
+
if (excluded.some((path) => reaches(path, declaredPath))) continue;
|
|
52
142
|
if (!paths.some((path) => path.startsWith(`${declaredPath}/`)) || !source) {
|
|
53
143
|
kept.push(declaredPath);
|
|
54
144
|
continue;
|
|
55
145
|
}
|
|
56
|
-
for (const member of source.members(kind, declaredPath))
|
|
146
|
+
for (const member of source.members(kind, declaredPath)) {
|
|
147
|
+
if (removed(member.path)) continue;
|
|
148
|
+
if (excluded.some((path) => reaches(path, member.path))) continue;
|
|
149
|
+
kept.push(member.path);
|
|
150
|
+
}
|
|
57
151
|
}
|
|
58
|
-
|
|
59
|
-
|
|
152
|
+
const exclusions = excluded.map((path) => `${EXCLUDE}${path}`);
|
|
153
|
+
if (kept.length === 0 && exclusions.length === 0) delete next[accessor];
|
|
154
|
+
else next[accessor] = [...kept, ...exclusions];
|
|
60
155
|
}
|
|
61
156
|
return Object.keys(next).length === 0 ? void 0 : next;
|
|
62
157
|
}
|
|
63
158
|
//#endregion
|
|
64
|
-
export { PIN_KIND, covers, declarationOf, narrowDeclaration };
|
|
159
|
+
export { PIN_KIND, covers, declarationOf, entriesFor, excludePaths, excludes, includePaths, narrowDeclaration };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { rejectData } from "../../framework/rejection.js";
|
|
2
2
|
import { collectRefs, isMintedRow, ref, sourceOf, splitRef } from "../../framework/utils/refs.js";
|
|
3
3
|
import { blockedBy } from "../../framework/plan.js";
|
|
4
|
-
import {
|
|
4
|
+
import { excludedCondition } from "../../framework/source-integrity.js";
|
|
5
|
+
import { PIN_KIND, covers, declarationOf, entriesFor, excludePaths, excludes, includePaths, narrowDeclaration } from "./link-declaration.js";
|
|
5
6
|
import { planCopy, sourceEntries } from "./copy-plan.js";
|
|
6
7
|
//#region src/entities/system/link-plan.ts
|
|
7
8
|
/** The kinds `data` names, with what to take of each. An accessor this config has no kind for is a typo, refused by name. */
|
|
@@ -201,7 +202,7 @@ function planLink({ config, input }) {
|
|
|
201
202
|
} else {
|
|
202
203
|
const next = { ...current ?? {} };
|
|
203
204
|
for (const [accessor, gain] of gains) {
|
|
204
|
-
const before =
|
|
205
|
+
const before = entriesFor(current, accessor);
|
|
205
206
|
if (before === "*") continue;
|
|
206
207
|
if (gain === "*") {
|
|
207
208
|
next[accessor] = "*";
|
|
@@ -213,7 +214,13 @@ function planLink({ config, input }) {
|
|
|
213
214
|
path
|
|
214
215
|
}));
|
|
215
216
|
if (added.length === 0) continue;
|
|
216
|
-
next[accessor] =
|
|
217
|
+
next[accessor] = includePaths({
|
|
218
|
+
config,
|
|
219
|
+
slug,
|
|
220
|
+
kind: config.accessors()[accessor]?.kind ?? accessor,
|
|
221
|
+
entries: before ?? [],
|
|
222
|
+
paths: [...new Set(added)]
|
|
223
|
+
});
|
|
217
224
|
}
|
|
218
225
|
for (const [accessor, value] of Object.entries(next)) {
|
|
219
226
|
if (current !== void 0 && current[accessor] === value) continue;
|
|
@@ -361,6 +368,104 @@ function planUnlink({ config, input }) {
|
|
|
361
368
|
};
|
|
362
369
|
}
|
|
363
370
|
/**
|
|
371
|
+
* Leave paths of a linked system out, with a `!` entry each in the pin's `link`. Stronger than
|
|
372
|
+
* `unlink`: nothing derives there however the link reaches it, a linked token naming it included,
|
|
373
|
+
* and the linked tokens' overrides under an excluded modifier are left out of the build. `link` with
|
|
374
|
+
* the same path lifts it again.
|
|
375
|
+
*/
|
|
376
|
+
function planExclude({ config, input }) {
|
|
377
|
+
const { path: slug } = input;
|
|
378
|
+
const source = config.sourceFor(slug);
|
|
379
|
+
if (!source) rejectData(`Cannot exclude through "${slug}" — nothing has resolved that link.`);
|
|
380
|
+
const current = declarationOf(config, slug);
|
|
381
|
+
const next = current === void 0 ? {} : current === "*" ? { "*": "*" } : { ...current };
|
|
382
|
+
for (const { kind, accessor, paths } of wantedKinds({
|
|
383
|
+
config,
|
|
384
|
+
slug,
|
|
385
|
+
data: input.data,
|
|
386
|
+
verb: "exclude"
|
|
387
|
+
})) {
|
|
388
|
+
if (paths === "*") rejectData(`Cannot exclude every "${accessor}" through "${slug}" — name the paths to leave out, or \`unlink\` the kind.`, ["data", accessor]);
|
|
389
|
+
for (const path of paths) if (sourceEntries({
|
|
390
|
+
source,
|
|
391
|
+
kind,
|
|
392
|
+
path
|
|
393
|
+
}).length === 0) rejectData(`Cannot exclude "${path}" through "${slug}" — the source has no ${kind} there.`, ["data", accessor]);
|
|
394
|
+
next[accessor] = excludePaths(entriesFor(current, accessor), paths);
|
|
395
|
+
}
|
|
396
|
+
const declared = next;
|
|
397
|
+
const dropped = [];
|
|
398
|
+
const optionDeletes = {};
|
|
399
|
+
const options = config.storedBody("linked-system", slug).options ?? {};
|
|
400
|
+
for (const [accessor, entries] of Object.entries(options)) {
|
|
401
|
+
if (!entries || typeof entries !== "object") continue;
|
|
402
|
+
const kind = config.accessors()[accessor]?.kind ?? accessor;
|
|
403
|
+
for (const path of Object.keys(entries)) {
|
|
404
|
+
if (!excludes({
|
|
405
|
+
declared,
|
|
406
|
+
accessor,
|
|
407
|
+
path
|
|
408
|
+
})) continue;
|
|
409
|
+
dropped.push({
|
|
410
|
+
kind,
|
|
411
|
+
path
|
|
412
|
+
});
|
|
413
|
+
optionDeletes[`options.${accessor}.${path}`] = null;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
const patches = [{
|
|
417
|
+
kind: PIN_KIND,
|
|
418
|
+
operation: "update",
|
|
419
|
+
path: slug,
|
|
420
|
+
data: {
|
|
421
|
+
link: declared,
|
|
422
|
+
...optionDeletes
|
|
423
|
+
}
|
|
424
|
+
}];
|
|
425
|
+
if (Object.entries(input.data).every(([accessor, paths]) => paths.every((path) => excludes({
|
|
426
|
+
declared: current,
|
|
427
|
+
accessor,
|
|
428
|
+
path
|
|
429
|
+
})))) return {
|
|
430
|
+
declared: current,
|
|
431
|
+
removed: [],
|
|
432
|
+
leftOut: [],
|
|
433
|
+
dropped: [],
|
|
434
|
+
blocked: `Nothing would change: what "${slug}" declares already leaves everything named out.`,
|
|
435
|
+
patches: []
|
|
436
|
+
};
|
|
437
|
+
const blocked = blockedBy(() => config.applyAll(patches));
|
|
438
|
+
const removed = [];
|
|
439
|
+
const leftOut = [];
|
|
440
|
+
if (blocked === void 0) {
|
|
441
|
+
const after = config.applyAll(patches);
|
|
442
|
+
for (const kind of config.borrowableKinds(slug)) for (const item of config.list(kind)) {
|
|
443
|
+
if (item.borrowedFrom !== slug) continue;
|
|
444
|
+
if (after.resolve(kind, item.path)?.borrowedFrom === slug) continue;
|
|
445
|
+
removed.push({
|
|
446
|
+
kind,
|
|
447
|
+
path: item.path
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
for (const item of after.list("token")) {
|
|
451
|
+
if (item.borrowedFrom !== slug) continue;
|
|
452
|
+
const body = source.resolve("token", item.path)?.toJSON();
|
|
453
|
+
for (const [override, { on }] of Object.entries(body?.overrides ?? {})) if (excludedCondition(after, on)) leftOut.push({
|
|
454
|
+
token: item.path,
|
|
455
|
+
override
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return {
|
|
460
|
+
declared,
|
|
461
|
+
removed,
|
|
462
|
+
leftOut,
|
|
463
|
+
dropped,
|
|
464
|
+
...blocked === void 0 ? {} : { blocked },
|
|
465
|
+
patches
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
364
469
|
* The linked rows that point at what `data` names, followed to a fixpoint: a row pointing at one of
|
|
365
470
|
* those, and so on. Read off both ref graphs, since a linked row's refs are the source's spelling and
|
|
366
471
|
* this config's index holds them qualified.
|
|
@@ -832,4 +937,4 @@ function sourceRowStates({ config, slug }) {
|
|
|
832
937
|
return rows;
|
|
833
938
|
}
|
|
834
939
|
//#endregion
|
|
835
|
-
export { planBreak, planLink, planUnlink, sourceRowStates };
|
|
940
|
+
export { planBreak, planExclude, planLink, planUnlink, sourceRowStates };
|
|
@@ -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 = 20261013182150;
|
|
77
77
|
interface SerializedConfig {
|
|
78
78
|
/** See {@link SERIALIZED_CONFIG_VERSION}. Always written; a stored artifact may lack it. */
|
|
79
79
|
readonly version: number;
|
|
@@ -1785,6 +1785,11 @@ declare class Config {
|
|
|
1785
1785
|
* subject is the overlay rather than whether a row is minted at all.
|
|
1786
1786
|
*/
|
|
1787
1787
|
sourceOverlay(slug: string, kind: string, path: string): SourceOverlay | undefined;
|
|
1788
|
+
/**
|
|
1789
|
+
* Whether the declared source at `slug` excludes one of its paths ({@link EntityKind.bulkExcludes}):
|
|
1790
|
+
* left out on purpose, so no row derives there. Matched on the path the source spells.
|
|
1791
|
+
*/
|
|
1792
|
+
sourceExcludes(slug: string, kind: string, path: string): boolean;
|
|
1788
1793
|
/**
|
|
1789
1794
|
* The field-path delta that records `overlay` as the pin's directive for one path, or `null` when
|
|
1790
1795
|
* there is nothing to write.
|