@nubbin/core 0.1.0-rc.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/LICENSE +21 -0
- package/dist/index.d.ts +219 -0
- package/dist/index.js +639 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jesse Wheeler
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
|
|
3
|
+
type UnknownProps = Record<string, unknown>;
|
|
4
|
+
/**
|
|
5
|
+
* A block's props, derived from its schema. The whole of invariant 1 in one type: there is no
|
|
6
|
+
* second definition of a block's shape, so nothing can drift from it.
|
|
7
|
+
*
|
|
8
|
+
* `InferOutput` rather than `InferInput` because a component receives what `validate()`
|
|
9
|
+
* returned — compile freezes the validated value, so a field a transform reshaped reaches the
|
|
10
|
+
* component in its output form, not as the author typed it.
|
|
11
|
+
*/
|
|
12
|
+
type InferProps<Schema extends StandardSchemaV1> = StandardSchemaV1.InferOutput<Schema>;
|
|
13
|
+
interface SlotConstraint {
|
|
14
|
+
/** Block names permitted here. Omitted means any registered block. */
|
|
15
|
+
allow?: readonly string[];
|
|
16
|
+
min?: number;
|
|
17
|
+
max?: number;
|
|
18
|
+
}
|
|
19
|
+
interface Block<Schema extends StandardSchemaV1 = StandardSchemaV1, Component = unknown> {
|
|
20
|
+
/** Stable identity, referenced by every node. Renaming it is a migration. */
|
|
21
|
+
name: string;
|
|
22
|
+
schema: Schema;
|
|
23
|
+
/** Generic so core never imports a rendering library. */
|
|
24
|
+
component: Component;
|
|
25
|
+
/** Bumped when the schema changes incompatibly. */
|
|
26
|
+
version: number;
|
|
27
|
+
/** A deprecated block still resolves; the studio hides it from the palette. */
|
|
28
|
+
status?: "active" | "deprecated";
|
|
29
|
+
slots: Record<string, SlotConstraint>;
|
|
30
|
+
/** Same-node prop reshaping only. It cannot touch slots, or split or delete a block. */
|
|
31
|
+
migrate?: Record<number, (props: UnknownProps) => UnknownProps>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** How a field's value resolves at render. Absent means static — the value freezes into props. */
|
|
35
|
+
type FieldHintData = "request" | {
|
|
36
|
+
revalidate: number;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Studio treatment for one schema path. Open by design: control resolution ranks testers over
|
|
40
|
+
* these hints, so a consumer can carry keys core does not read.
|
|
41
|
+
*/
|
|
42
|
+
interface FieldHint {
|
|
43
|
+
label?: string;
|
|
44
|
+
control?: string;
|
|
45
|
+
data?: FieldHintData;
|
|
46
|
+
}
|
|
47
|
+
interface BlockUi {
|
|
48
|
+
/** Keyed by schema path (`title`, `cta.label`, `items[].icon`). Unresolvable paths fail registration. */
|
|
49
|
+
fields?: Record<string, FieldHint>;
|
|
50
|
+
}
|
|
51
|
+
interface BlockDocs {
|
|
52
|
+
summary?: string;
|
|
53
|
+
usage?: string;
|
|
54
|
+
}
|
|
55
|
+
/** Serializable data only — what the studio and CI read. Components live in the registry. */
|
|
56
|
+
interface CatalogEntry {
|
|
57
|
+
schema: unknown;
|
|
58
|
+
ui?: BlockUi;
|
|
59
|
+
defaults?: UnknownProps;
|
|
60
|
+
docs?: BlockDocs;
|
|
61
|
+
}
|
|
62
|
+
type Catalog = Record<string, CatalogEntry>;
|
|
63
|
+
|
|
64
|
+
interface DocumentMeta {
|
|
65
|
+
title: string;
|
|
66
|
+
description?: string;
|
|
67
|
+
robots?: string;
|
|
68
|
+
canonical?: string;
|
|
69
|
+
}
|
|
70
|
+
/** The authoring shape: children are id references, so every editor operation is by id. */
|
|
71
|
+
interface Node {
|
|
72
|
+
id: string;
|
|
73
|
+
block: string;
|
|
74
|
+
props: UnknownProps;
|
|
75
|
+
/** Slot name → ordered child ids. */
|
|
76
|
+
slots?: Record<string, readonly string[]>;
|
|
77
|
+
}
|
|
78
|
+
interface DocumentVersion {
|
|
79
|
+
documentId: string;
|
|
80
|
+
version: number;
|
|
81
|
+
root: string;
|
|
82
|
+
elements: Record<string, Node>;
|
|
83
|
+
meta: DocumentMeta;
|
|
84
|
+
createdAt: string;
|
|
85
|
+
createdBy: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Field path → how the field resolves at render. */
|
|
89
|
+
type Holes = Record<string, FieldHintData>;
|
|
90
|
+
/** Resolved — no lookups, no dangling references possible. */
|
|
91
|
+
interface ArtifactNode {
|
|
92
|
+
id: string;
|
|
93
|
+
block: string;
|
|
94
|
+
/** Frozen fields only — literal values. */
|
|
95
|
+
props: UnknownProps;
|
|
96
|
+
holes?: Holes;
|
|
97
|
+
slots?: Record<string, ArtifactNode[]>;
|
|
98
|
+
}
|
|
99
|
+
/** The compiled result of one document version. Immutable and content-addressed. */
|
|
100
|
+
interface Artifact {
|
|
101
|
+
/** Content address — the identity. */
|
|
102
|
+
hash: string;
|
|
103
|
+
route: string;
|
|
104
|
+
documentId: string;
|
|
105
|
+
documentVersion: number;
|
|
106
|
+
registryFingerprint: string;
|
|
107
|
+
/** What this was compiled against — only the blocks the document uses. */
|
|
108
|
+
blockVersions: Record<string, number>;
|
|
109
|
+
tree: ArtifactNode[];
|
|
110
|
+
meta: DocumentMeta;
|
|
111
|
+
compiledWith: string;
|
|
112
|
+
}
|
|
113
|
+
/** The only mutable state in the output layer — one independently-writable record per route. */
|
|
114
|
+
interface RoutePointer {
|
|
115
|
+
route: string;
|
|
116
|
+
matchKind: "exact" | "param" | "prefix";
|
|
117
|
+
/** Artifact currently live at this route. */
|
|
118
|
+
hash: string;
|
|
119
|
+
updatedAt: string;
|
|
120
|
+
}
|
|
121
|
+
/** Advisory aggregation over every pointer, for the studio's route list and CI. */
|
|
122
|
+
interface Manifest {
|
|
123
|
+
routes: RoutePointer[];
|
|
124
|
+
generatedAt: string;
|
|
125
|
+
}
|
|
126
|
+
/** The output layer's whole IO surface. Adapters implement it; core only returns values for it. */
|
|
127
|
+
interface ArtifactStore {
|
|
128
|
+
read(hash: string): Promise<Artifact | null>;
|
|
129
|
+
write(artifact: Artifact): Promise<void>;
|
|
130
|
+
manifest(): Promise<Manifest>;
|
|
131
|
+
pointer(route: string): Promise<RoutePointer | null>;
|
|
132
|
+
publish(route: string, hash: string): Promise<void>;
|
|
133
|
+
unpublish(route: string): Promise<void>;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
type CompileIssueCode = "unknown-block" | "dangling-child" | "cycle" | "unreachable" | "slot-not-allowed" | "slot-min" | "slot-max" | "invalid-props";
|
|
137
|
+
interface CompileIssue {
|
|
138
|
+
nodeId: string;
|
|
139
|
+
/** Where in the node the problem sits: `block`, `slots.items`, or a dotted prop path. */
|
|
140
|
+
path: string;
|
|
141
|
+
code: CompileIssueCode;
|
|
142
|
+
message: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Carries every issue found in one pass, so an author fixing six problems sees six. */
|
|
146
|
+
declare class CompileError extends Error {
|
|
147
|
+
readonly issues: readonly CompileIssue[];
|
|
148
|
+
constructor(issues: readonly CompileIssue[]);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface Registry {
|
|
152
|
+
get(name: string): Block | undefined;
|
|
153
|
+
names(): string[];
|
|
154
|
+
/** Hash of every block name and version. Nothing else. */
|
|
155
|
+
fingerprint(): string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
type RollbackCheck = {
|
|
159
|
+
compatible: true;
|
|
160
|
+
} | {
|
|
161
|
+
compatible: false;
|
|
162
|
+
drifted: string[];
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Compares what the artifact was compiled against with the registry live now. A name the
|
|
167
|
+
* registry no longer holds is drift, not an absent check — a deleted block is exactly the
|
|
168
|
+
* failure a rollback must be warned about.
|
|
169
|
+
*/
|
|
170
|
+
declare function checkRollback(artifact: Artifact, registry: Registry): RollbackCheck;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Orchestration only. Structure first, and stop there if it failed — prop validation on a
|
|
174
|
+
* document with dangling references produces cascading noise that buries the real cause.
|
|
175
|
+
*/
|
|
176
|
+
declare function compile(version: DocumentVersion, catalog: Catalog, registry: Registry, route: string): Artifact;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Sorted by name so registration order cannot change the fingerprint, and built from name and
|
|
180
|
+
* version alone so unrelated edits — a slot constraint, a deprecation — do not invalidate every
|
|
181
|
+
* artifact compiled before them.
|
|
182
|
+
*/
|
|
183
|
+
declare function createRegistry(blocks: readonly Block[]): Registry;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Identity at runtime; its job is to fix the generic parameters at the call site so props are
|
|
187
|
+
* inferred from the schema rather than declared beside it. The checks here are the ones the
|
|
188
|
+
* type system cannot make — a slot that no composition could satisfy, or a migration keyed to
|
|
189
|
+
* a version this block never reaches.
|
|
190
|
+
*/
|
|
191
|
+
declare function defineBlock<Schema extends StandardSchemaV1, Component>(block: Block<Schema, Component>): Block<Schema, Component>;
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* The serializable half of the catalog/registry split: schema, ui, defaults, docs — no
|
|
195
|
+
* components. Everything checkable at registration is checked here, because a bad hint or
|
|
196
|
+
* bad defaults are silent at every later point.
|
|
197
|
+
*/
|
|
198
|
+
declare function defineCatalog(entries: Record<string, CatalogEntry>): Catalog;
|
|
199
|
+
|
|
200
|
+
type FieldKind = "string" | "number" | "boolean" | "enum" | "array" | "object" | "union" | "unknown";
|
|
201
|
+
interface FieldNode {
|
|
202
|
+
/** Dotted path from the schema root, with `[]` for array members: `cta.label`, `items[].title`. */
|
|
203
|
+
path: string;
|
|
204
|
+
kind: FieldKind;
|
|
205
|
+
optional: boolean;
|
|
206
|
+
/** Present only for `enum`. */
|
|
207
|
+
members?: readonly string[];
|
|
208
|
+
}
|
|
209
|
+
interface SchemaAdapter {
|
|
210
|
+
describe(schema: unknown): FieldNode[];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* matchKind is parsed from the route at publish, never caller-supplied. It lives in core so
|
|
215
|
+
* every adapter derives it from one implementation — a second parser is free to disagree.
|
|
216
|
+
*/
|
|
217
|
+
declare function parseMatchKind(route: string): RoutePointer["matchKind"];
|
|
218
|
+
|
|
219
|
+
export { type Artifact, type ArtifactNode, type ArtifactStore, type Block, type BlockDocs, type BlockUi, type Catalog, type CatalogEntry, CompileError, type CompileIssue, type CompileIssueCode, type DocumentMeta, type DocumentVersion, type FieldHint, type FieldHintData, type FieldKind, type FieldNode, type Holes, type InferProps, type Manifest, type Node, type Registry, type RollbackCheck, type RoutePointer, type SchemaAdapter, type SlotConstraint, type UnknownProps, checkRollback, compile, createRegistry, defineBlock, defineCatalog, parseMatchKind };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
// src/CompileError.ts
|
|
2
|
+
var CompileError = class extends Error {
|
|
3
|
+
issues;
|
|
4
|
+
constructor(issues) {
|
|
5
|
+
const summary = issues.map((issue) => `${issue.nodeId} ${issue.path} [${issue.code}]: ${issue.message}`).join("\n");
|
|
6
|
+
super(`Compile failed with ${issues.length} issue(s):
|
|
7
|
+
${summary}`);
|
|
8
|
+
this.name = "CompileError";
|
|
9
|
+
this.issues = issues;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// src/checkRollback.ts
|
|
14
|
+
function checkRollback(artifact, registry) {
|
|
15
|
+
const drifted = Object.entries(artifact.blockVersions).filter(([name, version]) => registry.get(name)?.version !== version).map(([name]) => name);
|
|
16
|
+
return drifted.length === 0 ? { compatible: true } : { compatible: false, drifted };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/artifactNodeOf.ts
|
|
20
|
+
function artifactNodeOf(node, resolve) {
|
|
21
|
+
const { props, holes } = resolve(node);
|
|
22
|
+
const artifactNode = { id: node.id, block: node.block, props };
|
|
23
|
+
if (Object.keys(holes).length > 0) {
|
|
24
|
+
artifactNode.holes = holes;
|
|
25
|
+
}
|
|
26
|
+
return artifactNode;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/slotEdges.ts
|
|
30
|
+
function slotEdges(node) {
|
|
31
|
+
const edges = [];
|
|
32
|
+
for (const [slot, children] of Object.entries(node.slots ?? {})) {
|
|
33
|
+
for (const childId of children) {
|
|
34
|
+
edges.push({ slot, childId });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return edges;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/wireSlots.ts
|
|
41
|
+
function wireSlots(version, built) {
|
|
42
|
+
for (const [id, artifactNode] of built) {
|
|
43
|
+
const source = version.elements[id];
|
|
44
|
+
if (source?.slots === void 0) continue;
|
|
45
|
+
const slots = {};
|
|
46
|
+
for (const [slotName, childIds] of Object.entries(source.slots)) {
|
|
47
|
+
slots[slotName] = childIds.flatMap((childId) => {
|
|
48
|
+
const child = built.get(childId);
|
|
49
|
+
return child === void 0 ? [] : [child];
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
artifactNode.slots = slots;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/denormalize.ts
|
|
57
|
+
function denormalize(version, resolve) {
|
|
58
|
+
const pending = [version.root];
|
|
59
|
+
const built = /* @__PURE__ */ new Map();
|
|
60
|
+
while (pending.length > 0) {
|
|
61
|
+
const id = pending.pop();
|
|
62
|
+
if (id === void 0) break;
|
|
63
|
+
const node = version.elements[id];
|
|
64
|
+
if (built.has(id) || node === void 0) continue;
|
|
65
|
+
built.set(id, artifactNodeOf(node, resolve));
|
|
66
|
+
for (const edge of slotEdges(node)) {
|
|
67
|
+
pending.push(edge.childId);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
wireSlots(version, built);
|
|
71
|
+
const root = built.get(version.root);
|
|
72
|
+
return root === void 0 ? [] : [root];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/fnv1a.ts
|
|
76
|
+
var OFFSET_BASIS = 2166136261;
|
|
77
|
+
var PRIME = 16777619;
|
|
78
|
+
var HEX_RADIX = 16;
|
|
79
|
+
var HEX_WIDTH = 8;
|
|
80
|
+
function fnv1a(input) {
|
|
81
|
+
let hash = OFFSET_BASIS;
|
|
82
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
83
|
+
hash ^= input.charCodeAt(index);
|
|
84
|
+
hash = Math.imul(hash, PRIME);
|
|
85
|
+
}
|
|
86
|
+
return (hash >>> 0).toString(HEX_RADIX).padStart(HEX_WIDTH, "0");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/hashArtifact.ts
|
|
90
|
+
function hashArtifact(artifact) {
|
|
91
|
+
const sortKeys = (_key, value) => {
|
|
92
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return value;
|
|
93
|
+
const entries = Object.entries(value).sort(([left], [right]) => left < right ? -1 : 1);
|
|
94
|
+
return Object.fromEntries(entries);
|
|
95
|
+
};
|
|
96
|
+
return fnv1a(JSON.stringify(artifact, sortKeys));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/partitionProps.ts
|
|
100
|
+
function partitionProps(validated, hints) {
|
|
101
|
+
const props = {};
|
|
102
|
+
const holes = {};
|
|
103
|
+
const fields = hints?.fields ?? {};
|
|
104
|
+
for (const [key, value] of Object.entries(validated)) {
|
|
105
|
+
const data = fields[key]?.data;
|
|
106
|
+
if (data === void 0) {
|
|
107
|
+
props[key] = value;
|
|
108
|
+
} else {
|
|
109
|
+
holes[key] = data;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { props, holes };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/formatIssuePath.ts
|
|
116
|
+
function formatIssuePath(path) {
|
|
117
|
+
if (path === void 0) return "";
|
|
118
|
+
return path.map(
|
|
119
|
+
(segment) => typeof segment === "object" && segment !== null ? String(segment.key) : String(segment)
|
|
120
|
+
).join(".");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/isUnknownProps.ts
|
|
124
|
+
function isUnknownProps(value) {
|
|
125
|
+
if (typeof value !== "object") return false;
|
|
126
|
+
return value !== null && !Array.isArray(value);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/isStandardSchema.ts
|
|
130
|
+
function isStandardSchema(value) {
|
|
131
|
+
if (typeof value !== "object" || value === null) return false;
|
|
132
|
+
if (!("~standard" in value)) return false;
|
|
133
|
+
const contract = value["~standard"];
|
|
134
|
+
if (typeof contract !== "object" || contract === null) return false;
|
|
135
|
+
return "validate" in contract && typeof contract.validate === "function";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/standardValidate.ts
|
|
139
|
+
function standardValidate(schema, value) {
|
|
140
|
+
if (!isStandardSchema(schema)) {
|
|
141
|
+
throw new Error("Schema does not implement Standard Schema (`~standard.validate`)");
|
|
142
|
+
}
|
|
143
|
+
const result = schema["~standard"].validate(value);
|
|
144
|
+
if (result instanceof Promise) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
"Schema validates asynchronously; compile and registration require synchronous validation"
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/validateNodeProps.ts
|
|
153
|
+
function validateNodeProps(node, schema) {
|
|
154
|
+
const result = standardValidate(schema, node.props);
|
|
155
|
+
if (result.issues !== void 0) {
|
|
156
|
+
const issues = result.issues.map((issue) => ({
|
|
157
|
+
nodeId: node.id,
|
|
158
|
+
path: formatIssuePath(issue.path),
|
|
159
|
+
code: "invalid-props",
|
|
160
|
+
message: issue.message
|
|
161
|
+
}));
|
|
162
|
+
return { issues };
|
|
163
|
+
}
|
|
164
|
+
if (!isUnknownProps(result.value)) {
|
|
165
|
+
return {
|
|
166
|
+
issues: [
|
|
167
|
+
{
|
|
168
|
+
nodeId: node.id,
|
|
169
|
+
path: "",
|
|
170
|
+
code: "invalid-props",
|
|
171
|
+
message: "block props must parse to an object"
|
|
172
|
+
}
|
|
173
|
+
]
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return { value: result.value, issues: [] };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/resolveAllProps.ts
|
|
180
|
+
function resolveAllProps(version, catalog) {
|
|
181
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
182
|
+
const issues = [];
|
|
183
|
+
for (const node of Object.values(version.elements)) {
|
|
184
|
+
const entry = catalog[node.block];
|
|
185
|
+
if (entry === void 0) {
|
|
186
|
+
const message = `"${node.block}" has no catalog entry, so its props cannot be validated`;
|
|
187
|
+
issues.push({ code: "unknown-block", message, nodeId: node.id, path: "block" });
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const { value, issues: propIssues } = validateNodeProps(node, entry.schema);
|
|
191
|
+
issues.push(...propIssues);
|
|
192
|
+
if (value !== void 0) {
|
|
193
|
+
resolved.set(node.id, partitionProps(value, entry.ui));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return { resolved, issues };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/usedBlockVersions.ts
|
|
200
|
+
function usedBlockVersions(version, registry) {
|
|
201
|
+
const versions = {};
|
|
202
|
+
for (const node of Object.values(version.elements)) {
|
|
203
|
+
const block = registry.get(node.block);
|
|
204
|
+
if (block !== void 0) {
|
|
205
|
+
versions[node.block] = block.version;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return versions;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/pushCycleFrame.ts
|
|
212
|
+
function pushCycleFrame(stack, state, version, id) {
|
|
213
|
+
const node = version.elements[id];
|
|
214
|
+
if (node === void 0) return;
|
|
215
|
+
state.set(id, "visiting");
|
|
216
|
+
stack.push({ id, edges: slotEdges(node), next: 0 });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/findCycles.ts
|
|
220
|
+
function findCycles(version) {
|
|
221
|
+
const state = /* @__PURE__ */ new Map();
|
|
222
|
+
const stack = [];
|
|
223
|
+
const issues = [];
|
|
224
|
+
pushCycleFrame(stack, state, version, version.root);
|
|
225
|
+
while (stack.length > 0) {
|
|
226
|
+
const frame = stack.at(-1);
|
|
227
|
+
if (frame === void 0) break;
|
|
228
|
+
const edge = frame.edges[frame.next];
|
|
229
|
+
if (edge === void 0) {
|
|
230
|
+
state.set(frame.id, "done");
|
|
231
|
+
stack.pop();
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
frame.next += 1;
|
|
235
|
+
if (state.get(edge.childId) === "visiting") {
|
|
236
|
+
issues.push({
|
|
237
|
+
nodeId: frame.id,
|
|
238
|
+
path: `slots.${edge.slot}`,
|
|
239
|
+
code: "cycle",
|
|
240
|
+
message: `"${frame.id}" reaches back to "${edge.childId}", so the graph cannot flatten into a tree`
|
|
241
|
+
});
|
|
242
|
+
} else if (!state.has(edge.childId)) {
|
|
243
|
+
pushCycleFrame(stack, state, version, edge.childId);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return issues;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/findDanglingChildren.ts
|
|
250
|
+
function findDanglingChildren(version) {
|
|
251
|
+
const issues = [];
|
|
252
|
+
if (version.elements[version.root] === void 0) {
|
|
253
|
+
issues.push({
|
|
254
|
+
nodeId: version.root,
|
|
255
|
+
path: "root",
|
|
256
|
+
code: "dangling-child",
|
|
257
|
+
message: `root "${version.root}" has no matching element`
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
for (const node of Object.values(version.elements)) {
|
|
261
|
+
for (const edge of slotEdges(node)) {
|
|
262
|
+
if (version.elements[edge.childId] === void 0) {
|
|
263
|
+
issues.push({
|
|
264
|
+
nodeId: node.id,
|
|
265
|
+
path: `slots.${edge.slot}`,
|
|
266
|
+
code: "dangling-child",
|
|
267
|
+
message: `child "${edge.childId}" has no matching element`
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return issues;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/disallowedChildren.ts
|
|
276
|
+
function disallowedChildren(parent, path, childIds, allow, version) {
|
|
277
|
+
if (allow === void 0) return [];
|
|
278
|
+
const issues = [];
|
|
279
|
+
for (const childId of childIds) {
|
|
280
|
+
const child = version.elements[childId];
|
|
281
|
+
if (child === void 0 || allow.includes(child.block)) continue;
|
|
282
|
+
issues.push({
|
|
283
|
+
nodeId: childId,
|
|
284
|
+
path,
|
|
285
|
+
code: "slot-not-allowed",
|
|
286
|
+
message: `"${child.block}" is not allowed in ${path} of "${parent.block}"; allowed: ${allow.join(", ")}`
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
return issues;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/slotBoundIssues.ts
|
|
293
|
+
function slotBoundIssues(parentId, path, count, constraint) {
|
|
294
|
+
const { min, max } = constraint;
|
|
295
|
+
const bounds = [
|
|
296
|
+
{
|
|
297
|
+
code: "slot-min",
|
|
298
|
+
limit: min,
|
|
299
|
+
breached: min !== void 0 && count < min,
|
|
300
|
+
sense: "at least"
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
code: "slot-max",
|
|
304
|
+
limit: max,
|
|
305
|
+
breached: max !== void 0 && count > max,
|
|
306
|
+
sense: "at most"
|
|
307
|
+
}
|
|
308
|
+
];
|
|
309
|
+
return bounds.filter((bound) => bound.breached).map((bound) => ({
|
|
310
|
+
nodeId: parentId,
|
|
311
|
+
path,
|
|
312
|
+
code: bound.code,
|
|
313
|
+
message: `${path} holds ${count} of ${bound.sense} ${bound.limit}`
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// src/slotIssuesAt.ts
|
|
318
|
+
function slotIssuesAt(parent, slotName, childIds, constraint, version) {
|
|
319
|
+
const path = `slots.${slotName}`;
|
|
320
|
+
if (constraint === void 0) {
|
|
321
|
+
return [
|
|
322
|
+
{
|
|
323
|
+
nodeId: parent.id,
|
|
324
|
+
path,
|
|
325
|
+
code: "slot-not-allowed",
|
|
326
|
+
message: `"${parent.block}" declares no slot "${slotName}"`
|
|
327
|
+
}
|
|
328
|
+
];
|
|
329
|
+
}
|
|
330
|
+
return [
|
|
331
|
+
...slotBoundIssues(parent.id, path, childIds.length, constraint),
|
|
332
|
+
...disallowedChildren(parent, path, childIds, constraint.allow, version)
|
|
333
|
+
];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// src/findSlotViolations.ts
|
|
337
|
+
function findSlotViolations(version, registry) {
|
|
338
|
+
return Object.values(version.elements).flatMap((node) => {
|
|
339
|
+
const block = registry.get(node.block);
|
|
340
|
+
if (block === void 0) return [];
|
|
341
|
+
const filled = node.slots ?? {};
|
|
342
|
+
const names = /* @__PURE__ */ new Set([...Object.keys(block.slots), ...Object.keys(filled)]);
|
|
343
|
+
return [...names].flatMap(
|
|
344
|
+
(slotName) => slotIssuesAt(node, slotName, filled[slotName] ?? [], block.slots[slotName], version)
|
|
345
|
+
);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/findUnknownBlocks.ts
|
|
350
|
+
function findUnknownBlocks(version, registry) {
|
|
351
|
+
return Object.values(version.elements).filter((node) => registry.get(node.block) === void 0).map((node) => ({
|
|
352
|
+
nodeId: node.id,
|
|
353
|
+
path: "block",
|
|
354
|
+
code: "unknown-block",
|
|
355
|
+
message: `"${node.block}" is not a registered block`
|
|
356
|
+
}));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// src/reachableIds.ts
|
|
360
|
+
function reachableIds(version) {
|
|
361
|
+
const seen = /* @__PURE__ */ new Set([version.root]);
|
|
362
|
+
const queue = [version.root];
|
|
363
|
+
while (queue.length > 0) {
|
|
364
|
+
const id = queue.pop();
|
|
365
|
+
const node = id === void 0 ? void 0 : version.elements[id];
|
|
366
|
+
if (node === void 0) continue;
|
|
367
|
+
for (const edge of slotEdges(node)) {
|
|
368
|
+
if (seen.has(edge.childId)) continue;
|
|
369
|
+
seen.add(edge.childId);
|
|
370
|
+
queue.push(edge.childId);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return seen;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/findUnreachable.ts
|
|
377
|
+
function findUnreachable(version) {
|
|
378
|
+
const reached = reachableIds(version);
|
|
379
|
+
const issues = [];
|
|
380
|
+
for (const node of Object.values(version.elements)) {
|
|
381
|
+
if (reached.has(node.id)) continue;
|
|
382
|
+
issues.push({
|
|
383
|
+
nodeId: node.id,
|
|
384
|
+
path: "",
|
|
385
|
+
code: "unreachable",
|
|
386
|
+
message: `no slot reaches "${node.id}" from the root`
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
return issues;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// src/validateStructure.ts
|
|
393
|
+
function validateStructure(version, registry) {
|
|
394
|
+
return [
|
|
395
|
+
...findUnknownBlocks(version, registry),
|
|
396
|
+
...findDanglingChildren(version),
|
|
397
|
+
...findCycles(version),
|
|
398
|
+
...findUnreachable(version),
|
|
399
|
+
...findSlotViolations(version, registry)
|
|
400
|
+
];
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// src/version.constants.ts
|
|
404
|
+
var NUBBIN_VERSION = "0.0.0";
|
|
405
|
+
|
|
406
|
+
// src/compile.ts
|
|
407
|
+
function compile(version, catalog, registry, route) {
|
|
408
|
+
const structural = validateStructure(version, registry);
|
|
409
|
+
if (structural.length > 0) throw new CompileError(structural);
|
|
410
|
+
const { resolved, issues } = resolveAllProps(version, catalog);
|
|
411
|
+
if (issues.length > 0) throw new CompileError(issues);
|
|
412
|
+
const tree = denormalize(version, (node) => resolved.get(node.id) ?? { props: {}, holes: {} });
|
|
413
|
+
const content = {
|
|
414
|
+
route,
|
|
415
|
+
documentId: version.documentId,
|
|
416
|
+
documentVersion: version.version,
|
|
417
|
+
registryFingerprint: registry.fingerprint(),
|
|
418
|
+
blockVersions: usedBlockVersions(version, registry),
|
|
419
|
+
tree,
|
|
420
|
+
meta: version.meta,
|
|
421
|
+
compiledWith: NUBBIN_VERSION
|
|
422
|
+
};
|
|
423
|
+
return { ...content, hash: hashArtifact(content) };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// src/createRegistry.ts
|
|
427
|
+
function createRegistry(blocks) {
|
|
428
|
+
const byName = /* @__PURE__ */ new Map();
|
|
429
|
+
for (const block of blocks) {
|
|
430
|
+
if (byName.has(block.name)) {
|
|
431
|
+
throw new Error(
|
|
432
|
+
`Duplicate block name "${block.name}" \u2014 names are the identity nodes resolve through`
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
byName.set(block.name, block);
|
|
436
|
+
}
|
|
437
|
+
const signature = [...byName.values()].map((block) => `${block.name}@${block.version}`).sort().join("\n");
|
|
438
|
+
const fingerprint = fnv1a(signature);
|
|
439
|
+
return {
|
|
440
|
+
get: (name) => byName.get(name),
|
|
441
|
+
names: () => [...byName.keys()],
|
|
442
|
+
fingerprint: () => fingerprint
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/assertBlockVersion.ts
|
|
447
|
+
function assertBlockVersion(name, version) {
|
|
448
|
+
if (!Number.isInteger(version) || version < 1) {
|
|
449
|
+
throw new Error(`${name}: version must be an integer of 1 or more`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// src/assertMigrateKeys.ts
|
|
454
|
+
var FIRST_MIGRATABLE_VERSION = 2;
|
|
455
|
+
function assertMigrateKeys(name, version, migrate) {
|
|
456
|
+
for (const key of Object.keys(migrate ?? {})) {
|
|
457
|
+
const target = Number(key);
|
|
458
|
+
if (target < FIRST_MIGRATABLE_VERSION || target > version) {
|
|
459
|
+
throw new Error(`${name}: migrate key ${key} is outside the reachable range 2..${version}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// src/assertSlotBounds.ts
|
|
465
|
+
function assertSlotBounds(name, slots) {
|
|
466
|
+
for (const [slot, { min, max }] of Object.entries(slots)) {
|
|
467
|
+
if (min !== void 0 && max !== void 0 && min > max) {
|
|
468
|
+
throw new Error(`${name}: slot "${slot}" has min ${min} above max ${max}`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/defineBlock.ts
|
|
474
|
+
function defineBlock(block) {
|
|
475
|
+
assertBlockVersion(block.name, block.version);
|
|
476
|
+
assertSlotBounds(block.name, block.slots);
|
|
477
|
+
assertMigrateKeys(block.name, block.version, block.migrate);
|
|
478
|
+
return block;
|
|
479
|
+
}
|
|
480
|
+
|
|
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
|
+
// src/adapters/resolveHintPaths.ts
|
|
590
|
+
function resolveHintPaths(blockName, schema, fields) {
|
|
591
|
+
const known = new Set(zodAdapter.describe(schema).map((field) => field.path));
|
|
592
|
+
const unresolved = Object.keys(fields).filter((path) => !known.has(path));
|
|
593
|
+
if (unresolved.length > 0) {
|
|
594
|
+
throw new Error(
|
|
595
|
+
`${blockName}: ui.fields references ${unresolved.map((p) => `"${p}"`).join(", ")}, which the schema does not define. Known paths: ${[...known].join(", ")}`
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// src/assertValidDefaults.ts
|
|
601
|
+
function assertValidDefaults(blockName, schema, defaults) {
|
|
602
|
+
const result = standardValidate(schema, defaults);
|
|
603
|
+
if (result.issues === void 0) return;
|
|
604
|
+
const detail = result.issues.map((issue) => `${formatIssuePath(issue.path)}: ${issue.message}`).join("; ");
|
|
605
|
+
throw new Error(`${blockName}: defaults do not satisfy the schema \u2014 ${detail}`);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// src/defineCatalog.ts
|
|
609
|
+
function defineCatalog(entries) {
|
|
610
|
+
for (const [blockName, entry] of Object.entries(entries)) {
|
|
611
|
+
if (entry.ui?.fields !== void 0) {
|
|
612
|
+
resolveHintPaths(blockName, entry.schema, entry.ui.fields);
|
|
613
|
+
}
|
|
614
|
+
if (entry.defaults !== void 0) {
|
|
615
|
+
assertValidDefaults(blockName, entry.schema, entry.defaults);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return entries;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// src/parseMatchKind.ts
|
|
622
|
+
function parseMatchKind(route) {
|
|
623
|
+
if (route.endsWith("/*")) {
|
|
624
|
+
return "prefix";
|
|
625
|
+
}
|
|
626
|
+
if (/\[[^/]+\]/.test(route)) {
|
|
627
|
+
return "param";
|
|
628
|
+
}
|
|
629
|
+
return "exact";
|
|
630
|
+
}
|
|
631
|
+
export {
|
|
632
|
+
CompileError,
|
|
633
|
+
checkRollback,
|
|
634
|
+
compile,
|
|
635
|
+
createRegistry,
|
|
636
|
+
defineBlock,
|
|
637
|
+
defineCatalog,
|
|
638
|
+
parseMatchKind
|
|
639
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nubbin/core",
|
|
3
|
+
"version": "0.1.0-rc.0",
|
|
4
|
+
"description": "The Nubbin contract: define blocks, split catalog from registry, and compile a page document into an immutable artifact.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"nubbin",
|
|
7
|
+
"page-builder",
|
|
8
|
+
"cms",
|
|
9
|
+
"standard-schema",
|
|
10
|
+
"content"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/effekt/nubbin.git",
|
|
16
|
+
"directory": "packages/core"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://effekt.github.io/nubbin/",
|
|
19
|
+
"bugs": "https://github.com/effekt/nubbin/issues",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/index.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist"
|
|
30
|
+
],
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@standard-schema/spec": "1.1.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"tsup": "8.5.1",
|
|
39
|
+
"typescript": "6.0.3",
|
|
40
|
+
"vitest": "4.1.10",
|
|
41
|
+
"zod": "4.4.3"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"typecheck": "tsc --noEmit"
|
|
47
|
+
}
|
|
48
|
+
}
|