@yahoo/uds-create-config 5.21.0 → 5.23.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/authoring/presence.d.ts +17 -0
- package/dist/authoring/presence.js +7 -0
- package/dist/entities/system/Component.js +5 -1
- package/dist/entities/system/Motion.js +4 -4
- package/dist/entities/system/Token.js +5 -1
- package/dist/framework/Config.d.ts +67 -2
- package/dist/framework/Config.js +212 -3
- package/dist/framework/defineEntity.d.ts +5 -2
- package/dist/framework/defineEntity.js +2 -1
- package/dist/framework/plan.d.ts +5 -1
- package/dist/framework/plan.js +5 -0
- package/dist/framework/registry.d.ts +7 -0
- package/dist/framework/registry.js +3 -2
- package/dist/framework/schemas.js +2 -2
- package/dist/framework/session.d.ts +5 -1
- package/dist/framework/session.js +7 -4
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/authoring/presence.d.ts
|
|
4
|
+
interface PresenceProps {
|
|
5
|
+
children?: ReactNode;
|
|
6
|
+
/** `false`: the children it starts with appear where they rest, with no entrance. */
|
|
7
|
+
initial?: boolean;
|
|
8
|
+
/** `popLayout`: a leaving child is taken out of the layout at once, so the one replacing it moves
|
|
9
|
+
* in without waiting. */
|
|
10
|
+
mode?: 'sync' | 'wait' | 'popLayout';
|
|
11
|
+
}
|
|
12
|
+
/** See {@link PresenceProps}. Never rendered: the build replaces it. */
|
|
13
|
+
declare function Presence({
|
|
14
|
+
children
|
|
15
|
+
}: PresenceProps): ReactNode;
|
|
16
|
+
//#endregion
|
|
17
|
+
export { Presence, PresenceProps };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { reject, rejectData } from "../../framework/rejection.js";
|
|
2
|
-
import { canonicalCondition, isRef, kindOf, memberRef, pathOf, ref, refLeaf, refSchema, splitRef } from "../../framework/utils/refs.js";
|
|
2
|
+
import { canonicalCondition, collectRefs, isRef, kindOf, memberRef, pathOf, ref, refLeaf, refSchema, splitRef } 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 { derivedColor, gradient, oklchColor } from "./color.js";
|
|
@@ -1724,6 +1724,10 @@ const ComponentEntity = defineEntity({
|
|
|
1724
1724
|
nameEdges: bagNameEdges(componentBags),
|
|
1725
1725
|
styleBags: componentBags,
|
|
1726
1726
|
rewriteName: bagRewriteName(componentBags),
|
|
1727
|
+
structuralBody: (data) => {
|
|
1728
|
+
const { layers, anatomy } = data;
|
|
1729
|
+
return { renders: collectRefs([...Object.values(layers ?? {}), ...Object.values(anatomy ?? {}).map((node) => node?.element)]).filter((target) => kindOf(target) === "component").map((target) => ({ __ref: target })) };
|
|
1730
|
+
},
|
|
1727
1731
|
schemas: {
|
|
1728
1732
|
create: {
|
|
1729
1733
|
data: componentFields,
|
|
@@ -14,9 +14,9 @@ import { z } from "zod";
|
|
|
14
14
|
* states (`variants`, `initial`, `animate`, `exit`, `whileHover`, `whileTap`, `whileFocus`) with a
|
|
15
15
|
* `transition` written as CSS, or both. The states mean what a JS preset's do.
|
|
16
16
|
* - `js` → played by `motion/react`: the interaction state the client drives, `exit` included,
|
|
17
|
-
* which runs under
|
|
18
|
-
* of values or the name of one of the preset's
|
|
19
|
-
* own size and position as they change.
|
|
17
|
+
* which runs under a `Presence` ancestor when the element unmounts (the build makes it motion's
|
|
18
|
+
* `AnimatePresence`). Each state is a set of values or the name of one of the preset's
|
|
19
|
+
* `variants`, and `layout` animates the element's own size and position as they change.
|
|
20
20
|
* Each takes only its own fields. `motion/convert` rewrites a preset from one to the other, and a
|
|
21
21
|
* link's `{ override: { runtime: 'css' } }` converts the source's preset as it reads
|
|
22
22
|
* (`motion-convert.ts`). A CSS preset with neither keyframes nor a state is refused, since it would
|
|
@@ -44,7 +44,7 @@ const initial = z.union([
|
|
|
44
44
|
z.literal(false)
|
|
45
45
|
]).optional().describe("The starting values, as `motion/react` reads them, or `false` to start at `animate` without playing on mount.");
|
|
46
46
|
const animate = gesture.describe("The values to animate to.");
|
|
47
|
-
const exit = gesture.describe("The values to animate to as the element leaves.");
|
|
47
|
+
const exit = gesture.describe("The values to animate to as the element leaves. It plays where a render writes `Presence` (from '@yahoo/uds-create-config') around the part it can unmount, which the build makes the runtime's own presence.");
|
|
48
48
|
const whileHover = gesture.describe("Values held while the element is hovered.");
|
|
49
49
|
const whileTap = gesture.describe("Values held while the element is pressed.");
|
|
50
50
|
const whileFocus = gesture.describe("Values held while the element has focus.");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isRef, pathOf, ref, refKey, sourceOf } from "../../framework/utils/refs.js";
|
|
1
|
+
import { isRef, kindOf, pathOf, ref, refKey, sourceOf } from "../../framework/utils/refs.js";
|
|
2
2
|
import { composedValueSchemas } from "./composed-values.js";
|
|
3
3
|
import { defineEntity } from "../../framework/defineEntity.js";
|
|
4
4
|
import { defineSubEntity } from "../../framework/defineSubEntity.js";
|
|
@@ -141,6 +141,10 @@ const TokenEntity = defineEntity({
|
|
|
141
141
|
cssVariables: z.boolean().optional().describe("`false` writes the group's tokens into the values that use them instead of declaring a custom property for each, as Tailwind's `@theme inline` does. A token's own `cssVariable` wins. A config that holds the group from a linked system keeps it unless it sets its own.")
|
|
142
142
|
}),
|
|
143
143
|
subEntities: { overrides: Override },
|
|
144
|
+
structuralBody: (data) => {
|
|
145
|
+
const { value, overrides } = data;
|
|
146
|
+
return { aliases: [value, ...Object.values(overrides ?? {}).map((override) => override?.value)].filter((alias) => isRef(alias) && kindOf(alias.__ref) === "token") };
|
|
147
|
+
},
|
|
144
148
|
schemas: { create: { description: `Create a token at \`<group>/<name>\` — \`data.value\` is the CSS value. The first path segment is its group, and a style property takes a whole group as one ref (\`${JSON.stringify(ref("token", "<group>"))}\`), so put the tokens one prop should offer in one group rather than listing values on the prop.` } }
|
|
145
149
|
});
|
|
146
150
|
/**
|
|
@@ -985,8 +985,18 @@ declare class Config {
|
|
|
985
985
|
* under an id of its own and has to settle that row when the write lands, so it needs the pending
|
|
986
986
|
* entry and its own staging to agree — and an id minted here would be a second source that
|
|
987
987
|
* silently disagrees with it. Omitted, one is minted, which is what every non-optimistic caller
|
|
988
|
-
* wants.
|
|
988
|
+
* wants.
|
|
989
|
+
*
|
|
990
|
+
* A delete records more than one patch when items declare they cannot outlive the target
|
|
991
|
+
* ({@link EntityKind.structuralBody}): a delete per dependent, leaves first, then the target, as
|
|
992
|
+
* one changeset. `id` then names the first of them and the changeset. */
|
|
989
993
|
apply(patch: Patch, id?: string): this;
|
|
994
|
+
/**
|
|
995
|
+
* {@link apply} for exactly the patch it is given: a delete takes nothing with it. For replaying a
|
|
996
|
+
* patch as it was recorded, since a recorded delete's cascade was recorded beside it, ahead of it.
|
|
997
|
+
* Deleting again what the base gained since would remove it with no patch saying so.
|
|
998
|
+
*/
|
|
999
|
+
applyRecorded(patch: Patch, id?: string): this;
|
|
990
1000
|
/**
|
|
991
1001
|
* `run` with each kind's invariants judged once, on the config it returns, rather than after every
|
|
992
1002
|
* `apply` inside it. An invariant walks its whole kind, so a pass that writes a row per entity and
|
|
@@ -1024,7 +1034,62 @@ declare class Config {
|
|
|
1024
1034
|
*
|
|
1025
1035
|
* Several patches are one gesture, so they share one changeset; a single patch carries none.
|
|
1026
1036
|
*/
|
|
1027
|
-
applyAll(
|
|
1037
|
+
applyAll(requested: readonly Patch[]): this;
|
|
1038
|
+
/**
|
|
1039
|
+
* `patches` with each delete preceded by the deletes that go with it, each judged against the state
|
|
1040
|
+
* the patches before it produce. A later delete of something the batch already took is dropped, so
|
|
1041
|
+
* a caller listing both the entity and its dependent, in either order, deletes each once.
|
|
1042
|
+
*
|
|
1043
|
+
* Applied unchecked to a scratch snapshot to learn those states. A refusal there stops the
|
|
1044
|
+
* expansion and hands back the rest of `patches` as given, so the real pass refuses at the same
|
|
1045
|
+
* patch, with the same message, a single `apply` would.
|
|
1046
|
+
*/
|
|
1047
|
+
private withStructuralDependents;
|
|
1048
|
+
/** The kinds that declare {@link EntityKind.structuralBody}, with it. */
|
|
1049
|
+
private cascadingKinds;
|
|
1050
|
+
/**
|
|
1051
|
+
* Every structural ref the stored items of the cascading kinds hold, keyed by the kind it names,
|
|
1052
|
+
* with bare leaves spelled out in full. A ref into a source names the source's entity, never this
|
|
1053
|
+
* config's, so it is left out, and so is a row a source minted.
|
|
1054
|
+
*/
|
|
1055
|
+
private structuralEdges;
|
|
1056
|
+
/**
|
|
1057
|
+
* The deletes that go with `patch`, leaves first: one for every item of a kind declaring
|
|
1058
|
+
* {@link EntityKind.structuralBody} whose structural refs name the deleted entity, under any
|
|
1059
|
+
* spelling {@link refAliases} admits, or anything under its path, preceded by whatever goes with
|
|
1060
|
+
* that item in turn. Empty unless `patch` is a delete.
|
|
1061
|
+
*
|
|
1062
|
+
* `edges` may come from an earlier state, so an edge whose item is gone is skipped.
|
|
1063
|
+
*/
|
|
1064
|
+
private structuralDependentsOf;
|
|
1065
|
+
/**
|
|
1066
|
+
* What deleting `kind:path` takes with it, in the order the delete writes them, whether or not the
|
|
1067
|
+
* delete would be refused. {@link EntityKind.structuralBody} decides what counts.
|
|
1068
|
+
*/
|
|
1069
|
+
cascadeOf({
|
|
1070
|
+
kind,
|
|
1071
|
+
path
|
|
1072
|
+
}: {
|
|
1073
|
+
kind: string;
|
|
1074
|
+
path: string;
|
|
1075
|
+
}): {
|
|
1076
|
+
kind: string;
|
|
1077
|
+
path: string;
|
|
1078
|
+
}[];
|
|
1079
|
+
/**
|
|
1080
|
+
* Refuse a delete whose cascade takes something that an entity outside it still refers to, naming
|
|
1081
|
+
* the entity the caller deleted. Without this the refusal comes from the dependent's own delete, and
|
|
1082
|
+
* names an entity the caller never asked to delete. A referrer of the target itself is left to the
|
|
1083
|
+
* delete's own rule.
|
|
1084
|
+
*/
|
|
1085
|
+
private assertCascadeUnblocked;
|
|
1086
|
+
/**
|
|
1087
|
+
* `deletes` reordered so each one comes after every other one whose entity still refers to it, by
|
|
1088
|
+
* any ref, since each delete is refused while something still points at it. The structural walk
|
|
1089
|
+
* already puts a dependent before what it depends on; this also covers a plain ref between two
|
|
1090
|
+
* dependents. Members of a cycle keep their order, and the delete that meets the cycle refuses.
|
|
1091
|
+
*/
|
|
1092
|
+
private referrersFirst;
|
|
1028
1093
|
/**
|
|
1029
1094
|
* Whether patches on kinds that name no source can change the rows a derive mints between `before`
|
|
1030
1095
|
* and `after`: they wrote a pin (an edit to a minted row lands in its pin's `options`), or a row
|
package/dist/framework/Config.js
CHANGED
|
@@ -1265,8 +1265,26 @@ var Config = class Config {
|
|
|
1265
1265
|
* under an id of its own and has to settle that row when the write lands, so it needs the pending
|
|
1266
1266
|
* entry and its own staging to agree — and an id minted here would be a second source that
|
|
1267
1267
|
* silently disagrees with it. Omitted, one is minted, which is what every non-optimistic caller
|
|
1268
|
-
* wants.
|
|
1268
|
+
* wants.
|
|
1269
|
+
*
|
|
1270
|
+
* A delete records more than one patch when items declare they cannot outlive the target
|
|
1271
|
+
* ({@link EntityKind.structuralBody}): a delete per dependent, leaves first, then the target, as
|
|
1272
|
+
* one changeset. `id` then names the first of them and the changeset. */
|
|
1269
1273
|
apply(patch, id) {
|
|
1274
|
+
const dependents = this.structuralDependentsOf({ patch });
|
|
1275
|
+
if (dependents.length === 0) return this.applyRecorded(patch, id);
|
|
1276
|
+
const since = this.state.pending.length;
|
|
1277
|
+
return [...dependents, patch].reduce((config, each) => config.applyRecorded(each), this).withChangeset({
|
|
1278
|
+
since,
|
|
1279
|
+
id
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* {@link apply} for exactly the patch it is given: a delete takes nothing with it. For replaying a
|
|
1284
|
+
* patch as it was recorded, since a recorded delete's cascade was recorded beside it, ahead of it.
|
|
1285
|
+
* Deleting again what the base gained since would remove it with no patch saying so.
|
|
1286
|
+
*/
|
|
1287
|
+
applyRecorded(patch, id) {
|
|
1270
1288
|
const deferred = this.state.deferredInvariants;
|
|
1271
1289
|
if (deferred?.open) deferred.kinds.add(patch.kind);
|
|
1272
1290
|
const applied = this.applyPatch(patch, id, !deferred?.open);
|
|
@@ -1361,8 +1379,9 @@ var Config = class Config {
|
|
|
1361
1379
|
*
|
|
1362
1380
|
* Several patches are one gesture, so they share one changeset; a single patch carries none.
|
|
1363
1381
|
*/
|
|
1364
|
-
applyAll(
|
|
1365
|
-
if (
|
|
1382
|
+
applyAll(requested) {
|
|
1383
|
+
if (requested.length === 0) return this;
|
|
1384
|
+
const patches = this.withStructuralDependents(requested);
|
|
1366
1385
|
const changeset = patches.length > 1 ? crypto.randomUUID() : void 0;
|
|
1367
1386
|
const stampOf = (patch) => stamp(patch, void 0, changeset);
|
|
1368
1387
|
const touched = new Set(patches.map((patch) => patch.kind));
|
|
@@ -1412,6 +1431,196 @@ var Config = class Config {
|
|
|
1412
1431
|
return config.derivedForEdit(actionOf(patches));
|
|
1413
1432
|
}
|
|
1414
1433
|
/**
|
|
1434
|
+
* `patches` with each delete preceded by the deletes that go with it, each judged against the state
|
|
1435
|
+
* the patches before it produce. A later delete of something the batch already took is dropped, so
|
|
1436
|
+
* a caller listing both the entity and its dependent, in either order, deletes each once.
|
|
1437
|
+
*
|
|
1438
|
+
* Applied unchecked to a scratch snapshot to learn those states. A refusal there stops the
|
|
1439
|
+
* expansion and hands back the rest of `patches` as given, so the real pass refuses at the same
|
|
1440
|
+
* patch, with the same message, a single `apply` would.
|
|
1441
|
+
*/
|
|
1442
|
+
withStructuralDependents(patches) {
|
|
1443
|
+
const cascading = this.cascadingKinds();
|
|
1444
|
+
if (cascading.length === 0) return patches;
|
|
1445
|
+
if (!patches.some((patch) => patch.operation === "delete")) return patches;
|
|
1446
|
+
if (!(cascading.some(({ kind }) => this.state.snapshot.slice(kind).items.size > 0) || patches.some((patch) => cascading.some(({ kind }) => kind === patch.kind)))) return patches;
|
|
1447
|
+
const expanded = [];
|
|
1448
|
+
const cascaded = /* @__PURE__ */ new Set();
|
|
1449
|
+
const movesSource = patches.some((patch) => this.entityOf(patch.kind)?.sourceIdentity);
|
|
1450
|
+
let view = movesSource ? this : this.derive({ snapshot: this.state.snapshot.drafting() });
|
|
1451
|
+
let edges;
|
|
1452
|
+
for (const [index, patch] of patches.entries()) {
|
|
1453
|
+
if (patch.operation === "delete" && cascaded.has(`${patch.kind}:${patch.path}`) && !view.state.snapshot.has(patch.kind, view.leafPath(patch.kind, patch.path) ?? patch.path)) continue;
|
|
1454
|
+
if (patch.operation === "delete") edges ??= view.structuralEdges();
|
|
1455
|
+
const step = [...view.structuralDependentsOf({
|
|
1456
|
+
patch,
|
|
1457
|
+
edges
|
|
1458
|
+
}), patch];
|
|
1459
|
+
expanded.push(...step);
|
|
1460
|
+
try {
|
|
1461
|
+
for (const each of step) view = movesSource ? view.applyPatch(each, void 0, false) : view.derive({ snapshot: view.computeApply(view.state.snapshot, each, "write", false) });
|
|
1462
|
+
} catch (error) {
|
|
1463
|
+
if (!isConfigRejection(error)) throw error;
|
|
1464
|
+
return [...expanded, ...patches.slice(index + 1)];
|
|
1465
|
+
}
|
|
1466
|
+
if (patch.operation !== "delete") edges = void 0;
|
|
1467
|
+
for (const each of step) if (each !== patch && each.operation === "delete") for (const spelling of view.refAliases(each.kind, each.path)) cascaded.add(spelling);
|
|
1468
|
+
}
|
|
1469
|
+
return expanded;
|
|
1470
|
+
}
|
|
1471
|
+
/** The kinds that declare {@link EntityKind.structuralBody}, with it. */
|
|
1472
|
+
cascadingKinds() {
|
|
1473
|
+
const cascading = [];
|
|
1474
|
+
for (const [kind, entry] of this.state.entities) if (entry.structuralBody) cascading.push({
|
|
1475
|
+
kind,
|
|
1476
|
+
structuralBody: entry.structuralBody
|
|
1477
|
+
});
|
|
1478
|
+
return cascading;
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Every structural ref the stored items of the cascading kinds hold, keyed by the kind it names,
|
|
1482
|
+
* with bare leaves spelled out in full. A ref into a source names the source's entity, never this
|
|
1483
|
+
* config's, so it is left out, and so is a row a source minted.
|
|
1484
|
+
*/
|
|
1485
|
+
structuralEdges() {
|
|
1486
|
+
const edges = /* @__PURE__ */ new Map();
|
|
1487
|
+
for (const { kind, structuralBody } of this.cascadingKinds()) for (const entity of this.state.snapshot.values(kind)) {
|
|
1488
|
+
const body = entity.toJSON();
|
|
1489
|
+
if (isMintedRow(body)) continue;
|
|
1490
|
+
const fields = isExtendedRef(body) ? body.extend : body;
|
|
1491
|
+
for (const ref of collectRefs(structuralBody(fields))) {
|
|
1492
|
+
if (sourceOf(ref) !== void 0) continue;
|
|
1493
|
+
const target = splitRef(ref);
|
|
1494
|
+
const named = edges.get(target.kind) ?? [];
|
|
1495
|
+
named.push({
|
|
1496
|
+
targetPath: this.leafPath(target.kind, target.qualifiedPath) ?? target.qualifiedPath,
|
|
1497
|
+
kind,
|
|
1498
|
+
path: entity.path
|
|
1499
|
+
});
|
|
1500
|
+
edges.set(target.kind, named);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
return edges;
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* The deletes that go with `patch`, leaves first: one for every item of a kind declaring
|
|
1507
|
+
* {@link EntityKind.structuralBody} whose structural refs name the deleted entity, under any
|
|
1508
|
+
* spelling {@link refAliases} admits, or anything under its path, preceded by whatever goes with
|
|
1509
|
+
* that item in turn. Empty unless `patch` is a delete.
|
|
1510
|
+
*
|
|
1511
|
+
* `edges` may come from an earlier state, so an edge whose item is gone is skipped.
|
|
1512
|
+
*/
|
|
1513
|
+
structuralDependentsOf({ patch, edges: given, checked = true }) {
|
|
1514
|
+
if (patch.operation !== "delete") return [];
|
|
1515
|
+
if (this.cascadingKinds().length === 0) return [];
|
|
1516
|
+
if (this.declaringSource(patch.kind, patch.path) !== void 0) return [];
|
|
1517
|
+
const edges = given ?? this.structuralEdges();
|
|
1518
|
+
if (edges.size === 0) return [];
|
|
1519
|
+
const seen = new Set([`${patch.kind}:${patch.path}`]);
|
|
1520
|
+
const ordered = [];
|
|
1521
|
+
const visit = ({ kind, path }) => {
|
|
1522
|
+
const spellings = new Set(this.refAliases(kind, path));
|
|
1523
|
+
const fullPath = this.leafPath(kind, path) ?? path;
|
|
1524
|
+
for (const edge of edges.get(kind) ?? []) {
|
|
1525
|
+
if (!(spellings.has(`${kind}:${edge.targetPath}`) || edge.targetPath.startsWith(`${fullPath}/`))) continue;
|
|
1526
|
+
const key = `${edge.kind}:${edge.path}`;
|
|
1527
|
+
if (seen.has(key)) continue;
|
|
1528
|
+
if (!this.state.snapshot.has(edge.kind, edge.path)) continue;
|
|
1529
|
+
seen.add(key);
|
|
1530
|
+
if (this.declaringSource(edge.kind, edge.path) === void 0) visit({
|
|
1531
|
+
kind: edge.kind,
|
|
1532
|
+
path: edge.path
|
|
1533
|
+
});
|
|
1534
|
+
ordered.push({
|
|
1535
|
+
kind: edge.kind,
|
|
1536
|
+
operation: "delete",
|
|
1537
|
+
path: edge.path
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
};
|
|
1541
|
+
visit({
|
|
1542
|
+
kind: patch.kind,
|
|
1543
|
+
path: patch.path
|
|
1544
|
+
});
|
|
1545
|
+
if (checked) this.assertCascadeUnblocked({
|
|
1546
|
+
patch,
|
|
1547
|
+
cascade: ordered
|
|
1548
|
+
});
|
|
1549
|
+
return this.referrersFirst(ordered);
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* What deleting `kind:path` takes with it, in the order the delete writes them, whether or not the
|
|
1553
|
+
* delete would be refused. {@link EntityKind.structuralBody} decides what counts.
|
|
1554
|
+
*/
|
|
1555
|
+
cascadeOf({ kind, path }) {
|
|
1556
|
+
return this.structuralDependentsOf({
|
|
1557
|
+
patch: {
|
|
1558
|
+
kind,
|
|
1559
|
+
operation: "delete",
|
|
1560
|
+
path
|
|
1561
|
+
},
|
|
1562
|
+
checked: false
|
|
1563
|
+
}).flatMap((each) => each.operation === "delete" ? [{
|
|
1564
|
+
kind: each.kind,
|
|
1565
|
+
path: each.path
|
|
1566
|
+
}] : []);
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* Refuse a delete whose cascade takes something that an entity outside it still refers to, naming
|
|
1570
|
+
* the entity the caller deleted. Without this the refusal comes from the dependent's own delete, and
|
|
1571
|
+
* names an entity the caller never asked to delete. A referrer of the target itself is left to the
|
|
1572
|
+
* delete's own rule.
|
|
1573
|
+
*/
|
|
1574
|
+
assertCascadeUnblocked({ patch, cascade }) {
|
|
1575
|
+
if (!("path" in patch)) return;
|
|
1576
|
+
const keyOf = (target) => {
|
|
1577
|
+
const { kind, qualifiedPath } = splitRef(target);
|
|
1578
|
+
return `${kind}:${qualifiedPath}`;
|
|
1579
|
+
};
|
|
1580
|
+
const target = `${patch.kind}:${patch.path}`;
|
|
1581
|
+
const going = new Set([target, ...cascade.map((each) => `${each.kind}:${"path" in each ? each.path : ""}`)]);
|
|
1582
|
+
const snapshot = this.state.snapshot;
|
|
1583
|
+
for (const each of cascade) {
|
|
1584
|
+
if (!("path" in each)) continue;
|
|
1585
|
+
if (this.declaringSource(each.kind, each.path) !== void 0) continue;
|
|
1586
|
+
const blockers = this.referrersOf(snapshot, each.kind, each.path).filter((referrer) => !going.has(keyOf(referrer))).filter((referrer) => !isMintedAt(snapshot, referrer));
|
|
1587
|
+
if (blockers.length === 0) continue;
|
|
1588
|
+
rejectReferrers({
|
|
1589
|
+
message: `Cannot delete ${target} — it would also delete ${each.kind}:${each.path}, which ${blockers.join(", ")} still ${blockers.length === 1 ? "references" : "reference"}.`,
|
|
1590
|
+
referrers: blockers
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
/**
|
|
1595
|
+
* `deletes` reordered so each one comes after every other one whose entity still refers to it, by
|
|
1596
|
+
* any ref, since each delete is refused while something still points at it. The structural walk
|
|
1597
|
+
* already puts a dependent before what it depends on; this also covers a plain ref between two
|
|
1598
|
+
* dependents. Members of a cycle keep their order, and the delete that meets the cycle refuses.
|
|
1599
|
+
*/
|
|
1600
|
+
referrersFirst(deletes) {
|
|
1601
|
+
const keyOf = (target) => {
|
|
1602
|
+
const { kind, qualifiedPath } = splitRef(target);
|
|
1603
|
+
return `${kind}:${qualifiedPath}`;
|
|
1604
|
+
};
|
|
1605
|
+
const remaining = new Map(deletes.map((each) => [`${each.kind}:${"path" in each ? each.path : ""}`, each]));
|
|
1606
|
+
const sorted = [];
|
|
1607
|
+
const referredToFromRemaining = (key) => {
|
|
1608
|
+
const { kind, qualifiedPath } = splitRef(key);
|
|
1609
|
+
return this.referrersOf(this.state.snapshot, kind, qualifiedPath).some((referrer) => keyOf(referrer) !== key && remaining.has(keyOf(referrer)));
|
|
1610
|
+
};
|
|
1611
|
+
while (remaining.size > 0) {
|
|
1612
|
+
const next = [...remaining.keys()].find((key) => !referredToFromRemaining(key));
|
|
1613
|
+
if (next === void 0) {
|
|
1614
|
+
sorted.push(...remaining.values());
|
|
1615
|
+
break;
|
|
1616
|
+
}
|
|
1617
|
+
const each = remaining.get(next);
|
|
1618
|
+
if (each) sorted.push(each);
|
|
1619
|
+
remaining.delete(next);
|
|
1620
|
+
}
|
|
1621
|
+
return sorted;
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1415
1624
|
* Whether patches on kinds that name no source can change the rows a derive mints between `before`
|
|
1416
1625
|
* and `after`: they wrote a pin (an edit to a minted row lands in its pin's `options`), or a row
|
|
1417
1626
|
* they touched points at a source, changes which source-qualified refs it holds, or appears or
|
|
@@ -163,7 +163,9 @@ GI extends GroupInputs = Record<never, never>> = Omit<RefSchema, 'extend'> & {
|
|
|
163
163
|
/** Create an item of this kind — `Component.create(config, 'Button', {…})`. Typed body; returns
|
|
164
164
|
* the config for chaining. (The typed twin of `config.apply({kind, operation:'create', …})`.) */
|
|
165
165
|
create<C extends Config>(config: C, path: string, data: CreateInput<F>): C; /** Update an item — a partial patch merged over the current value. */
|
|
166
|
-
update<C extends Config>(config: C, path: string, data: Partial<z.infer<F>>): C;
|
|
166
|
+
update<C extends Config>(config: C, path: string, data: Partial<z.infer<F>>): C;
|
|
167
|
+
/** Delete an item, with whatever cannot exist without it ({@link EntityKind.structuralBody}). Refused
|
|
168
|
+
* while anything else still references it. */
|
|
167
169
|
delete<C extends Config>(config: C, path: string): C; /** Rename an item's path — references cascade. */
|
|
168
170
|
rename<C extends Config>(config: C, path: string, to: string): C;
|
|
169
171
|
/**
|
|
@@ -321,7 +323,8 @@ declare function defineEntity<F extends FieldsSchema, G extends FieldsSchema = z
|
|
|
321
323
|
/** Fields holding prose with mentions, authored through the class's `text` tag — see
|
|
322
324
|
* {@link EntityKind.mentionFields}. */
|
|
323
325
|
mentionFields?: readonly string[]; /** Cross-item rules the kind holds — see {@link EntityKind.invariants}. */
|
|
324
|
-
invariants?: (config: Config) => string[];
|
|
326
|
+
invariants?: (config: Config) => string[]; /** See {@link EntityKind.structuralBody}. */
|
|
327
|
+
structuralBody?: (data: unknown) => Record<string, unknown>;
|
|
325
328
|
/**
|
|
326
329
|
* For a source kind that can be resolved: the two schemas its resolutions are validated against —
|
|
327
330
|
* `{ resolved, unavailable }`. Declare it alongside `sourceIdentity` for a kind a config lists in its
|
|
@@ -132,7 +132,8 @@ function defineEntity(spec) {
|
|
|
132
132
|
uniqueLeaves: spec.uniqueLeaves,
|
|
133
133
|
authoredValue: spec.authoredValue,
|
|
134
134
|
mentionFields: spec.mentionFields,
|
|
135
|
-
invariants: spec.invariants
|
|
135
|
+
invariants: spec.invariants,
|
|
136
|
+
structuralBody: spec.structuralBody
|
|
136
137
|
});
|
|
137
138
|
const schema = refTo(spec.kind);
|
|
138
139
|
/**
|
package/dist/framework/plan.d.ts
CHANGED
|
@@ -37,8 +37,12 @@ interface RenamePlan {
|
|
|
37
37
|
}
|
|
38
38
|
interface DeletePlan {
|
|
39
39
|
readonly path: string;
|
|
40
|
-
/** What still points at the entity. Each has to move or go before the delete is allowed
|
|
40
|
+
/** What still points at the entity. Each has to move or go before the delete is allowed, unless it
|
|
41
|
+
* is in `cascade`. */
|
|
41
42
|
readonly dependents: readonly EntityAddress[];
|
|
43
|
+
/** What the delete takes with it, in the order it deletes them, ahead of the entity itself. Reported
|
|
44
|
+
* even when the delete would be refused, as what it would take. */
|
|
45
|
+
readonly cascade: readonly EntityAddress[];
|
|
42
46
|
/** Why the delete would be refused. `undefined` when it would go through. */
|
|
43
47
|
readonly blocked?: string;
|
|
44
48
|
}
|
package/dist/framework/plan.js
CHANGED
|
@@ -66,9 +66,14 @@ function planDelete({ config, kind, path }) {
|
|
|
66
66
|
operation: "delete",
|
|
67
67
|
path
|
|
68
68
|
}]));
|
|
69
|
+
const cascade = config.cascadeOf({
|
|
70
|
+
kind,
|
|
71
|
+
path
|
|
72
|
+
});
|
|
69
73
|
return {
|
|
70
74
|
path,
|
|
71
75
|
dependents: addresses(config.dependentsOf(kind, path)),
|
|
76
|
+
cascade,
|
|
72
77
|
...blocked === void 0 ? {} : { blocked }
|
|
73
78
|
};
|
|
74
79
|
}
|
|
@@ -338,6 +338,13 @@ interface EntityKind {
|
|
|
338
338
|
* a stored artifact stays loadable.
|
|
339
339
|
*/
|
|
340
340
|
readonly invariants?: (config: Config) => string[];
|
|
341
|
+
/**
|
|
342
|
+
* The slice of an item's stored fields whose refs it cannot exist without. Deleting the entity one
|
|
343
|
+
* of these refs names, or an entity whose path it is nested under, deletes the item too, in the
|
|
344
|
+
* same changeset and ahead of the entity. A kind that declares nothing here keeps a plain referrer,
|
|
345
|
+
* which refuses the delete. `group-delete` does not cascade.
|
|
346
|
+
*/
|
|
347
|
+
readonly structuralBody?: (data: unknown) => Record<string, unknown>;
|
|
341
348
|
/**
|
|
342
349
|
* Rewrite one name wherever this kind's data holds it as a key — the cascade half of
|
|
343
350
|
* {@link nameEdges}, applied by `Config.apply`'s rename alongside `rewriteRefs`.
|
|
@@ -32,7 +32,7 @@ function titleCase(kind) {
|
|
|
32
32
|
/** Derive a kind's immutable metadata from its field schemas — the value a `defineEntity`/
|
|
33
33
|
* `defineDerivedEntity` attaches to its class and a `Config.register` stores in `#entities`. */
|
|
34
34
|
function buildKind(spec) {
|
|
35
|
-
const { name, prepareLinkedBody, rebaseLinkedBody, layerKeys, fields, groupFields, subEntities = {}, computed, label, labelPlural, schemas: userSchemas, derived, sourceIdentity, sourceOrigin, authoredRefs, linkable, localGroupFields, emergentGroups, singleton = false, bareMember, sourceResolution, bulkAdopts, bulkOverlay, bulkDirectives, bulkOverlayWrite, nameEdges, rewriteName, styleBags, uniqueLeaves, authoredValue, mentionFields, invariants } = spec;
|
|
35
|
+
const { name, prepareLinkedBody, rebaseLinkedBody, layerKeys, fields, groupFields, subEntities = {}, computed, label, labelPlural, schemas: userSchemas, derived, sourceIdentity, sourceOrigin, authoredRefs, linkable, localGroupFields, emergentGroups, singleton = false, bareMember, sourceResolution, bulkAdopts, bulkOverlay, bulkDirectives, bulkOverlayWrite, nameEdges, rewriteName, styleBags, uniqueLeaves, authoredValue, mentionFields, invariants, structuralBody } = spec;
|
|
36
36
|
const allComputed = {
|
|
37
37
|
sources: ({ member, config }) => sourceSlugs(member, config),
|
|
38
38
|
borrowedFrom: ({ config, path }) => config.borrowedFrom(name, path),
|
|
@@ -82,7 +82,8 @@ function buildKind(spec) {
|
|
|
82
82
|
...uniqueLeaves ? { uniqueLeaves } : {},
|
|
83
83
|
...authoredValue ? { authoredValue } : {},
|
|
84
84
|
...mentionFields?.length ? { mentionFields } : {},
|
|
85
|
-
...invariants ? { invariants } : {}
|
|
85
|
+
...invariants ? { invariants } : {},
|
|
86
|
+
...structuralBody ? { structuralBody } : {}
|
|
86
87
|
};
|
|
87
88
|
}
|
|
88
89
|
/** Every operation a kind exposes — its merged `schemas` map, flattened to `{ kind, verb, ... }`.
|
|
@@ -150,7 +150,7 @@ function defaultSchemas(kind, declaredFields, groupFields, subEntities, { single
|
|
|
150
150
|
input: z.object({ path: PATH }),
|
|
151
151
|
readOnly: false,
|
|
152
152
|
label: "Delete",
|
|
153
|
-
description: `Delete the ${kind} at \`path
|
|
153
|
+
description: `Delete the ${kind} at \`path\`, together with any entity that cannot exist without it, such as a token aliasing it or a component whose layers render it. Refused while anything else still references it; \`delete-plan\` names both first.`,
|
|
154
154
|
scope: "item",
|
|
155
155
|
approval: "destructive",
|
|
156
156
|
handler: (i, c) => c.apply({
|
|
@@ -222,7 +222,7 @@ function defaultSchemas(kind, declaredFields, groupFields, subEntities, { single
|
|
|
222
222
|
input: z.object({ path: PATH }),
|
|
223
223
|
readOnly: true,
|
|
224
224
|
label: "Plan a delete",
|
|
225
|
-
description: `What deleting the ${kind} at \`path\`
|
|
225
|
+
description: `What deleting the ${kind} at \`path\` would do: the entities still pointing at it, the ones the delete takes with it (\`cascade\`), and the refusal the delete would answer with. Same input as \`delete\`; writes nothing.`,
|
|
226
226
|
scope: "item",
|
|
227
227
|
title: (entity) => `Plan deleting ${entity}`,
|
|
228
228
|
handler: (i, c) => planDelete({
|
|
@@ -183,7 +183,11 @@ declare class ConfigSession<C extends Config = Config> implements LiveConfig {
|
|
|
183
183
|
* so a patch removed only here reappears on the next change notification — any peer edit, any
|
|
184
184
|
* reconnect. See {@link ConfigSink.retract}.
|
|
185
185
|
*/
|
|
186
|
-
drop(patchIds: readonly string[]
|
|
186
|
+
drop(patchIds: readonly string[], {
|
|
187
|
+
patchesOnly
|
|
188
|
+
}?: {
|
|
189
|
+
patchesOnly?: boolean;
|
|
190
|
+
}): void;
|
|
187
191
|
/** Hand the pending batch to the sink now. Resolves once the sink has taken it. */
|
|
188
192
|
flush(): Promise<void>;
|
|
189
193
|
/** Detach from the source. The config value remains readable — it just stops advancing. */
|
|
@@ -201,18 +201,21 @@ var ConfigSession = class {
|
|
|
201
201
|
* so a patch removed only here reappears on the next change notification — any peer edit, any
|
|
202
202
|
* reconnect. See {@link ConfigSink.retract}.
|
|
203
203
|
*/
|
|
204
|
-
drop(patchIds) {
|
|
204
|
+
drop(patchIds, { patchesOnly = false } = {}) {
|
|
205
205
|
if (patchIds.length === 0) return;
|
|
206
|
-
const
|
|
206
|
+
const named = new Set(patchIds);
|
|
207
207
|
const before = this.#config.pending;
|
|
208
|
-
const
|
|
208
|
+
const refused = new Set(before.filter((entry) => named.has(entry.id) || !patchesOnly && entry.changeset !== void 0 && named.has(entry.changeset)).map((entry) => entry.id));
|
|
209
|
+
const kept = before.filter((entry) => !refused.has(entry.id));
|
|
209
210
|
if (kept.length === before.length) return;
|
|
210
211
|
const neverSent = new Set(this.#uncommitted.map((entry) => entry.id));
|
|
211
212
|
this.#uncommitted = this.#uncommitted.filter((entry) => !refused.has(entry.id));
|
|
212
213
|
this.#config = this.#afterRebase(this.#config.withDraft(kept), "drop");
|
|
213
214
|
this.#notify();
|
|
214
215
|
const survived = new Set(this.#config.pending.map((entry) => entry.id));
|
|
215
|
-
|
|
216
|
+
const removed = new Set(before.map((entry) => entry.id).filter((id) => !survived.has(id)));
|
|
217
|
+
this.#uncommitted = this.#uncommitted.filter((entry) => !removed.has(entry.id));
|
|
218
|
+
this.#retract([...removed].filter((id) => !neverSent.has(id)));
|
|
216
219
|
}
|
|
217
220
|
/** Write a removal through to the sink, deferring any id whose own commit is still in flight. */
|
|
218
221
|
#retract(ids) {
|