@nubbin/core 0.1.0-rc.6 → 0.1.0-rc.7
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/index.d.ts +184 -56
- package/dist/index.js +369 -100
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -45,41 +45,9 @@ interface Block<Schema extends StandardSchemaV1 = StandardSchemaV1, Component =
|
|
|
45
45
|
component: Component;
|
|
46
46
|
/** Bumped when the schema changes incompatibly. */
|
|
47
47
|
version: number;
|
|
48
|
-
/** A deprecated block still resolves; the studio hides it from the palette. */
|
|
49
|
-
status?: "active" | "deprecated";
|
|
50
48
|
slots: Record<string, SlotConstraint>;
|
|
51
49
|
}
|
|
52
50
|
|
|
53
|
-
/** How a field's value resolves at render. Absent means static — the value freezes into props. */
|
|
54
|
-
type FieldHintData = "request" | {
|
|
55
|
-
revalidate: number;
|
|
56
|
-
};
|
|
57
|
-
/**
|
|
58
|
-
* Studio treatment for one schema path. Open by design: control resolution ranks testers over
|
|
59
|
-
* these hints, so a consumer can carry keys core does not read.
|
|
60
|
-
*/
|
|
61
|
-
interface FieldHint {
|
|
62
|
-
label?: string;
|
|
63
|
-
control?: string;
|
|
64
|
-
data?: FieldHintData;
|
|
65
|
-
}
|
|
66
|
-
interface BlockUi {
|
|
67
|
-
/** Keyed by schema path (`title`, `cta.label`, `items[].icon`). Unresolvable paths fail registration. */
|
|
68
|
-
fields?: Record<string, FieldHint>;
|
|
69
|
-
}
|
|
70
|
-
interface BlockDocs {
|
|
71
|
-
summary?: string;
|
|
72
|
-
usage?: string;
|
|
73
|
-
}
|
|
74
|
-
/** Serializable data only — what the studio and CI read. Components live in the registry. */
|
|
75
|
-
interface CatalogEntry {
|
|
76
|
-
schema: unknown;
|
|
77
|
-
ui?: BlockUi;
|
|
78
|
-
defaults?: UnknownProps;
|
|
79
|
-
docs?: BlockDocs;
|
|
80
|
-
}
|
|
81
|
-
type Catalog = Record<string, CatalogEntry>;
|
|
82
|
-
|
|
83
51
|
interface DocumentMeta {
|
|
84
52
|
title: string;
|
|
85
53
|
description?: string;
|
|
@@ -108,6 +76,46 @@ interface DocumentVersion {
|
|
|
108
76
|
createdBy: string;
|
|
109
77
|
}
|
|
110
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Places a node in a parent's slot: a new `DocumentVersion` with the node registered and the
|
|
81
|
+
* slot's order rewritten, copy-on-write, every other node untouched by reference.
|
|
82
|
+
*
|
|
83
|
+
* `node.id` arrives already minted. `core` runs in a browser, a worker and a build step, and a
|
|
84
|
+
* generator inside it would make the same composition produce a different document every time —
|
|
85
|
+
* so the caller mints, and the two things this can still check are the parent it names and the
|
|
86
|
+
* id it brings.
|
|
87
|
+
*
|
|
88
|
+
* Slot legality is not one of them. Whether the block belongs in that slot, and whether the slot
|
|
89
|
+
* is now over its `max`, is `compile`'s judgment — the same division `setNodeProp` follows for a
|
|
90
|
+
* prop's validity, and what lets a document be illegal between two edits that end legal.
|
|
91
|
+
*
|
|
92
|
+
* `index` inserts; its absence appends.
|
|
93
|
+
*/
|
|
94
|
+
declare function addNode(version: DocumentVersion, parentId: string, slot: string, node: Node, index?: number): DocumentVersion;
|
|
95
|
+
|
|
96
|
+
/** How a field's value resolves at render. Absent means static — the value freezes into props. */
|
|
97
|
+
type FieldHintData = {
|
|
98
|
+
revalidate: number;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Studio treatment for one schema path. Open by design: a consumer may carry keys core does not
|
|
102
|
+
* read, and core reads exactly one — how the field resolves.
|
|
103
|
+
*/
|
|
104
|
+
interface FieldHint {
|
|
105
|
+
data?: FieldHintData;
|
|
106
|
+
}
|
|
107
|
+
interface BlockUi {
|
|
108
|
+
/** Keyed by schema path (`title`, `cta.label`, `items[].icon`). Unresolvable paths fail registration. */
|
|
109
|
+
fields?: Record<string, FieldHint>;
|
|
110
|
+
}
|
|
111
|
+
/** Serializable data only — what the studio and CI read. Components live in the registry. */
|
|
112
|
+
interface CatalogEntry {
|
|
113
|
+
schema: unknown;
|
|
114
|
+
ui?: BlockUi;
|
|
115
|
+
defaults?: UnknownProps;
|
|
116
|
+
}
|
|
117
|
+
type Catalog = Record<string, CatalogEntry>;
|
|
118
|
+
|
|
111
119
|
/** Field path → how the field resolves at render. */
|
|
112
120
|
type Holes = Record<string, FieldHintData>;
|
|
113
121
|
/** Resolved — no lookups, no dangling references possible. */
|
|
@@ -126,7 +134,6 @@ interface Artifact {
|
|
|
126
134
|
route: string;
|
|
127
135
|
documentId: string;
|
|
128
136
|
documentVersion: number;
|
|
129
|
-
registryFingerprint: string;
|
|
130
137
|
/** What this was compiled against — only the blocks the document uses. */
|
|
131
138
|
blockVersions: Record<string, number>;
|
|
132
139
|
tree: ArtifactNode[];
|
|
@@ -156,21 +163,6 @@ interface ArtifactStore {
|
|
|
156
163
|
unpublish(route: string): Promise<void>;
|
|
157
164
|
}
|
|
158
165
|
|
|
159
|
-
type CompileIssueCode = "no-roots" | "unknown-block" | "dangling-child" | "cycle" | "unreachable" | "slot-not-allowed" | "slot-min" | "slot-max" | "invalid-props";
|
|
160
|
-
interface CompileIssue {
|
|
161
|
-
nodeId: string;
|
|
162
|
-
/** Where in the node the problem sits: `block`, `slots.items`, or a dotted prop path. */
|
|
163
|
-
path: string;
|
|
164
|
-
code: CompileIssueCode;
|
|
165
|
-
message: string;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/** Carries every issue found in one pass, so an author fixing six problems sees six. */
|
|
169
|
-
declare class CompileError extends Error {
|
|
170
|
-
readonly issues: readonly CompileIssue[];
|
|
171
|
-
constructor(issues: readonly CompileIssue[]);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
166
|
/** One route pointer and the artifact it names, as an adapter read them. */
|
|
175
167
|
interface LiveRoute {
|
|
176
168
|
pointer: RoutePointer;
|
|
@@ -209,8 +201,6 @@ interface CompatibilityReport {
|
|
|
209
201
|
interface Registry {
|
|
210
202
|
get(name: string): Block | undefined;
|
|
211
203
|
names(): string[];
|
|
212
|
-
/** Hash of every block name and version. Nothing else. */
|
|
213
|
-
fingerprint(): string;
|
|
214
204
|
}
|
|
215
205
|
|
|
216
206
|
/**
|
|
@@ -238,17 +228,96 @@ type RollbackCheck = {
|
|
|
238
228
|
*/
|
|
239
229
|
declare function checkRollback(artifact: Artifact, registry: Registry): RollbackCheck;
|
|
240
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Every reason Nubbin refuses something, as a value a consumer can key off rather than a string
|
|
233
|
+
* they have to match. Each member's value is its own name in kebab-case, so a serialized issue
|
|
234
|
+
* reads the same in a log as it does in code.
|
|
235
|
+
*
|
|
236
|
+
* A const object rather than a TypeScript `enum`: it yields the same two things — a value to
|
|
237
|
+
* branch on and a type to check against — without the runtime object an `enum` emits, and `core`
|
|
238
|
+
* has to run unchanged in a browser, a worker and a build step.
|
|
239
|
+
*
|
|
240
|
+
* Closed on purpose. A new refusal is a new member here, which is what makes the reference page
|
|
241
|
+
* listing them checkable against this file.
|
|
242
|
+
*/
|
|
243
|
+
declare const NubbinIssueCode: {
|
|
244
|
+
readonly BlockVersion: "block-version";
|
|
245
|
+
readonly SlotBounds: "slot-bounds";
|
|
246
|
+
readonly SlotAllowUnknown: "slot-allow-unknown";
|
|
247
|
+
readonly DuplicateBlockName: "duplicate-block-name";
|
|
248
|
+
readonly InvalidDefaults: "invalid-defaults";
|
|
249
|
+
readonly HintPathUnresolvable: "hint-path-unresolvable";
|
|
250
|
+
readonly HintNotAddressable: "hint-not-addressable";
|
|
251
|
+
readonly NotStandardSchema: "not-standard-schema";
|
|
252
|
+
readonly NoJsonSchema: "no-json-schema";
|
|
253
|
+
readonly NoRoots: "no-roots";
|
|
254
|
+
readonly UnknownBlock: "unknown-block";
|
|
255
|
+
readonly DanglingChild: "dangling-child";
|
|
256
|
+
readonly Cycle: "cycle";
|
|
257
|
+
readonly Unreachable: "unreachable";
|
|
258
|
+
readonly SlotNotAllowed: "slot-not-allowed";
|
|
259
|
+
readonly SlotMin: "slot-min";
|
|
260
|
+
readonly SlotMax: "slot-max";
|
|
261
|
+
readonly InvalidProps: "invalid-props";
|
|
262
|
+
readonly UnknownProp: "unknown-prop";
|
|
263
|
+
readonly NoSuchNode: "no-such-node";
|
|
264
|
+
readonly DuplicateNodeId: "duplicate-node-id";
|
|
265
|
+
readonly PathNotAddressable: "path-not-addressable";
|
|
266
|
+
readonly InvalidRoute: "invalid-route";
|
|
267
|
+
readonly BlockNotLoaded: "block-not-loaded";
|
|
268
|
+
readonly NoHoleResolver: "no-hole-resolver";
|
|
269
|
+
readonly NotOneHostElement: "not-one-host-element";
|
|
270
|
+
readonly ArtifactNotStored: "artifact-not-stored";
|
|
271
|
+
};
|
|
272
|
+
/** The value of any member, for a consumer narrowing on `issue.code`. */
|
|
273
|
+
type NubbinIssueCode = (typeof NubbinIssueCode)[keyof typeof NubbinIssueCode];
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* One reason Nubbin refused something. The shape every refusal takes, whether it was thrown or
|
|
277
|
+
* returned, so a consumer writes one handler and serializes one thing.
|
|
278
|
+
*
|
|
279
|
+
* `code` is for branching and `message` is for reading — the prose says what a person needs and
|
|
280
|
+
* the code says what a program can act on, and neither is asked to do the other's job.
|
|
281
|
+
*
|
|
282
|
+
* `at` and `path` are two coordinates rather than one string because an editing surface needs
|
|
283
|
+
* both: `at` names the thing to select — a node, a block, a route — and `path` names where
|
|
284
|
+
* inside it to highlight. Joining them into prose would leave a studio parsing its own error
|
|
285
|
+
* messages to find the field an author has to fix.
|
|
286
|
+
*/
|
|
287
|
+
interface NubbinIssue {
|
|
288
|
+
code: NubbinIssueCode;
|
|
289
|
+
message: string;
|
|
290
|
+
/** What it concerns: a node id, a block name, or a route. */
|
|
291
|
+
at?: string;
|
|
292
|
+
/** Where within that: a dotted prop path, `slots.items`, or `block`. */
|
|
293
|
+
path?: string;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* What `compile` produces: the artifact, and everything it has to say about the document that
|
|
298
|
+
* did not stop it producing one.
|
|
299
|
+
*
|
|
300
|
+
* The two travel together because the split is not severity, it is whether an artifact exists.
|
|
301
|
+
* A document the schema refuses has none, and `compile` throws; a document carrying a key the
|
|
302
|
+
* schema did not declare has a perfectly good one, and what became of that key is the consumer's
|
|
303
|
+
* to log, ship, or ignore.
|
|
304
|
+
*/
|
|
305
|
+
interface CompileResult {
|
|
306
|
+
artifact: Artifact;
|
|
307
|
+
issues: readonly NubbinIssue[];
|
|
308
|
+
}
|
|
309
|
+
|
|
241
310
|
/**
|
|
242
311
|
* Orchestration only. Structure first, and stop there if it failed — prop validation on a
|
|
243
312
|
* document with dangling references produces cascading noise that buries the real cause.
|
|
313
|
+
*
|
|
314
|
+
* The route is judged before any of it, because it is baked into the artifact and into the
|
|
315
|
+
* content address: an unaddressable route would otherwise compile, hash, and store cleanly, and
|
|
316
|
+
* only fail to match a request once it was live.
|
|
244
317
|
*/
|
|
245
|
-
declare function compile(version: DocumentVersion, catalog: Catalog, registry: Registry, route: string):
|
|
318
|
+
declare function compile(version: DocumentVersion, catalog: Catalog, registry: Registry, route: string): CompileResult;
|
|
246
319
|
|
|
247
320
|
/**
|
|
248
|
-
* Sorted by name so registration order cannot change the fingerprint, and built from name and
|
|
249
|
-
* version alone so unrelated edits — a slot constraint, a deprecation — do not invalidate every
|
|
250
|
-
* artifact compiled before them.
|
|
251
|
-
*
|
|
252
321
|
* Slot `allow` lists resolve once the whole array is ingested, so a block may name a sibling
|
|
253
322
|
* registered after it.
|
|
254
323
|
*/
|
|
@@ -275,12 +344,71 @@ declare function defineCatalog(entries: Record<string, CatalogEntry>): Catalog;
|
|
|
275
344
|
*/
|
|
276
345
|
declare function formatCompatibilityReport(report: CompatibilityReport): string;
|
|
277
346
|
|
|
347
|
+
/**
|
|
348
|
+
* Moves a node into a slot: a new `DocumentVersion` with the reference taken out of whatever held
|
|
349
|
+
* it — a slot or the roots — and placed in the target, the node itself untouched by reference.
|
|
350
|
+
*
|
|
351
|
+
* `index` names a position in the target slot *as it stands after the node is taken out*, which
|
|
352
|
+
* is the only reading under which "move it to the end" is the slot's length. Its absence appends.
|
|
353
|
+
*
|
|
354
|
+
* It does not refuse a move into the node's own subtree. That makes a cycle and detaches the node
|
|
355
|
+
* from the roots, and `compile` refuses both by name — judging it here would be a second opinion
|
|
356
|
+
* on one question, which is the division every operation in this package follows.
|
|
357
|
+
*/
|
|
358
|
+
declare function moveNode(version: DocumentVersion, nodeId: string, toParentId: string, toSlot: string, index?: number): DocumentVersion;
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Every refusal Nubbin throws, carrying its causes as data rather than only as prose. One class
|
|
362
|
+
* for the whole surface: a consumer writes one `catch`, reads `code` or `issues`, and ships that
|
|
363
|
+
* shape to whatever tooling they keep — the package neither logs nor decides what a refusal
|
|
364
|
+
* means to them.
|
|
365
|
+
*
|
|
366
|
+
* It extends `Error`, so a handler that only ever read `.message` keeps working.
|
|
367
|
+
*/
|
|
368
|
+
declare class NubbinError extends Error {
|
|
369
|
+
/**
|
|
370
|
+
* The first issue's code, so the common case reads as
|
|
371
|
+
* `error.code === NubbinIssueCode.UnknownProp` with no array to index and no string to typo.
|
|
372
|
+
* Every refusal but `compile`'s carries exactly one issue; read `issues` for the rest.
|
|
373
|
+
*/
|
|
374
|
+
readonly code: NubbinIssue["code"];
|
|
375
|
+
readonly issues: readonly NubbinIssue[];
|
|
376
|
+
constructor(issues: readonly NubbinIssue[]);
|
|
377
|
+
}
|
|
378
|
+
|
|
278
379
|
/**
|
|
279
380
|
* matchKind is parsed from the route at publish, never caller-supplied. It lives in core so
|
|
280
381
|
* every adapter derives it from one implementation — a second parser is free to disagree.
|
|
382
|
+
*
|
|
383
|
+
* The route is judged before it is classified: this is the last point before a pointer is
|
|
384
|
+
* written, so an adapter that never called `compile` still cannot publish an unaddressable one.
|
|
281
385
|
*/
|
|
282
386
|
declare function parseMatchKind(route: string): RoutePointer["matchKind"];
|
|
283
387
|
|
|
388
|
+
/**
|
|
389
|
+
* Throws the one-cause refusal that almost every surface raises, so a site says what it refuses
|
|
390
|
+
* and why in one line rather than assembling an issue array around it.
|
|
391
|
+
*
|
|
392
|
+
* `compile` is the exception and builds its own `NubbinError`, because it collects: an author
|
|
393
|
+
* fixing six problems should see six, not the first.
|
|
394
|
+
*
|
|
395
|
+
* Returns `never`, so a caller need not `return` after it for the narrowing to hold.
|
|
396
|
+
*/
|
|
397
|
+
declare function refuse(code: NubbinIssue["code"], message: string, at?: string): never;
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Removes a node and everything beneath it: a new `DocumentVersion` with the subtree gone, the
|
|
401
|
+
* reference to it dropped from whatever held it, and every other node untouched by reference.
|
|
402
|
+
*
|
|
403
|
+
* It cascades because removing a section means removing what was in it. Left behind, the children
|
|
404
|
+
* would be unreachable — which `compile` already refuses — so the alternative is a document that
|
|
405
|
+
* cannot compile until an author deletes each orphan by hand.
|
|
406
|
+
*
|
|
407
|
+
* Whether the slot it emptied is now below its `min` is `compile`'s judgment, as it is for every
|
|
408
|
+
* other operation here.
|
|
409
|
+
*/
|
|
410
|
+
declare function removeNode(version: DocumentVersion, nodeId: string): DocumentVersion;
|
|
411
|
+
|
|
284
412
|
/**
|
|
285
413
|
* The inline emphasis a span may carry. Semantic, never stylistic, and closed: a construct
|
|
286
414
|
* outside this set is added as a member deliberately, not smuggled in as markup.
|
|
@@ -343,4 +471,4 @@ declare function setAtPath(target: Record<string, unknown>, path: string, value:
|
|
|
343
471
|
*/
|
|
344
472
|
declare function setNodeProp(version: DocumentVersion, nodeId: string, path: string, value: unknown): DocumentVersion;
|
|
345
473
|
|
|
346
|
-
export { type Artifact, type ArtifactNode, type ArtifactStore, type Block, type
|
|
474
|
+
export { type Artifact, type ArtifactNode, type ArtifactStore, type Block, type BlockDrift, type BlockUi, type Catalog, type CatalogEntry, type CompatibilityReport, type CompileResult, type DocumentMeta, type DocumentVersion, type FieldHint, type FieldHintData, type FieldKind, type FieldNode, type Holes, type InferProps, type LiveRoute, type Manifest, type Node, NubbinError, type NubbinIssue, NubbinIssueCode, type Registry, type RichText, type RichTextBlock, type RichTextBlockKind, type RichTextMark, type RichTextSpan, type RollbackCheck, type RouteIncompatibility, type RoutePointer, type SchemaAdapter, type SlotConstraint, type StandardDataSchema, type UnknownProps, addNode, checkCompatibility, checkRollback, compile, createRegistry, defineBlock, defineCatalog, formatCompatibilityReport, moveNode, parseMatchKind, refuse, removeNode, richText, setAtPath, setNodeProp, zodAdapter };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,80 @@
|
|
|
1
|
+
// src/NubbinIssueCode.ts
|
|
2
|
+
var NubbinIssueCode = {
|
|
3
|
+
// Registration — a block, catalog or registry the developer wrote cannot be used as written.
|
|
4
|
+
BlockVersion: "block-version",
|
|
5
|
+
SlotBounds: "slot-bounds",
|
|
6
|
+
SlotAllowUnknown: "slot-allow-unknown",
|
|
7
|
+
DuplicateBlockName: "duplicate-block-name",
|
|
8
|
+
InvalidDefaults: "invalid-defaults",
|
|
9
|
+
HintPathUnresolvable: "hint-path-unresolvable",
|
|
10
|
+
HintNotAddressable: "hint-not-addressable",
|
|
11
|
+
// Schema — what the consumer brought does not answer the one door core reads a schema through.
|
|
12
|
+
NotStandardSchema: "not-standard-schema",
|
|
13
|
+
NoJsonSchema: "no-json-schema",
|
|
14
|
+
// Structure — the document's graph cannot become a tree.
|
|
15
|
+
NoRoots: "no-roots",
|
|
16
|
+
UnknownBlock: "unknown-block",
|
|
17
|
+
DanglingChild: "dangling-child",
|
|
18
|
+
Cycle: "cycle",
|
|
19
|
+
Unreachable: "unreachable",
|
|
20
|
+
SlotNotAllowed: "slot-not-allowed",
|
|
21
|
+
SlotMin: "slot-min",
|
|
22
|
+
SlotMax: "slot-max",
|
|
23
|
+
// Props — the values a node carries, judged against the block's schema.
|
|
24
|
+
InvalidProps: "invalid-props",
|
|
25
|
+
UnknownProp: "unknown-prop",
|
|
26
|
+
// Document operations — the caller named something the document does not hold.
|
|
27
|
+
NoSuchNode: "no-such-node",
|
|
28
|
+
DuplicateNodeId: "duplicate-node-id",
|
|
29
|
+
PathNotAddressable: "path-not-addressable",
|
|
30
|
+
InvalidRoute: "invalid-route",
|
|
31
|
+
// Render — the artifact and the registry serving it disagree.
|
|
32
|
+
BlockNotLoaded: "block-not-loaded",
|
|
33
|
+
NoHoleResolver: "no-hole-resolver",
|
|
34
|
+
NotOneHostElement: "not-one-host-element",
|
|
35
|
+
// Store — a write the store cannot honour.
|
|
36
|
+
ArtifactNotStored: "artifact-not-stored"
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// src/summariseIssues.ts
|
|
40
|
+
function summariseIssues(issues) {
|
|
41
|
+
const lines = issues.map(
|
|
42
|
+
(issue) => issue.at === void 0 ? `${issue.message} [${issue.code}]` : `${issue.at} [${issue.code}]: ${issue.message}`
|
|
43
|
+
);
|
|
44
|
+
const only = lines[0];
|
|
45
|
+
if (lines.length === 1 && only !== void 0) {
|
|
46
|
+
return only;
|
|
47
|
+
}
|
|
48
|
+
return `Refused with ${issues.length} issue(s):
|
|
49
|
+
${lines.join("\n")}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/NubbinError.ts
|
|
53
|
+
var NubbinError = class extends Error {
|
|
54
|
+
/**
|
|
55
|
+
* The first issue's code, so the common case reads as
|
|
56
|
+
* `error.code === NubbinIssueCode.UnknownProp` with no array to index and no string to typo.
|
|
57
|
+
* Every refusal but `compile`'s carries exactly one issue; read `issues` for the rest.
|
|
58
|
+
*/
|
|
59
|
+
code;
|
|
60
|
+
issues;
|
|
61
|
+
constructor(issues) {
|
|
62
|
+
const first = issues[0];
|
|
63
|
+
if (first === void 0) {
|
|
64
|
+
throw new Error("NubbinError needs an issue \u2014 a refusal with no cause names nothing");
|
|
65
|
+
}
|
|
66
|
+
super(summariseIssues(issues));
|
|
67
|
+
this.name = "NubbinError";
|
|
68
|
+
this.code = first.code;
|
|
69
|
+
this.issues = issues;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// src/refuse.ts
|
|
74
|
+
function refuse(code, message, at) {
|
|
75
|
+
throw new NubbinError([at === void 0 ? { code, message } : { code, message, at }]);
|
|
76
|
+
}
|
|
77
|
+
|
|
1
78
|
// src/adapters/isStandardJsonSchemaCapable.ts
|
|
2
79
|
function isStandardJsonSchemaCapable(value) {
|
|
3
80
|
if (typeof value !== "object" || value === null || !("~standard" in value)) return false;
|
|
@@ -16,7 +93,10 @@ var OPTIONS = {
|
|
|
16
93
|
};
|
|
17
94
|
function projectJsonSchema(schema) {
|
|
18
95
|
if (!isStandardJsonSchemaCapable(schema)) {
|
|
19
|
-
|
|
96
|
+
refuse(
|
|
97
|
+
NubbinIssueCode.NoJsonSchema,
|
|
98
|
+
"Schema does not expose the Standard JSON Schema converter (spec >= 1.1)"
|
|
99
|
+
);
|
|
20
100
|
}
|
|
21
101
|
return schema["~standard"].jsonSchema.input(OPTIONS);
|
|
22
102
|
}
|
|
@@ -106,17 +186,47 @@ var zodAdapter = {
|
|
|
106
186
|
}
|
|
107
187
|
};
|
|
108
188
|
|
|
109
|
-
// src/
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
${
|
|
116
|
-
|
|
117
|
-
|
|
189
|
+
// src/requireNode.ts
|
|
190
|
+
function requireNode(version, nodeId) {
|
|
191
|
+
const node = version.elements[nodeId];
|
|
192
|
+
if (node === void 0) {
|
|
193
|
+
refuse(
|
|
194
|
+
NubbinIssueCode.NoSuchNode,
|
|
195
|
+
`no node "${nodeId}" in document "${version.documentId}"`,
|
|
196
|
+
nodeId
|
|
197
|
+
);
|
|
118
198
|
}
|
|
119
|
-
|
|
199
|
+
return node;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// src/withElements.ts
|
|
203
|
+
function withElements(version, ...nodes) {
|
|
204
|
+
const elements = { ...version.elements };
|
|
205
|
+
for (const node of nodes) {
|
|
206
|
+
elements[node.id] = node;
|
|
207
|
+
}
|
|
208
|
+
return { ...version, elements };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/withSlotChild.ts
|
|
212
|
+
function withSlotChild(node, slot, childId, index) {
|
|
213
|
+
const children = [...node.slots?.[slot] ?? []];
|
|
214
|
+
children.splice(index ?? children.length, 0, childId);
|
|
215
|
+
return { ...node, slots: { ...node.slots, [slot]: children } };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/addNode.ts
|
|
219
|
+
function addNode(version, parentId, slot, node, index) {
|
|
220
|
+
const parent = requireNode(version, parentId);
|
|
221
|
+
if (version.elements[node.id] !== void 0) {
|
|
222
|
+
refuse(
|
|
223
|
+
NubbinIssueCode.DuplicateNodeId,
|
|
224
|
+
`document "${version.documentId}" already holds a node "${node.id}"`,
|
|
225
|
+
node.id
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
return withElements(version, withSlotChild(parent, slot, node.id, index), node);
|
|
229
|
+
}
|
|
120
230
|
|
|
121
231
|
// src/checkRollback.ts
|
|
122
232
|
function checkRollback(artifact, registry) {
|
|
@@ -152,6 +262,43 @@ function checkCompatibility(live, registry) {
|
|
|
152
262
|
return { checked: live.length, compatible: incompatible.length === 0, incompatible };
|
|
153
263
|
}
|
|
154
264
|
|
|
265
|
+
// src/routeSegmentIssue.ts
|
|
266
|
+
var SEGMENT = /^[A-Za-z0-9\-._~%!$&'()+,;=:@]+$/;
|
|
267
|
+
var PARAM = /^\[[A-Za-z0-9_-]+\]$/;
|
|
268
|
+
function routeSegmentIssue(segment, isLast) {
|
|
269
|
+
if (segment === "") {
|
|
270
|
+
return "a segment names nothing";
|
|
271
|
+
}
|
|
272
|
+
if (segment === "*") {
|
|
273
|
+
return isLast ? void 0 : "a prefix star is only the final segment";
|
|
274
|
+
}
|
|
275
|
+
if (segment.startsWith("[") || segment.endsWith("]")) {
|
|
276
|
+
return PARAM.test(segment) ? void 0 : `"${segment}" is not a param segment`;
|
|
277
|
+
}
|
|
278
|
+
return SEGMENT.test(segment) ? void 0 : `"${segment}" carries a character a route cannot`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/assertValidRoute.ts
|
|
282
|
+
function assertValidRoute(route) {
|
|
283
|
+
const unaddressable = (why) => refuse(NubbinIssueCode.InvalidRoute, `route "${route}" is not addressable: ${why}`, route);
|
|
284
|
+
if (!route.startsWith("/")) {
|
|
285
|
+
unaddressable("a route starts at the root, with a slash");
|
|
286
|
+
}
|
|
287
|
+
if (route === "/") {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (route.endsWith("/")) {
|
|
291
|
+
unaddressable("a trailing slash would key a second pointer to one page");
|
|
292
|
+
}
|
|
293
|
+
const segments = route.slice(1).split("/");
|
|
294
|
+
for (const [index, segment] of segments.entries()) {
|
|
295
|
+
const issue = routeSegmentIssue(segment, index === segments.length - 1);
|
|
296
|
+
if (issue !== void 0) {
|
|
297
|
+
unaddressable(issue);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
155
302
|
// src/artifactNodeOf.ts
|
|
156
303
|
function artifactNodeOf(node, resolve) {
|
|
157
304
|
const { props, holes } = resolve(node);
|
|
@@ -211,17 +358,18 @@ function denormalize(version, resolve) {
|
|
|
211
358
|
}
|
|
212
359
|
|
|
213
360
|
// src/fnv1a.ts
|
|
214
|
-
var OFFSET_BASIS =
|
|
215
|
-
var PRIME =
|
|
361
|
+
var OFFSET_BASIS = 14695981039346656037n;
|
|
362
|
+
var PRIME = 1099511628211n;
|
|
363
|
+
var MASK = 0xffffffffffffffffn;
|
|
216
364
|
var HEX_RADIX = 16;
|
|
217
|
-
var HEX_WIDTH =
|
|
365
|
+
var HEX_WIDTH = 16;
|
|
218
366
|
function fnv1a(input) {
|
|
219
367
|
let hash = OFFSET_BASIS;
|
|
220
368
|
for (let index = 0; index < input.length; index += 1) {
|
|
221
|
-
hash ^= input.charCodeAt(index);
|
|
222
|
-
hash =
|
|
369
|
+
hash ^= BigInt(input.charCodeAt(index));
|
|
370
|
+
hash = hash * PRIME & MASK;
|
|
223
371
|
}
|
|
224
|
-
return
|
|
372
|
+
return hash.toString(HEX_RADIX).padStart(HEX_WIDTH, "0");
|
|
225
373
|
}
|
|
226
374
|
|
|
227
375
|
// src/hashArtifact.ts
|
|
@@ -234,11 +382,34 @@ function hashArtifact(artifact) {
|
|
|
234
382
|
return fnv1a(JSON.stringify(artifact, sortKeys));
|
|
235
383
|
}
|
|
236
384
|
|
|
385
|
+
// src/isUnknownProps.ts
|
|
386
|
+
function isUnknownProps(value) {
|
|
387
|
+
if (typeof value !== "object") return false;
|
|
388
|
+
return value !== null && !Array.isArray(value);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/droppedKeyPaths.ts
|
|
392
|
+
function droppedKeyPaths(written, parsed, prefix = "") {
|
|
393
|
+
const dropped = [];
|
|
394
|
+
for (const [key, value] of Object.entries(written)) {
|
|
395
|
+
const path = prefix === "" ? key : `${prefix}.${key}`;
|
|
396
|
+
if (!Object.hasOwn(parsed, key)) {
|
|
397
|
+
dropped.push(path);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
const kept = parsed[key];
|
|
401
|
+
if (isUnknownProps(value) && isUnknownProps(kept)) {
|
|
402
|
+
dropped.push(...droppedKeyPaths(value, kept, path));
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return dropped;
|
|
406
|
+
}
|
|
407
|
+
|
|
237
408
|
// src/splitPath.ts
|
|
238
409
|
function splitPath(path) {
|
|
239
410
|
const [head, ...tail] = path.split(".");
|
|
240
411
|
if (head === void 0 || head === "" || head.includes("[]")) {
|
|
241
|
-
|
|
412
|
+
refuse(NubbinIssueCode.PathNotAddressable, `path "${path}" is not addressable`, path);
|
|
242
413
|
}
|
|
243
414
|
return { head, tail };
|
|
244
415
|
}
|
|
@@ -283,12 +454,6 @@ function formatIssuePath(path) {
|
|
|
283
454
|
).join(".");
|
|
284
455
|
}
|
|
285
456
|
|
|
286
|
-
// src/isUnknownProps.ts
|
|
287
|
-
function isUnknownProps(value) {
|
|
288
|
-
if (typeof value !== "object") return false;
|
|
289
|
-
return value !== null && !Array.isArray(value);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
457
|
// src/isStandardSchema.ts
|
|
293
458
|
function isStandardSchema(value) {
|
|
294
459
|
if (typeof value !== "object" || value === null) return false;
|
|
@@ -301,11 +466,15 @@ function isStandardSchema(value) {
|
|
|
301
466
|
// src/standardValidate.ts
|
|
302
467
|
function standardValidate(schema, value) {
|
|
303
468
|
if (!isStandardSchema(schema)) {
|
|
304
|
-
|
|
469
|
+
refuse(
|
|
470
|
+
NubbinIssueCode.NotStandardSchema,
|
|
471
|
+
"Schema does not implement Standard Schema (`~standard.validate`)"
|
|
472
|
+
);
|
|
305
473
|
}
|
|
306
474
|
const result = schema["~standard"].validate(value);
|
|
307
475
|
if (result instanceof Promise) {
|
|
308
|
-
|
|
476
|
+
refuse(
|
|
477
|
+
NubbinIssueCode.NotStandardSchema,
|
|
309
478
|
"Schema validates asynchronously; compile and registration require synchronous validation"
|
|
310
479
|
);
|
|
311
480
|
}
|
|
@@ -317,9 +486,9 @@ function validateNodeProps(node, schema) {
|
|
|
317
486
|
const result = standardValidate(schema, node.props);
|
|
318
487
|
if (result.issues !== void 0) {
|
|
319
488
|
const issues = result.issues.map((issue) => ({
|
|
320
|
-
|
|
489
|
+
at: node.id,
|
|
321
490
|
path: formatIssuePath(issue.path),
|
|
322
|
-
code:
|
|
491
|
+
code: NubbinIssueCode.InvalidProps,
|
|
323
492
|
message: issue.message
|
|
324
493
|
}));
|
|
325
494
|
return { issues };
|
|
@@ -328,9 +497,9 @@ function validateNodeProps(node, schema) {
|
|
|
328
497
|
return {
|
|
329
498
|
issues: [
|
|
330
499
|
{
|
|
331
|
-
|
|
500
|
+
at: node.id,
|
|
332
501
|
path: "",
|
|
333
|
-
code:
|
|
502
|
+
code: NubbinIssueCode.InvalidProps,
|
|
334
503
|
message: "block props must parse to an object"
|
|
335
504
|
}
|
|
336
505
|
]
|
|
@@ -343,20 +512,29 @@ function validateNodeProps(node, schema) {
|
|
|
343
512
|
function resolveAllProps(version, catalog) {
|
|
344
513
|
const resolved = /* @__PURE__ */ new Map();
|
|
345
514
|
const issues = [];
|
|
515
|
+
const reported = [];
|
|
346
516
|
for (const node of Object.values(version.elements)) {
|
|
347
517
|
const entry = catalog[node.block];
|
|
348
518
|
if (entry === void 0) {
|
|
349
519
|
const message = `"${node.block}" has no catalog entry, so its props cannot be validated`;
|
|
350
|
-
issues.push({ code:
|
|
520
|
+
issues.push({ code: NubbinIssueCode.UnknownBlock, message, at: node.id, path: "block" });
|
|
351
521
|
continue;
|
|
352
522
|
}
|
|
353
523
|
const { value, issues: propIssues } = validateNodeProps(node, entry.schema);
|
|
354
524
|
issues.push(...propIssues);
|
|
355
525
|
if (value !== void 0) {
|
|
526
|
+
for (const path of droppedKeyPaths(node.props, value)) {
|
|
527
|
+
reported.push({
|
|
528
|
+
code: NubbinIssueCode.UnknownProp,
|
|
529
|
+
message: `"${path}" is not a field of ${node.block}, so the schema did not keep it`,
|
|
530
|
+
at: node.id,
|
|
531
|
+
path
|
|
532
|
+
});
|
|
533
|
+
}
|
|
356
534
|
resolved.set(node.id, partitionProps(value, entry.ui));
|
|
357
535
|
}
|
|
358
536
|
}
|
|
359
|
-
return { resolved, issues };
|
|
537
|
+
return { resolved, issues, reported };
|
|
360
538
|
}
|
|
361
539
|
|
|
362
540
|
// src/usedBlockVersions.ts
|
|
@@ -379,6 +557,16 @@ function pushCycleFrame(stack, state, version, id) {
|
|
|
379
557
|
stack.push({ id, edges: slotEdges(node), next: 0 });
|
|
380
558
|
}
|
|
381
559
|
|
|
560
|
+
// src/toIssue.ts
|
|
561
|
+
function toIssue(code, message, at, path) {
|
|
562
|
+
return {
|
|
563
|
+
code,
|
|
564
|
+
message,
|
|
565
|
+
...at === void 0 ? {} : { at },
|
|
566
|
+
...path === void 0 ? {} : { path }
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
382
570
|
// src/findCyclesFrom.ts
|
|
383
571
|
function findCyclesFrom(version, root, state) {
|
|
384
572
|
const stack = [];
|
|
@@ -395,12 +583,14 @@ function findCyclesFrom(version, root, state) {
|
|
|
395
583
|
}
|
|
396
584
|
frame.next += 1;
|
|
397
585
|
if (state.get(edge.childId) === "visiting") {
|
|
398
|
-
issues.push(
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
586
|
+
issues.push(
|
|
587
|
+
toIssue(
|
|
588
|
+
NubbinIssueCode.Cycle,
|
|
589
|
+
`"${frame.id}" reaches back to "${edge.childId}", so the graph cannot flatten into a tree`,
|
|
590
|
+
frame.id,
|
|
591
|
+
`slots.${edge.slot}`
|
|
592
|
+
)
|
|
593
|
+
);
|
|
404
594
|
} else if (!state.has(edge.childId)) {
|
|
405
595
|
pushCycleFrame(stack, state, version, edge.childId);
|
|
406
596
|
}
|
|
@@ -422,12 +612,14 @@ function findDanglingChildren(version) {
|
|
|
422
612
|
for (const node of Object.values(version.elements)) {
|
|
423
613
|
for (const edge of slotEdges(node)) {
|
|
424
614
|
if (version.elements[edge.childId] === void 0) {
|
|
425
|
-
issues.push(
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
615
|
+
issues.push(
|
|
616
|
+
toIssue(
|
|
617
|
+
NubbinIssueCode.DanglingChild,
|
|
618
|
+
`child "${edge.childId}" has no matching element`,
|
|
619
|
+
node.id,
|
|
620
|
+
`slots.${edge.slot}`
|
|
621
|
+
)
|
|
622
|
+
);
|
|
431
623
|
}
|
|
432
624
|
}
|
|
433
625
|
}
|
|
@@ -439,9 +631,9 @@ function findRootIssues(version) {
|
|
|
439
631
|
if (version.roots.length === 0) {
|
|
440
632
|
return [
|
|
441
633
|
{
|
|
442
|
-
|
|
634
|
+
at: "",
|
|
443
635
|
path: "roots",
|
|
444
|
-
code:
|
|
636
|
+
code: NubbinIssueCode.NoRoots,
|
|
445
637
|
message: "a document needs at least one root, and this one names none"
|
|
446
638
|
}
|
|
447
639
|
];
|
|
@@ -449,9 +641,9 @@ function findRootIssues(version) {
|
|
|
449
641
|
return version.roots.flatMap(
|
|
450
642
|
(root) => version.elements[root] === void 0 ? [
|
|
451
643
|
{
|
|
452
|
-
|
|
644
|
+
at: root,
|
|
453
645
|
path: "roots",
|
|
454
|
-
code:
|
|
646
|
+
code: NubbinIssueCode.DanglingChild,
|
|
455
647
|
message: `root "${root}" has no matching element`
|
|
456
648
|
}
|
|
457
649
|
] : []
|
|
@@ -466,9 +658,9 @@ function disallowedChildren(parent, path, childIds, allow, version) {
|
|
|
466
658
|
const child = version.elements[childId];
|
|
467
659
|
if (child === void 0 || allow.includes(child.block)) continue;
|
|
468
660
|
issues.push({
|
|
469
|
-
|
|
661
|
+
at: childId,
|
|
470
662
|
path,
|
|
471
|
-
code:
|
|
663
|
+
code: NubbinIssueCode.SlotNotAllowed,
|
|
472
664
|
message: `"${child.block}" is not allowed in ${path} of "${parent.block}"; allowed: ${allow.join(", ")}`
|
|
473
665
|
});
|
|
474
666
|
}
|
|
@@ -480,20 +672,20 @@ function slotBoundIssues(parentId, path, count, constraint) {
|
|
|
480
672
|
const { min, max } = constraint;
|
|
481
673
|
const bounds = [
|
|
482
674
|
{
|
|
483
|
-
code:
|
|
675
|
+
code: NubbinIssueCode.SlotMin,
|
|
484
676
|
limit: min,
|
|
485
677
|
breached: min !== void 0 && count < min,
|
|
486
678
|
sense: "at least"
|
|
487
679
|
},
|
|
488
680
|
{
|
|
489
|
-
code:
|
|
681
|
+
code: NubbinIssueCode.SlotMax,
|
|
490
682
|
limit: max,
|
|
491
683
|
breached: max !== void 0 && count > max,
|
|
492
684
|
sense: "at most"
|
|
493
685
|
}
|
|
494
686
|
];
|
|
495
687
|
return bounds.filter((bound) => bound.breached).map((bound) => ({
|
|
496
|
-
|
|
688
|
+
at: parentId,
|
|
497
689
|
path,
|
|
498
690
|
code: bound.code,
|
|
499
691
|
message: `${path} holds ${count} of ${bound.sense} ${bound.limit}`
|
|
@@ -506,9 +698,9 @@ function slotIssuesAt(parent, slotName, childIds, constraint, version) {
|
|
|
506
698
|
if (constraint === void 0) {
|
|
507
699
|
return [
|
|
508
700
|
{
|
|
509
|
-
|
|
701
|
+
at: parent.id,
|
|
510
702
|
path,
|
|
511
|
-
code:
|
|
703
|
+
code: NubbinIssueCode.SlotNotAllowed,
|
|
512
704
|
message: `"${parent.block}" declares no slot "${slotName}"`
|
|
513
705
|
}
|
|
514
706
|
];
|
|
@@ -535,28 +727,41 @@ function findSlotViolations(version, registry) {
|
|
|
535
727
|
// src/findUnknownBlocks.ts
|
|
536
728
|
function findUnknownBlocks(version, registry) {
|
|
537
729
|
return Object.values(version.elements).filter((node) => registry.get(node.block) === void 0).map((node) => ({
|
|
538
|
-
|
|
730
|
+
at: node.id,
|
|
539
731
|
path: "block",
|
|
540
|
-
code:
|
|
732
|
+
code: NubbinIssueCode.UnknownBlock,
|
|
541
733
|
message: `"${node.block}" is not a registered block`
|
|
542
734
|
}));
|
|
543
735
|
}
|
|
544
736
|
|
|
545
|
-
// src/
|
|
546
|
-
function
|
|
547
|
-
const
|
|
548
|
-
const
|
|
549
|
-
while (
|
|
550
|
-
const id =
|
|
551
|
-
const node = id === void 0 ? void 0 :
|
|
552
|
-
if (node === void 0)
|
|
737
|
+
// src/idsReachableFrom.ts
|
|
738
|
+
function idsReachableFrom(elements, seeds) {
|
|
739
|
+
const found = /* @__PURE__ */ new Set();
|
|
740
|
+
const pending = [...seeds];
|
|
741
|
+
while (pending.length > 0) {
|
|
742
|
+
const id = pending.pop();
|
|
743
|
+
const node = id === void 0 ? void 0 : elements[id];
|
|
744
|
+
if (node === void 0) {
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
553
747
|
for (const edge of slotEdges(node)) {
|
|
554
|
-
if (
|
|
555
|
-
|
|
556
|
-
|
|
748
|
+
if (found.has(edge.childId)) {
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
found.add(edge.childId);
|
|
752
|
+
pending.push(edge.childId);
|
|
557
753
|
}
|
|
558
754
|
}
|
|
559
|
-
return
|
|
755
|
+
return found;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// src/reachableIds.ts
|
|
759
|
+
function reachableIds(version) {
|
|
760
|
+
const reached = idsReachableFrom(version.elements, version.roots);
|
|
761
|
+
for (const root of version.roots) {
|
|
762
|
+
reached.add(root);
|
|
763
|
+
}
|
|
764
|
+
return reached;
|
|
560
765
|
}
|
|
561
766
|
|
|
562
767
|
// src/findUnreachable.ts
|
|
@@ -566,9 +771,9 @@ function findUnreachable(version) {
|
|
|
566
771
|
for (const node of Object.values(version.elements)) {
|
|
567
772
|
if (reached.has(node.id)) continue;
|
|
568
773
|
issues.push({
|
|
569
|
-
|
|
774
|
+
at: node.id,
|
|
570
775
|
path: "",
|
|
571
|
-
code:
|
|
776
|
+
code: NubbinIssueCode.Unreachable,
|
|
572
777
|
message: `no slot reaches "${node.id}" from any root`
|
|
573
778
|
});
|
|
574
779
|
}
|
|
@@ -590,26 +795,26 @@ function validateStructure(version, registry) {
|
|
|
590
795
|
}
|
|
591
796
|
|
|
592
797
|
// src/version.constants.ts
|
|
593
|
-
var NUBBIN_VERSION = "0.1.0-rc.
|
|
798
|
+
var NUBBIN_VERSION = "0.1.0-rc.7";
|
|
594
799
|
|
|
595
800
|
// src/compile.ts
|
|
596
801
|
function compile(version, catalog, registry, route) {
|
|
802
|
+
assertValidRoute(route);
|
|
597
803
|
const structural = validateStructure(version, registry);
|
|
598
|
-
if (structural.length > 0) throw new
|
|
599
|
-
const { resolved, issues } = resolveAllProps(version, catalog);
|
|
600
|
-
if (issues.length > 0) throw new
|
|
804
|
+
if (structural.length > 0) throw new NubbinError(structural);
|
|
805
|
+
const { resolved, issues, reported } = resolveAllProps(version, catalog);
|
|
806
|
+
if (issues.length > 0) throw new NubbinError(issues);
|
|
601
807
|
const tree = denormalize(version, (node) => resolved.get(node.id) ?? { props: {}, holes: {} });
|
|
602
808
|
const content = {
|
|
603
809
|
route,
|
|
604
810
|
documentId: version.documentId,
|
|
605
811
|
documentVersion: version.version,
|
|
606
|
-
registryFingerprint: registry.fingerprint(),
|
|
607
812
|
blockVersions: usedBlockVersions(version, registry),
|
|
608
813
|
tree,
|
|
609
814
|
meta: version.meta,
|
|
610
815
|
compiledWith: NUBBIN_VERSION
|
|
611
816
|
};
|
|
612
|
-
return { ...content, hash: hashArtifact(content) };
|
|
817
|
+
return { artifact: { ...content, hash: hashArtifact(content) }, issues: reported };
|
|
613
818
|
}
|
|
614
819
|
|
|
615
820
|
// src/unknownAllowEntries.ts
|
|
@@ -624,7 +829,8 @@ function assertSlotAllows(blocks) {
|
|
|
624
829
|
const known = new Set(blocks.map((block) => block.name));
|
|
625
830
|
const unresolved = blocks.flatMap((block) => unknownAllowEntries(block, known));
|
|
626
831
|
if (unresolved.length > 0) {
|
|
627
|
-
|
|
832
|
+
refuse(
|
|
833
|
+
NubbinIssueCode.SlotAllowUnknown,
|
|
628
834
|
`Slot allow lists name ${unresolved.join(", ")}, which no registered block defines. Registered blocks: ${[...known].sort().join(", ")}`
|
|
629
835
|
);
|
|
630
836
|
}
|
|
@@ -635,26 +841,25 @@ function createRegistry(blocks) {
|
|
|
635
841
|
const byName = /* @__PURE__ */ new Map();
|
|
636
842
|
for (const block of blocks) {
|
|
637
843
|
if (byName.has(block.name)) {
|
|
638
|
-
|
|
639
|
-
|
|
844
|
+
refuse(
|
|
845
|
+
NubbinIssueCode.DuplicateBlockName,
|
|
846
|
+
`Duplicate block name "${block.name}" \u2014 names are the identity nodes resolve through`,
|
|
847
|
+
block.name
|
|
640
848
|
);
|
|
641
849
|
}
|
|
642
850
|
byName.set(block.name, block);
|
|
643
851
|
}
|
|
644
852
|
assertSlotAllows([...byName.values()]);
|
|
645
|
-
const signature = [...byName.values()].map((block) => `${block.name}@${block.version}`).sort().join("\n");
|
|
646
|
-
const fingerprint = fnv1a(signature);
|
|
647
853
|
return {
|
|
648
854
|
get: (name) => byName.get(name),
|
|
649
|
-
names: () => [...byName.keys()]
|
|
650
|
-
fingerprint: () => fingerprint
|
|
855
|
+
names: () => [...byName.keys()]
|
|
651
856
|
};
|
|
652
857
|
}
|
|
653
858
|
|
|
654
859
|
// src/assertBlockVersion.ts
|
|
655
860
|
function assertBlockVersion(name, version) {
|
|
656
861
|
if (!Number.isInteger(version) || version < 1) {
|
|
657
|
-
|
|
862
|
+
refuse(NubbinIssueCode.BlockVersion, "version must be an integer of 1 or more", name);
|
|
658
863
|
}
|
|
659
864
|
}
|
|
660
865
|
|
|
@@ -662,7 +867,7 @@ function assertBlockVersion(name, version) {
|
|
|
662
867
|
function assertSlotBounds(name, slots) {
|
|
663
868
|
for (const [slot, { min, max }] of Object.entries(slots)) {
|
|
664
869
|
if (min !== void 0 && max !== void 0 && min > max) {
|
|
665
|
-
|
|
870
|
+
refuse(NubbinIssueCode.SlotBounds, `min ${min} is above max ${max}`, `${name} slots.${slot}`);
|
|
666
871
|
}
|
|
667
872
|
}
|
|
668
873
|
}
|
|
@@ -679,8 +884,10 @@ function resolveHintPaths(blockName, schema, fields) {
|
|
|
679
884
|
const known = new Set(zodAdapter.describe(schema).map((field) => field.path));
|
|
680
885
|
const unresolved = Object.keys(fields).filter((path) => !known.has(path));
|
|
681
886
|
if (unresolved.length > 0) {
|
|
682
|
-
|
|
683
|
-
|
|
887
|
+
refuse(
|
|
888
|
+
NubbinIssueCode.HintPathUnresolvable,
|
|
889
|
+
`ui.fields references ${unresolved.map((p) => `"${p}"`).join(", ")}, which the schema does not define. Known paths: ${[...known].join(", ")}`,
|
|
890
|
+
blockName
|
|
684
891
|
);
|
|
685
892
|
}
|
|
686
893
|
}
|
|
@@ -691,16 +898,20 @@ function assertDataHintAddressable(blockName, fields) {
|
|
|
691
898
|
for (const [path, hint] of Object.entries(fields)) {
|
|
692
899
|
if (hint.data === void 0) continue;
|
|
693
900
|
if (path.includes("[]")) {
|
|
694
|
-
|
|
695
|
-
|
|
901
|
+
refuse(
|
|
902
|
+
NubbinIssueCode.HintNotAddressable,
|
|
903
|
+
`ui.fields["${path}"] sets \`data\`, but a hole cannot address an array member \u2014 "[]" has no single target`,
|
|
904
|
+
blockName
|
|
696
905
|
);
|
|
697
906
|
}
|
|
698
907
|
const nested = seen.find(
|
|
699
908
|
(other) => path.startsWith(`${other}.`) || other.startsWith(`${path}.`)
|
|
700
909
|
);
|
|
701
910
|
if (nested !== void 0) {
|
|
702
|
-
|
|
703
|
-
|
|
911
|
+
refuse(
|
|
912
|
+
NubbinIssueCode.HintNotAddressable,
|
|
913
|
+
`ui.fields["${nested}"] and ui.fields["${path}"] both set \`data\`, but their paths overlap \u2014 two holes over one value have no defined order of application`,
|
|
914
|
+
blockName
|
|
704
915
|
);
|
|
705
916
|
}
|
|
706
917
|
seen.push(path);
|
|
@@ -712,7 +923,11 @@ function assertValidDefaults(blockName, schema, defaults) {
|
|
|
712
923
|
const result = standardValidate(schema, defaults);
|
|
713
924
|
if (result.issues === void 0) return;
|
|
714
925
|
const detail = result.issues.map((issue) => `${formatIssuePath(issue.path)}: ${issue.message}`).join("; ");
|
|
715
|
-
|
|
926
|
+
refuse(
|
|
927
|
+
NubbinIssueCode.InvalidDefaults,
|
|
928
|
+
`defaults do not satisfy the schema \u2014 ${detail}`,
|
|
929
|
+
blockName
|
|
930
|
+
);
|
|
716
931
|
}
|
|
717
932
|
|
|
718
933
|
// src/defineCatalog.ts
|
|
@@ -750,8 +965,40 @@ function formatCompatibilityReport(report) {
|
|
|
750
965
|
return [summary, ...report.incompatible.map(formatRouteIncompatibility)].join("\n");
|
|
751
966
|
}
|
|
752
967
|
|
|
968
|
+
// src/withoutSlotChildren.ts
|
|
969
|
+
function withoutSlotChildren(node, removed) {
|
|
970
|
+
const entries = Object.entries(node.slots ?? {});
|
|
971
|
+
const holdsRemoved = entries.some(([, children]) => children.some((id) => removed.has(id)));
|
|
972
|
+
if (!holdsRemoved) {
|
|
973
|
+
return node;
|
|
974
|
+
}
|
|
975
|
+
const slots = Object.fromEntries(
|
|
976
|
+
entries.map(([slot, children]) => [slot, children.filter((id) => !removed.has(id))])
|
|
977
|
+
);
|
|
978
|
+
return { ...node, slots };
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// src/detachIds.ts
|
|
982
|
+
function detachIds(version, ids) {
|
|
983
|
+
const elements = {};
|
|
984
|
+
for (const [id, node] of Object.entries(version.elements)) {
|
|
985
|
+
elements[id] = withoutSlotChildren(node, ids);
|
|
986
|
+
}
|
|
987
|
+
return { ...version, roots: version.roots.filter((id) => !ids.has(id)), elements };
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// src/moveNode.ts
|
|
991
|
+
function moveNode(version, nodeId, toParentId, toSlot, index) {
|
|
992
|
+
requireNode(version, nodeId);
|
|
993
|
+
requireNode(version, toParentId);
|
|
994
|
+
const detached = detachIds(version, /* @__PURE__ */ new Set([nodeId]));
|
|
995
|
+
const parent = requireNode(detached, toParentId);
|
|
996
|
+
return withElements(detached, withSlotChild(parent, toSlot, nodeId, index));
|
|
997
|
+
}
|
|
998
|
+
|
|
753
999
|
// src/parseMatchKind.ts
|
|
754
1000
|
function parseMatchKind(route) {
|
|
1001
|
+
assertValidRoute(route);
|
|
755
1002
|
if (route.endsWith("/*")) {
|
|
756
1003
|
return "prefix";
|
|
757
1004
|
}
|
|
@@ -761,6 +1008,20 @@ function parseMatchKind(route) {
|
|
|
761
1008
|
return "exact";
|
|
762
1009
|
}
|
|
763
1010
|
|
|
1011
|
+
// src/removeNode.ts
|
|
1012
|
+
function removeNode(version, nodeId) {
|
|
1013
|
+
requireNode(version, nodeId);
|
|
1014
|
+
const removed = idsReachableFrom(version.elements, [nodeId]).add(nodeId);
|
|
1015
|
+
const detached = detachIds(version, removed);
|
|
1016
|
+
const elements = {};
|
|
1017
|
+
for (const [id, node] of Object.entries(detached.elements)) {
|
|
1018
|
+
if (!removed.has(id)) {
|
|
1019
|
+
elements[id] = node;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
return { ...detached, elements };
|
|
1023
|
+
}
|
|
1024
|
+
|
|
764
1025
|
// src/defineStandardSchema.ts
|
|
765
1026
|
var STANDARD_SCHEMA_VERSION = 1;
|
|
766
1027
|
function defineStandardSchema(issuesOf, jsonSchemaOf) {
|
|
@@ -914,21 +1175,26 @@ function setAtPath(target, path, value) {
|
|
|
914
1175
|
return { ...target, [head]: value };
|
|
915
1176
|
}
|
|
916
1177
|
const child = target[head];
|
|
1178
|
+
if (Array.isArray(child)) {
|
|
1179
|
+
refuse(
|
|
1180
|
+
NubbinIssueCode.PathNotAddressable,
|
|
1181
|
+
`path "${path}" descends into an array at "${head}", which addresses no field`,
|
|
1182
|
+
path
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
917
1185
|
const base = typeof child === "object" && child !== null ? child : {};
|
|
918
1186
|
return { ...target, [head]: setAtPath(base, tail.join("."), value) };
|
|
919
1187
|
}
|
|
920
1188
|
|
|
921
1189
|
// src/setNodeProp.ts
|
|
922
1190
|
function setNodeProp(version, nodeId, path, value) {
|
|
923
|
-
const node = version
|
|
924
|
-
|
|
925
|
-
throw new Error(`no node "${nodeId}" in document "${version.documentId}"`);
|
|
926
|
-
}
|
|
927
|
-
const edited = { ...node, props: setAtPath(node.props, path, value) };
|
|
928
|
-
return { ...version, elements: { ...version.elements, [nodeId]: edited } };
|
|
1191
|
+
const node = requireNode(version, nodeId);
|
|
1192
|
+
return withElements(version, { ...node, props: setAtPath(node.props, path, value) });
|
|
929
1193
|
}
|
|
930
1194
|
export {
|
|
931
|
-
|
|
1195
|
+
NubbinError,
|
|
1196
|
+
NubbinIssueCode,
|
|
1197
|
+
addNode,
|
|
932
1198
|
checkCompatibility,
|
|
933
1199
|
checkRollback,
|
|
934
1200
|
compile,
|
|
@@ -936,7 +1202,10 @@ export {
|
|
|
936
1202
|
defineBlock,
|
|
937
1203
|
defineCatalog,
|
|
938
1204
|
formatCompatibilityReport,
|
|
1205
|
+
moveNode,
|
|
939
1206
|
parseMatchKind,
|
|
1207
|
+
refuse,
|
|
1208
|
+
removeNode,
|
|
940
1209
|
richText,
|
|
941
1210
|
setAtPath,
|
|
942
1211
|
setNodeProp,
|
package/package.json
CHANGED