@nubbin/core 0.1.0-rc.0 → 0.1.0-rc.5
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/README.md +37 -0
- package/dist/index.d.ts +93 -14
- package/dist/index.js +219 -110
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# @nubbin/core
|
|
2
|
+
|
|
3
|
+
The contract every other Nubbin package is built around. It defines what a block is, keeps the
|
|
4
|
+
catalog and the registry apart, and compiles a page document into an immutable artifact.
|
|
5
|
+
|
|
6
|
+
It has one runtime dependency — [Standard Schema](https://standardschema.dev) — and imports no
|
|
7
|
+
validator, no framework and no node builtin. A build gate fails on any of the three, so the
|
|
8
|
+
claim that it runs in a browser, a worker and a build step is checked rather than asserted.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @nubbin/core@rc
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { defineBlock, defineCatalog, createRegistry, compile } from "@nubbin/core";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
|
|
18
|
+
const heroSchema = z.object({ title: z.string() });
|
|
19
|
+
|
|
20
|
+
export const heroBlock = defineBlock({
|
|
21
|
+
name: "Hero",
|
|
22
|
+
schema: heroSchema,
|
|
23
|
+
component: Hero,
|
|
24
|
+
version: 1,
|
|
25
|
+
slots: {},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const artifact = compile(documentVersion, catalog, registry, "/promotions/summer");
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Props are inferred from the schema with `InferProps<typeof heroSchema>`, so nothing declares a
|
|
32
|
+
block's shape twice.
|
|
33
|
+
|
|
34
|
+
**Release candidate.** The API is settled enough to build against and not yet stable.
|
|
35
|
+
|
|
36
|
+
The design record, including the paths not taken, is at
|
|
37
|
+
<https://effekt.github.io/nubbin/>. MIT.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
2
|
|
|
3
|
+
type FieldKind = "string" | "number" | "boolean" | "enum" | "array" | "object" | "union" | "unknown";
|
|
4
|
+
interface FieldNode {
|
|
5
|
+
/** Dotted path from the schema root, with `[]` for array members: `cta.label`, `items[].title`. */
|
|
6
|
+
path: string;
|
|
7
|
+
kind: FieldKind;
|
|
8
|
+
optional: boolean;
|
|
9
|
+
/** Present only for `enum`. */
|
|
10
|
+
members?: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
interface SchemaAdapter {
|
|
13
|
+
describe(schema: unknown): FieldNode[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Reads a zod schema for the studio and hint resolution, entirely through the Standard JSON
|
|
18
|
+
* Schema converter the schema itself carries — `core` imports nothing from zod to do it.
|
|
19
|
+
* Validation never runs against this projection; it always runs `schema["~standard"].validate()`
|
|
20
|
+
* on the real schema.
|
|
21
|
+
*/
|
|
22
|
+
declare const zodAdapter: SchemaAdapter;
|
|
23
|
+
|
|
3
24
|
type UnknownProps = Record<string, unknown>;
|
|
4
25
|
/**
|
|
5
26
|
* A block's props, derived from its schema. The whole of invariant 1 in one type: there is no
|
|
@@ -11,7 +32,7 @@ type UnknownProps = Record<string, unknown>;
|
|
|
11
32
|
*/
|
|
12
33
|
type InferProps<Schema extends StandardSchemaV1> = StandardSchemaV1.InferOutput<Schema>;
|
|
13
34
|
interface SlotConstraint {
|
|
14
|
-
/** Block names permitted here. Omitted means any registered block. */
|
|
35
|
+
/** Block names permitted here, each resolved at registration. Omitted means any registered block. */
|
|
15
36
|
allow?: readonly string[];
|
|
16
37
|
min?: number;
|
|
17
38
|
max?: number;
|
|
@@ -148,6 +169,41 @@ declare class CompileError extends Error {
|
|
|
148
169
|
constructor(issues: readonly CompileIssue[]);
|
|
149
170
|
}
|
|
150
171
|
|
|
172
|
+
/** One route pointer and the artifact it names, as an adapter read them. */
|
|
173
|
+
interface LiveRoute {
|
|
174
|
+
pointer: RoutePointer;
|
|
175
|
+
/** `null` when the store holds no artifact at the pointer's hash — a pointer into nothing. */
|
|
176
|
+
artifact: Artifact | null;
|
|
177
|
+
}
|
|
178
|
+
/** One block's version delta between what a live artifact needs and what is registered now. */
|
|
179
|
+
interface BlockDrift {
|
|
180
|
+
block: string;
|
|
181
|
+
/** The version the artifact was compiled against. */
|
|
182
|
+
live: number;
|
|
183
|
+
/** The version registered now — `null` when the registry no longer holds the block at all. */
|
|
184
|
+
registered: number | null;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Why one live route would not render. `unreadable-artifact` is a pointer whose hash the store
|
|
188
|
+
* cannot resolve, which breaks the route without any registry change at all.
|
|
189
|
+
*/
|
|
190
|
+
type RouteIncompatibility = {
|
|
191
|
+
route: string;
|
|
192
|
+
hash: string;
|
|
193
|
+
reason: "unreadable-artifact";
|
|
194
|
+
} | {
|
|
195
|
+
route: string;
|
|
196
|
+
hash: string;
|
|
197
|
+
reason: "block-drift";
|
|
198
|
+
drifted: BlockDrift[];
|
|
199
|
+
};
|
|
200
|
+
/** The verdict over every live pointer. `checked` is reported so a run over nothing cannot read as a pass. */
|
|
201
|
+
interface CompatibilityReport {
|
|
202
|
+
checked: number;
|
|
203
|
+
compatible: boolean;
|
|
204
|
+
incompatible: RouteIncompatibility[];
|
|
205
|
+
}
|
|
206
|
+
|
|
151
207
|
interface Registry {
|
|
152
208
|
get(name: string): Block | undefined;
|
|
153
209
|
names(): string[];
|
|
@@ -155,6 +211,17 @@ interface Registry {
|
|
|
155
211
|
fingerprint(): string;
|
|
156
212
|
}
|
|
157
213
|
|
|
214
|
+
/**
|
|
215
|
+
* The guardrail's whole question: would this registry fail to render an artifact a live route
|
|
216
|
+
* pointer currently references? `checkRollback` decides each route — one artifact against the
|
|
217
|
+
* registry is the same comparison whether a pointer is moving or a registry is — and this walks
|
|
218
|
+
* every pointer the caller read, so publishing and merging are held to one rule.
|
|
219
|
+
*
|
|
220
|
+
* Pure, and synchronous: the caller reads the store and hands over what it found, so this runs
|
|
221
|
+
* in CI, in a worker, or in a browser studio unchanged.
|
|
222
|
+
*/
|
|
223
|
+
declare function checkCompatibility(live: readonly LiveRoute[], registry: Registry): CompatibilityReport;
|
|
224
|
+
|
|
158
225
|
type RollbackCheck = {
|
|
159
226
|
compatible: true;
|
|
160
227
|
} | {
|
|
@@ -179,6 +246,9 @@ declare function compile(version: DocumentVersion, catalog: Catalog, registry: R
|
|
|
179
246
|
* Sorted by name so registration order cannot change the fingerprint, and built from name and
|
|
180
247
|
* version alone so unrelated edits — a slot constraint, a deprecation — do not invalidate every
|
|
181
248
|
* artifact compiled before them.
|
|
249
|
+
*
|
|
250
|
+
* Slot `allow` lists resolve once the whole array is ingested, so a block may name a sibling
|
|
251
|
+
* registered after it.
|
|
182
252
|
*/
|
|
183
253
|
declare function createRegistry(blocks: readonly Block[]): Registry;
|
|
184
254
|
|
|
@@ -197,18 +267,12 @@ declare function defineBlock<Schema extends StandardSchemaV1, Component>(block:
|
|
|
197
267
|
*/
|
|
198
268
|
declare function defineCatalog(entries: Record<string, CatalogEntry>): Catalog;
|
|
199
269
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
/** Present only for `enum`. */
|
|
207
|
-
members?: readonly string[];
|
|
208
|
-
}
|
|
209
|
-
interface SchemaAdapter {
|
|
210
|
-
describe(schema: unknown): FieldNode[];
|
|
211
|
-
}
|
|
270
|
+
/**
|
|
271
|
+
* The report as a CI log reads it. `checked` leads every line, including the passing one: a run
|
|
272
|
+
* that found no pointers and a run that cleared eight are the same word otherwise, and the first
|
|
273
|
+
* of those is a gate certifying nothing.
|
|
274
|
+
*/
|
|
275
|
+
declare function formatCompatibilityReport(report: CompatibilityReport): string;
|
|
212
276
|
|
|
213
277
|
/**
|
|
214
278
|
* matchKind is parsed from the route at publish, never caller-supplied. It lives in core so
|
|
@@ -216,4 +280,19 @@ interface SchemaAdapter {
|
|
|
216
280
|
*/
|
|
217
281
|
declare function parseMatchKind(route: string): RoutePointer["matchKind"];
|
|
218
282
|
|
|
219
|
-
|
|
283
|
+
/** Copy-on-write down one dotted path. Paths address object fields only; `[]` has no single target. */
|
|
284
|
+
declare function setAtPath(target: Record<string, unknown>, path: string, value: unknown): Record<string, unknown>;
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* The first document operation: a new `DocumentVersion` with one prop set on one node,
|
|
288
|
+
* copy-on-write, every other node untouched by reference. It lives beside `compile` so any
|
|
289
|
+
* caller — an editor, a script, an agent — writes a document the same way
|
|
290
|
+
* ([#134](https://github.com/effekt/nubbin/issues/134)).
|
|
291
|
+
*
|
|
292
|
+
* It does not validate the value (that is `compile`'s job at the next compile) and does not
|
|
293
|
+
* bump `version` — appending a version is the authoring store's concern
|
|
294
|
+
* ([#11](https://github.com/effekt/nubbin/issues/11)), not a property of one edit.
|
|
295
|
+
*/
|
|
296
|
+
declare function setNodeProp(version: DocumentVersion, nodeId: string, path: string, value: unknown): DocumentVersion;
|
|
297
|
+
|
|
298
|
+
export { type Artifact, type ArtifactNode, type ArtifactStore, type Block, type BlockDocs, type BlockDrift, type BlockUi, type Catalog, type CatalogEntry, type CompatibilityReport, CompileError, type CompileIssue, type CompileIssueCode, type DocumentMeta, type DocumentVersion, type FieldHint, type FieldHintData, type FieldKind, type FieldNode, type Holes, type InferProps, type LiveRoute, type Manifest, type Node, type Registry, type RollbackCheck, type RouteIncompatibility, type RoutePointer, type SchemaAdapter, type SlotConstraint, type UnknownProps, checkCompatibility, checkRollback, compile, createRegistry, defineBlock, defineCatalog, formatCompatibilityReport, parseMatchKind, setAtPath, setNodeProp, zodAdapter };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,111 @@
|
|
|
1
|
+
// src/adapters/isStandardJsonSchemaCapable.ts
|
|
2
|
+
function isStandardJsonSchemaCapable(value) {
|
|
3
|
+
if (typeof value !== "object" || value === null || !("~standard" in value)) return false;
|
|
4
|
+
const props = value["~standard"];
|
|
5
|
+
if (typeof props !== "object" || props === null || !("jsonSchema" in props)) return false;
|
|
6
|
+
const converter = props.jsonSchema;
|
|
7
|
+
if (typeof converter !== "object" || converter === null || !("input" in converter)) return false;
|
|
8
|
+
return typeof converter.input === "function";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// src/adapters/projectJsonSchema.ts
|
|
12
|
+
var OPTIONS = {
|
|
13
|
+
target: "draft-2020-12",
|
|
14
|
+
/** zod's option name: a type JSON Schema cannot express throws here, at registration. */
|
|
15
|
+
libraryOptions: { unrepresentable: "throw" }
|
|
16
|
+
};
|
|
17
|
+
function projectJsonSchema(schema) {
|
|
18
|
+
if (!isStandardJsonSchemaCapable(schema)) {
|
|
19
|
+
throw new Error("Schema does not expose the Standard JSON Schema converter (spec >= 1.1)");
|
|
20
|
+
}
|
|
21
|
+
return schema["~standard"].jsonSchema.input(OPTIONS);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/adapters/kindOfJsonSchema.ts
|
|
25
|
+
function kindOfJsonSchema(node) {
|
|
26
|
+
if (Array.isArray(node.enum)) return "enum";
|
|
27
|
+
if (Array.isArray(node.oneOf) || Array.isArray(node.anyOf)) return "union";
|
|
28
|
+
switch (node.type) {
|
|
29
|
+
case "string":
|
|
30
|
+
return "string";
|
|
31
|
+
case "number":
|
|
32
|
+
case "integer":
|
|
33
|
+
return "number";
|
|
34
|
+
case "boolean":
|
|
35
|
+
return "boolean";
|
|
36
|
+
case "array":
|
|
37
|
+
return "array";
|
|
38
|
+
case "object":
|
|
39
|
+
return "object";
|
|
40
|
+
default:
|
|
41
|
+
return "unknown";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/adapters/fieldNodeAt.ts
|
|
46
|
+
function fieldNodeAt(path, node, optional) {
|
|
47
|
+
const kind = kindOfJsonSchema(node);
|
|
48
|
+
if (kind === "enum" && Array.isArray(node.enum)) {
|
|
49
|
+
return { path, kind, optional, members: node.enum.map(String) };
|
|
50
|
+
}
|
|
51
|
+
return { path, kind, optional };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/adapters/isJsonSchemaNode.ts
|
|
55
|
+
function isJsonSchemaNode(value) {
|
|
56
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/adapters/walkArrayItems.ts
|
|
60
|
+
function walkArrayItems(arrayNode, path, descend) {
|
|
61
|
+
if (!isJsonSchemaNode(arrayNode.items)) return [];
|
|
62
|
+
const itemPath = `${path}[]`;
|
|
63
|
+
return [fieldNodeAt(itemPath, arrayNode.items, false), ...descend(arrayNode.items, itemPath)];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/adapters/walkObjectProperties.ts
|
|
67
|
+
function walkObjectProperties(objectNode, basePath, descend) {
|
|
68
|
+
const { properties, required } = objectNode;
|
|
69
|
+
if (!isJsonSchemaNode(properties)) return [];
|
|
70
|
+
const requiredNames = Array.isArray(required) ? required : [];
|
|
71
|
+
const fields = [];
|
|
72
|
+
for (const [name, child] of Object.entries(properties)) {
|
|
73
|
+
if (!isJsonSchemaNode(child)) continue;
|
|
74
|
+
const path = basePath === "" ? name : `${basePath}.${name}`;
|
|
75
|
+
fields.push(fieldNodeAt(path, child, !requiredNames.includes(name)));
|
|
76
|
+
fields.push(...descend(child, path));
|
|
77
|
+
}
|
|
78
|
+
return fields;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/adapters/walkUnionBranches.ts
|
|
82
|
+
function walkUnionBranches(unionNode, path, descend) {
|
|
83
|
+
const branches = [unionNode.oneOf, unionNode.anyOf].filter(Array.isArray).flat();
|
|
84
|
+
return branches.flatMap((branch) => isJsonSchemaNode(branch) ? descend(branch, path) : []);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/adapters/walkJsonSchema.ts
|
|
88
|
+
function walkJsonSchema(node, basePath) {
|
|
89
|
+
const kind = kindOfJsonSchema(node);
|
|
90
|
+
if (kind === "object") return walkObjectProperties(node, basePath, walkJsonSchema);
|
|
91
|
+
if (kind === "array") return walkArrayItems(node, basePath, walkJsonSchema);
|
|
92
|
+
if (kind === "union") return walkUnionBranches(node, basePath, walkJsonSchema);
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/adapters/zodAdapter.ts
|
|
97
|
+
var zodAdapter = {
|
|
98
|
+
describe(schema) {
|
|
99
|
+
const fields = walkJsonSchema(projectJsonSchema(schema), "");
|
|
100
|
+
const seen = /* @__PURE__ */ new Set();
|
|
101
|
+
return fields.filter((field) => {
|
|
102
|
+
if (seen.has(field.path)) return false;
|
|
103
|
+
seen.add(field.path);
|
|
104
|
+
return true;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
1
109
|
// src/CompileError.ts
|
|
2
110
|
var CompileError = class extends Error {
|
|
3
111
|
issues;
|
|
@@ -16,6 +124,34 @@ function checkRollback(artifact, registry) {
|
|
|
16
124
|
return drifted.length === 0 ? { compatible: true } : { compatible: false, drifted };
|
|
17
125
|
}
|
|
18
126
|
|
|
127
|
+
// src/describeDrift.ts
|
|
128
|
+
function describeDrift(artifact, registry, drifted) {
|
|
129
|
+
return drifted.flatMap((block) => {
|
|
130
|
+
const live = artifact.blockVersions[block];
|
|
131
|
+
return live === void 0 ? [] : [{ block, live, registered: registry.get(block)?.version ?? null }];
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/checkCompatibility.ts
|
|
136
|
+
function checkCompatibility(live, registry) {
|
|
137
|
+
const incompatible = live.flatMap(({ pointer, artifact }) => {
|
|
138
|
+
const { route, hash } = pointer;
|
|
139
|
+
if (artifact === null) {
|
|
140
|
+
return [{ route, hash, reason: "unreadable-artifact" }];
|
|
141
|
+
}
|
|
142
|
+
const check = checkRollback(artifact, registry);
|
|
143
|
+
return check.compatible ? [] : [
|
|
144
|
+
{
|
|
145
|
+
route,
|
|
146
|
+
hash,
|
|
147
|
+
reason: "block-drift",
|
|
148
|
+
drifted: describeDrift(artifact, registry, check.drifted)
|
|
149
|
+
}
|
|
150
|
+
];
|
|
151
|
+
});
|
|
152
|
+
return { checked: live.length, compatible: incompatible.length === 0, incompatible };
|
|
153
|
+
}
|
|
154
|
+
|
|
19
155
|
// src/artifactNodeOf.ts
|
|
20
156
|
function artifactNodeOf(node, resolve) {
|
|
21
157
|
const { props, holes } = resolve(node);
|
|
@@ -401,7 +537,7 @@ function validateStructure(version, registry) {
|
|
|
401
537
|
}
|
|
402
538
|
|
|
403
539
|
// src/version.constants.ts
|
|
404
|
-
var NUBBIN_VERSION = "0.0.
|
|
540
|
+
var NUBBIN_VERSION = "0.1.0-rc.5";
|
|
405
541
|
|
|
406
542
|
// src/compile.ts
|
|
407
543
|
function compile(version, catalog, registry, route) {
|
|
@@ -423,6 +559,24 @@ function compile(version, catalog, registry, route) {
|
|
|
423
559
|
return { ...content, hash: hashArtifact(content) };
|
|
424
560
|
}
|
|
425
561
|
|
|
562
|
+
// src/unknownAllowEntries.ts
|
|
563
|
+
function unknownAllowEntries(block, known) {
|
|
564
|
+
return Object.entries(block.slots).flatMap(
|
|
565
|
+
([slot, constraint]) => (constraint.allow ?? []).filter((allowed) => !known.has(allowed)).map((allowed) => `"${allowed}" (${block.name}.${slot})`)
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// src/assertSlotAllows.ts
|
|
570
|
+
function assertSlotAllows(blocks) {
|
|
571
|
+
const known = new Set(blocks.map((block) => block.name));
|
|
572
|
+
const unresolved = blocks.flatMap((block) => unknownAllowEntries(block, known));
|
|
573
|
+
if (unresolved.length > 0) {
|
|
574
|
+
throw new Error(
|
|
575
|
+
`Slot allow lists name ${unresolved.join(", ")}, which no registered block defines. Registered blocks: ${[...known].sort().join(", ")}`
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
426
580
|
// src/createRegistry.ts
|
|
427
581
|
function createRegistry(blocks) {
|
|
428
582
|
const byName = /* @__PURE__ */ new Map();
|
|
@@ -434,6 +588,7 @@ function createRegistry(blocks) {
|
|
|
434
588
|
}
|
|
435
589
|
byName.set(block.name, block);
|
|
436
590
|
}
|
|
591
|
+
assertSlotAllows([...byName.values()]);
|
|
437
592
|
const signature = [...byName.values()].map((block) => `${block.name}@${block.version}`).sort().join("\n");
|
|
438
593
|
const fingerprint = fnv1a(signature);
|
|
439
594
|
return {
|
|
@@ -478,114 +633,6 @@ function defineBlock(block) {
|
|
|
478
633
|
return block;
|
|
479
634
|
}
|
|
480
635
|
|
|
481
|
-
// src/adapters/isStandardJsonSchemaCapable.ts
|
|
482
|
-
function isStandardJsonSchemaCapable(value) {
|
|
483
|
-
if (typeof value !== "object" || value === null || !("~standard" in value)) return false;
|
|
484
|
-
const props = value["~standard"];
|
|
485
|
-
if (typeof props !== "object" || props === null || !("jsonSchema" in props)) return false;
|
|
486
|
-
const converter = props.jsonSchema;
|
|
487
|
-
if (typeof converter !== "object" || converter === null || !("input" in converter)) return false;
|
|
488
|
-
return typeof converter.input === "function";
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// src/adapters/projectJsonSchema.ts
|
|
492
|
-
var OPTIONS = {
|
|
493
|
-
target: "draft-2020-12",
|
|
494
|
-
/** zod's option name: a type JSON Schema cannot express throws here, at registration. */
|
|
495
|
-
libraryOptions: { unrepresentable: "throw" }
|
|
496
|
-
};
|
|
497
|
-
function projectJsonSchema(schema) {
|
|
498
|
-
if (!isStandardJsonSchemaCapable(schema)) {
|
|
499
|
-
throw new Error("Schema does not expose the Standard JSON Schema converter (spec >= 1.1)");
|
|
500
|
-
}
|
|
501
|
-
return schema["~standard"].jsonSchema.input(OPTIONS);
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// src/adapters/kindOfJsonSchema.ts
|
|
505
|
-
function kindOfJsonSchema(node) {
|
|
506
|
-
if (Array.isArray(node.enum)) return "enum";
|
|
507
|
-
if (Array.isArray(node.oneOf) || Array.isArray(node.anyOf)) return "union";
|
|
508
|
-
switch (node.type) {
|
|
509
|
-
case "string":
|
|
510
|
-
return "string";
|
|
511
|
-
case "number":
|
|
512
|
-
case "integer":
|
|
513
|
-
return "number";
|
|
514
|
-
case "boolean":
|
|
515
|
-
return "boolean";
|
|
516
|
-
case "array":
|
|
517
|
-
return "array";
|
|
518
|
-
case "object":
|
|
519
|
-
return "object";
|
|
520
|
-
default:
|
|
521
|
-
return "unknown";
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// src/adapters/fieldNodeAt.ts
|
|
526
|
-
function fieldNodeAt(path, node, optional) {
|
|
527
|
-
const kind = kindOfJsonSchema(node);
|
|
528
|
-
if (kind === "enum" && Array.isArray(node.enum)) {
|
|
529
|
-
return { path, kind, optional, members: node.enum.map(String) };
|
|
530
|
-
}
|
|
531
|
-
return { path, kind, optional };
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// src/adapters/isJsonSchemaNode.ts
|
|
535
|
-
function isJsonSchemaNode(value) {
|
|
536
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// src/adapters/walkArrayItems.ts
|
|
540
|
-
function walkArrayItems(arrayNode, path, descend) {
|
|
541
|
-
if (!isJsonSchemaNode(arrayNode.items)) return [];
|
|
542
|
-
const itemPath = `${path}[]`;
|
|
543
|
-
return [fieldNodeAt(itemPath, arrayNode.items, false), ...descend(arrayNode.items, itemPath)];
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// src/adapters/walkObjectProperties.ts
|
|
547
|
-
function walkObjectProperties(objectNode, basePath, descend) {
|
|
548
|
-
const { properties, required } = objectNode;
|
|
549
|
-
if (!isJsonSchemaNode(properties)) return [];
|
|
550
|
-
const requiredNames = Array.isArray(required) ? required : [];
|
|
551
|
-
const fields = [];
|
|
552
|
-
for (const [name, child] of Object.entries(properties)) {
|
|
553
|
-
if (!isJsonSchemaNode(child)) continue;
|
|
554
|
-
const path = basePath === "" ? name : `${basePath}.${name}`;
|
|
555
|
-
fields.push(fieldNodeAt(path, child, !requiredNames.includes(name)));
|
|
556
|
-
fields.push(...descend(child, path));
|
|
557
|
-
}
|
|
558
|
-
return fields;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
// src/adapters/walkUnionBranches.ts
|
|
562
|
-
function walkUnionBranches(unionNode, path, descend) {
|
|
563
|
-
const branches = [unionNode.oneOf, unionNode.anyOf].filter(Array.isArray).flat();
|
|
564
|
-
return branches.flatMap((branch) => isJsonSchemaNode(branch) ? descend(branch, path) : []);
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// src/adapters/walkJsonSchema.ts
|
|
568
|
-
function walkJsonSchema(node, basePath) {
|
|
569
|
-
const kind = kindOfJsonSchema(node);
|
|
570
|
-
if (kind === "object") return walkObjectProperties(node, basePath, walkJsonSchema);
|
|
571
|
-
if (kind === "array") return walkArrayItems(node, basePath, walkJsonSchema);
|
|
572
|
-
if (kind === "union") return walkUnionBranches(node, basePath, walkJsonSchema);
|
|
573
|
-
return [];
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
// src/adapters/zodAdapter.ts
|
|
577
|
-
var zodAdapter = {
|
|
578
|
-
describe(schema) {
|
|
579
|
-
const fields = walkJsonSchema(projectJsonSchema(schema), "");
|
|
580
|
-
const seen = /* @__PURE__ */ new Set();
|
|
581
|
-
return fields.filter((field) => {
|
|
582
|
-
if (seen.has(field.path)) return false;
|
|
583
|
-
seen.add(field.path);
|
|
584
|
-
return true;
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
|
-
};
|
|
588
|
-
|
|
589
636
|
// src/adapters/resolveHintPaths.ts
|
|
590
637
|
function resolveHintPaths(blockName, schema, fields) {
|
|
591
638
|
const known = new Set(zodAdapter.describe(schema).map((field) => field.path));
|
|
@@ -597,6 +644,17 @@ function resolveHintPaths(blockName, schema, fields) {
|
|
|
597
644
|
}
|
|
598
645
|
}
|
|
599
646
|
|
|
647
|
+
// src/assertDataHintAddressable.ts
|
|
648
|
+
function assertDataHintAddressable(blockName, fields) {
|
|
649
|
+
for (const [path, hint] of Object.entries(fields)) {
|
|
650
|
+
if (hint.data !== void 0 && path.includes("[]")) {
|
|
651
|
+
throw new Error(
|
|
652
|
+
`${blockName}: ui.fields["${path}"] sets \`data\`, but a hole cannot address an array member \u2014 "[]" has no single target`
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
600
658
|
// src/assertValidDefaults.ts
|
|
601
659
|
function assertValidDefaults(blockName, schema, defaults) {
|
|
602
660
|
const result = standardValidate(schema, defaults);
|
|
@@ -610,6 +668,7 @@ function defineCatalog(entries) {
|
|
|
610
668
|
for (const [blockName, entry] of Object.entries(entries)) {
|
|
611
669
|
if (entry.ui?.fields !== void 0) {
|
|
612
670
|
resolveHintPaths(blockName, entry.schema, entry.ui.fields);
|
|
671
|
+
assertDataHintAddressable(blockName, entry.ui.fields);
|
|
613
672
|
}
|
|
614
673
|
if (entry.defaults !== void 0) {
|
|
615
674
|
assertValidDefaults(blockName, entry.schema, entry.defaults);
|
|
@@ -618,6 +677,27 @@ function defineCatalog(entries) {
|
|
|
618
677
|
return entries;
|
|
619
678
|
}
|
|
620
679
|
|
|
680
|
+
// src/formatBlockDrift.ts
|
|
681
|
+
function formatBlockDrift(drift) {
|
|
682
|
+
return drift.registered === null ? `${drift.block}: page needs v${drift.live}, no longer in the registry` : `${drift.block}: page needs v${drift.live}, registry has v${drift.registered}`;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// src/formatRouteIncompatibility.ts
|
|
686
|
+
function formatRouteIncompatibility(incompatibility) {
|
|
687
|
+
const heading = ` ${incompatibility.route} (artifact ${incompatibility.hash})`;
|
|
688
|
+
const reasons = incompatibility.reason === "unreadable-artifact" ? ["the store holds no artifact at this hash"] : incompatibility.drifted.map(formatBlockDrift);
|
|
689
|
+
return [heading, ...reasons.map((reason) => ` ${reason}`)].join("\n");
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// src/formatCompatibilityReport.ts
|
|
693
|
+
function formatCompatibilityReport(report) {
|
|
694
|
+
if (report.compatible) {
|
|
695
|
+
return `${report.checked} live route pointer(s) checked; every one is compatible with this registry.`;
|
|
696
|
+
}
|
|
697
|
+
const summary = `${report.incompatible.length} of ${report.checked} live route pointer(s) are incompatible with this registry:`;
|
|
698
|
+
return [summary, ...report.incompatible.map(formatRouteIncompatibility)].join("\n");
|
|
699
|
+
}
|
|
700
|
+
|
|
621
701
|
// src/parseMatchKind.ts
|
|
622
702
|
function parseMatchKind(route) {
|
|
623
703
|
if (route.endsWith("/*")) {
|
|
@@ -628,12 +708,41 @@ function parseMatchKind(route) {
|
|
|
628
708
|
}
|
|
629
709
|
return "exact";
|
|
630
710
|
}
|
|
711
|
+
|
|
712
|
+
// src/setAtPath.ts
|
|
713
|
+
function setAtPath(target, path, value) {
|
|
714
|
+
const [head, ...rest] = path.split(".");
|
|
715
|
+
if (head === void 0 || head === "" || head.includes("[]")) {
|
|
716
|
+
throw new Error(`path "${path}" is not addressable`);
|
|
717
|
+
}
|
|
718
|
+
if (rest.length === 0) {
|
|
719
|
+
return { ...target, [head]: value };
|
|
720
|
+
}
|
|
721
|
+
const child = target[head];
|
|
722
|
+
const base = typeof child === "object" && child !== null ? child : {};
|
|
723
|
+
return { ...target, [head]: setAtPath(base, rest.join("."), value) };
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// src/setNodeProp.ts
|
|
727
|
+
function setNodeProp(version, nodeId, path, value) {
|
|
728
|
+
const node = version.elements[nodeId];
|
|
729
|
+
if (node === void 0) {
|
|
730
|
+
throw new Error(`no node "${nodeId}" in document "${version.documentId}"`);
|
|
731
|
+
}
|
|
732
|
+
const edited = { ...node, props: setAtPath(node.props, path, value) };
|
|
733
|
+
return { ...version, elements: { ...version.elements, [nodeId]: edited } };
|
|
734
|
+
}
|
|
631
735
|
export {
|
|
632
736
|
CompileError,
|
|
737
|
+
checkCompatibility,
|
|
633
738
|
checkRollback,
|
|
634
739
|
compile,
|
|
635
740
|
createRegistry,
|
|
636
741
|
defineBlock,
|
|
637
742
|
defineCatalog,
|
|
638
|
-
|
|
743
|
+
formatCompatibilityReport,
|
|
744
|
+
parseMatchKind,
|
|
745
|
+
setAtPath,
|
|
746
|
+
setNodeProp,
|
|
747
|
+
zodAdapter
|
|
639
748
|
};
|
package/package.json
CHANGED