@elytracms/next 0.0.1
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/client.d.ts +1 -0
- package/dist/client.js +47 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +2616 -0
- package/dist/index.js +4484 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4484 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export { z } from 'zod';
|
|
3
|
+
import { createContext, createElement, Fragment as Fragment$1, Component, isValidElement } from 'react';
|
|
4
|
+
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
|
|
5
|
+
import { revalidateTag, unstable_cache } from 'next/cache';
|
|
6
|
+
import { draftMode } from 'next/headers';
|
|
7
|
+
import { redirect, notFound, permanentRedirect } from 'next/navigation';
|
|
8
|
+
import { createHmac, timingSafeEqual } from 'crypto';
|
|
9
|
+
import NextImage from 'next/image';
|
|
10
|
+
|
|
11
|
+
// ../cms-core/src/issues.ts
|
|
12
|
+
var cmsIssueCodeSchema = z.enum([
|
|
13
|
+
// collections & fields (EC-016)
|
|
14
|
+
"duplicate-collection",
|
|
15
|
+
"duplicate-field",
|
|
16
|
+
"unknown-collection",
|
|
17
|
+
"invalid-collection-config",
|
|
18
|
+
"invalid-field-config",
|
|
19
|
+
"missing-required-field",
|
|
20
|
+
"invalid-field-value",
|
|
21
|
+
// relations & assets (EC-017)
|
|
22
|
+
"unknown-relation-target",
|
|
23
|
+
// broken published references after a weak unpublish (EC-173)
|
|
24
|
+
"unpublished-relation-target",
|
|
25
|
+
"cardinality-violation",
|
|
26
|
+
"unknown-asset",
|
|
27
|
+
"duplicate-document",
|
|
28
|
+
// localization (EC-018)
|
|
29
|
+
"unknown-locale",
|
|
30
|
+
"missing-localized-value",
|
|
31
|
+
// routes / redirects / resolution (EC-019, EC-020)
|
|
32
|
+
"route-conflict",
|
|
33
|
+
"redirect-loop",
|
|
34
|
+
"unknown-route-target",
|
|
35
|
+
// page hierarchy (EC-218): a parent chain that loops
|
|
36
|
+
"hierarchy-cycle",
|
|
37
|
+
// versions (EC-021)
|
|
38
|
+
"unknown-version"
|
|
39
|
+
]);
|
|
40
|
+
var cmsIssueSeveritySchema = z.enum(["error", "warning"]);
|
|
41
|
+
z.object({
|
|
42
|
+
code: cmsIssueCodeSchema,
|
|
43
|
+
severity: cmsIssueSeveritySchema,
|
|
44
|
+
message: z.string(),
|
|
45
|
+
/** Path into the CMS data, e.g. ['collections', 'post', 'fields', 'author']. */
|
|
46
|
+
path: z.array(z.union([z.string(), z.number()])),
|
|
47
|
+
/** The offending collection id, when applicable. */
|
|
48
|
+
collectionId: z.string().optional(),
|
|
49
|
+
/** The offending document id, when applicable. */
|
|
50
|
+
documentId: z.string().optional(),
|
|
51
|
+
meta: z.record(z.string(), z.unknown()).optional()
|
|
52
|
+
});
|
|
53
|
+
function cmsIssue(init) {
|
|
54
|
+
return {
|
|
55
|
+
code: init.code,
|
|
56
|
+
severity: init.severity ?? "error",
|
|
57
|
+
message: init.message,
|
|
58
|
+
path: [...init.path],
|
|
59
|
+
...init.collectionId ? { collectionId: init.collectionId } : {},
|
|
60
|
+
...init.documentId ? { documentId: init.documentId } : {},
|
|
61
|
+
...init.meta ? { meta: init.meta } : {}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
z.enum([
|
|
65
|
+
"text",
|
|
66
|
+
"number",
|
|
67
|
+
"boolean",
|
|
68
|
+
"date",
|
|
69
|
+
"select",
|
|
70
|
+
"richText",
|
|
71
|
+
"relation",
|
|
72
|
+
"asset",
|
|
73
|
+
// Composition field (EC-186, AD-12): the value is a constrained tree of
|
|
74
|
+
// registry components (a canvas), not a scalar. The vocabulary is code.
|
|
75
|
+
"blocks",
|
|
76
|
+
// Object / repeater field (EC-253): a nested group of field-defs — a single
|
|
77
|
+
// object (`cardinality: 'one'`) or an ordered array of them (`'many'`). The
|
|
78
|
+
// missing array-of-objects primitive real sites lean on (menus, forms, galleries).
|
|
79
|
+
"object"
|
|
80
|
+
]);
|
|
81
|
+
var cardinalitySchema = z.enum(["one", "many"]);
|
|
82
|
+
var fieldContextSchema = z.enum(["document", "prop", "both"]);
|
|
83
|
+
var fieldControlSchema = z.enum(["input", "textarea", "color", "json"]);
|
|
84
|
+
var fieldFormMetaSchema = z.object({
|
|
85
|
+
/** Human label shown in the editor; falls back to the field name when omitted. */
|
|
86
|
+
label: z.string().optional(),
|
|
87
|
+
/** Helper/description text shown under the field. */
|
|
88
|
+
description: z.string().optional(),
|
|
89
|
+
/** Placeholder for text-like inputs. */
|
|
90
|
+
placeholder: z.string().optional(),
|
|
91
|
+
/** Grouping section the field belongs to (a fieldset). */
|
|
92
|
+
group: z.string().optional(),
|
|
93
|
+
/** Tab the field belongs to (for tabbed editors). */
|
|
94
|
+
tab: z.string().optional(),
|
|
95
|
+
/** Sort order within its group/tab; lower comes first. */
|
|
96
|
+
order: z.number().optional(),
|
|
97
|
+
/** When true the editor renders the field read-only. */
|
|
98
|
+
readOnly: z.boolean().optional(),
|
|
99
|
+
/** When true the editor hides the field (still validated/stored). */
|
|
100
|
+
hidden: z.boolean().optional(),
|
|
101
|
+
/** Preferred input control when the field type maps to several (EC-190). */
|
|
102
|
+
control: fieldControlSchema.optional(),
|
|
103
|
+
/**
|
|
104
|
+
* When true the editor edits this value inline on the canvas (EC-158) rather
|
|
105
|
+
* than as a form row — used for text-like component props rendered as visible
|
|
106
|
+
* content. The single home for inline-editing across documents and props.
|
|
107
|
+
*/
|
|
108
|
+
inlineEditable: z.boolean().optional()
|
|
109
|
+
});
|
|
110
|
+
var fieldValidationSchema = z.object({
|
|
111
|
+
/** When true a value must be present (and non-empty for strings/arrays). */
|
|
112
|
+
required: z.boolean().optional(),
|
|
113
|
+
/** Minimum string length / array length / numeric value (type-dependent). */
|
|
114
|
+
min: z.number().optional(),
|
|
115
|
+
/** Maximum string length / array length / numeric value (type-dependent). */
|
|
116
|
+
max: z.number().optional(),
|
|
117
|
+
/** RegExp source string a text value must match. */
|
|
118
|
+
pattern: z.string().optional(),
|
|
119
|
+
/** Value must be unique across the collection's documents. */
|
|
120
|
+
unique: z.boolean().optional()
|
|
121
|
+
});
|
|
122
|
+
var selectOptionSchema = z.object({
|
|
123
|
+
value: z.string(),
|
|
124
|
+
label: z.string().optional()
|
|
125
|
+
});
|
|
126
|
+
var baseFieldShape = {
|
|
127
|
+
/** Stable field name (the key in a document's values; survives label changes). */
|
|
128
|
+
name: z.string().min(1),
|
|
129
|
+
/** When true the field stores a separate value per locale (EC-018). */
|
|
130
|
+
localized: z.boolean().optional(),
|
|
131
|
+
/**
|
|
132
|
+
* Marks the field as filterable/sortable for the closed-form list accessors
|
|
133
|
+
* (EC-143, vision AD-3). `listDocuments` filters and sorts are permitted only
|
|
134
|
+
* on declared fields — this keeps backend indexes honest and stops filter
|
|
135
|
+
* vocabulary creep. Never affects stored values or validation.
|
|
136
|
+
*/
|
|
137
|
+
filterable: z.boolean().optional(),
|
|
138
|
+
/**
|
|
139
|
+
* Where this field-def is valid (EC-190): a CMS document field, a component
|
|
140
|
+
* prop, or both. Defaults to `document` when omitted (back-compat — every
|
|
141
|
+
* existing collection field-def stays a document field). Document-only
|
|
142
|
+
* attributes (`filterable`, `validation.unique`) declared on a `context:'prop'`
|
|
143
|
+
* field are a structured issue (`assertPropFieldDef`), never silent.
|
|
144
|
+
*/
|
|
145
|
+
context: fieldContextSchema.optional(),
|
|
146
|
+
/**
|
|
147
|
+
* Default value applied when none is set (EC-190). On a component prop this is
|
|
148
|
+
* the renderer's static fallback (EC-015); on a document field it seeds new
|
|
149
|
+
* documents — the per-field starter-content mechanism (no template entity
|
|
150
|
+
* needed). Never affects whether a value is *stored*, only what fills a gap.
|
|
151
|
+
*/
|
|
152
|
+
default: z.unknown().optional(),
|
|
153
|
+
form: fieldFormMetaSchema.optional(),
|
|
154
|
+
validation: fieldValidationSchema.optional()
|
|
155
|
+
};
|
|
156
|
+
z.object(baseFieldShape);
|
|
157
|
+
var fieldDefSchema = z.discriminatedUnion("type", [
|
|
158
|
+
z.object({ ...baseFieldShape, type: z.literal("text") }),
|
|
159
|
+
z.object({ ...baseFieldShape, type: z.literal("number") }),
|
|
160
|
+
z.object({ ...baseFieldShape, type: z.literal("boolean") }),
|
|
161
|
+
z.object({ ...baseFieldShape, type: z.literal("date") }),
|
|
162
|
+
z.object({
|
|
163
|
+
...baseFieldShape,
|
|
164
|
+
type: z.literal("select"),
|
|
165
|
+
options: z.array(selectOptionSchema).min(1),
|
|
166
|
+
/** Allow selecting multiple options (stored as an array). */
|
|
167
|
+
multiple: z.boolean().optional()
|
|
168
|
+
}),
|
|
169
|
+
z.object({
|
|
170
|
+
...baseFieldShape,
|
|
171
|
+
type: z.literal("richText"),
|
|
172
|
+
/**
|
|
173
|
+
* Embeddable composition vocabulary (EC-189, AD-12): component ids editors
|
|
174
|
+
* may embed as `componentEmbed` blocks inside the prose — the SAME `allow`
|
|
175
|
+
* mechanism a `blocks` field uses, applied to rich-text embeds. Each embed
|
|
176
|
+
* holds a single `ComponentNode` validated against this list by
|
|
177
|
+
* `validateCompositionValue` (`@elytracms/project-graph`). Omit to allow any
|
|
178
|
+
* registered component; deeper nesting is governed by each component's slot
|
|
179
|
+
* `allow`. The leash is code.
|
|
180
|
+
*/
|
|
181
|
+
allow: z.array(z.string().min(1)).optional()
|
|
182
|
+
}),
|
|
183
|
+
z.object({
|
|
184
|
+
...baseFieldShape,
|
|
185
|
+
type: z.literal("relation"),
|
|
186
|
+
/** Id of the collection this relation points at. */
|
|
187
|
+
target: z.string().min(1),
|
|
188
|
+
cardinality: cardinalitySchema,
|
|
189
|
+
/**
|
|
190
|
+
* Delivery-shape population declaration (EC-142). Defaults to populated:
|
|
191
|
+
* delivery resolution expands the relation to a depth-1 populated reference.
|
|
192
|
+
* Set to `false` to opt out and receive reference stubs
|
|
193
|
+
* (`{ id, collection }`) instead. Never affects stored values.
|
|
194
|
+
*/
|
|
195
|
+
populate: z.boolean().optional(),
|
|
196
|
+
/**
|
|
197
|
+
* Strict referential integrity (EC-173). References are **weak by
|
|
198
|
+
* default**: unpublishing/deleting a referenced target is allowed and the
|
|
199
|
+
* reference degrades to an explicit validation issue plus a delivery
|
|
200
|
+
* fallback. `strict: true` is the deliberate opt-in that inverts this for
|
|
201
|
+
* one field: while any document references a target through this field,
|
|
202
|
+
* deleting the target is blocked — and while a *published* document does,
|
|
203
|
+
* unpublishing it is blocked too (enforced in `@elytracms/operations`).
|
|
204
|
+
*/
|
|
205
|
+
strict: z.boolean().optional()
|
|
206
|
+
}),
|
|
207
|
+
z.object({
|
|
208
|
+
...baseFieldShape,
|
|
209
|
+
type: z.literal("asset"),
|
|
210
|
+
cardinality: cardinalitySchema.default("one"),
|
|
211
|
+
/** Restrict to specific asset kinds (e.g. 'image', 'file'); empty = any. */
|
|
212
|
+
accept: z.array(z.string()).optional()
|
|
213
|
+
}),
|
|
214
|
+
z.object({
|
|
215
|
+
...baseFieldShape,
|
|
216
|
+
type: z.literal("blocks"),
|
|
217
|
+
/**
|
|
218
|
+
* The composition vocabulary (EC-186, AD-12): allowed component ids editors
|
|
219
|
+
* may place as TOP-LEVEL blocks. Omit to allow any registered component;
|
|
220
|
+
* deeper nesting is governed by each component's slot `allow`. The value
|
|
221
|
+
* (a `ComponentNode` tree) is validated against this by
|
|
222
|
+
* `validateCompositionValue` (`@elytracms/project-graph`). The leash is code.
|
|
223
|
+
*/
|
|
224
|
+
allow: z.array(z.string().min(1)).optional(),
|
|
225
|
+
/** `one` = a single root component; `many` = an ordered list (default). */
|
|
226
|
+
cardinality: cardinalitySchema.default("many")
|
|
227
|
+
}),
|
|
228
|
+
z.object({
|
|
229
|
+
...baseFieldShape,
|
|
230
|
+
type: z.literal("object"),
|
|
231
|
+
/**
|
|
232
|
+
* The nested field-defs. Recursive via `z.lazy` (a subfield may itself be an
|
|
233
|
+
* `object`), so Konditorei's nested menu, the form builder's groups→fields,
|
|
234
|
+
* and multi-entry settings model faithfully. Subfields carry their own `name`
|
|
235
|
+
* (they are object keys), so this is the full `FieldDef`, not the prop shape.
|
|
236
|
+
*/
|
|
237
|
+
fields: z.lazy(() => z.array(fieldDefSchema)),
|
|
238
|
+
/** `one` = a single nested object; `many` = an ordered array (the repeater). */
|
|
239
|
+
cardinality: cardinalitySchema.default("one")
|
|
240
|
+
})
|
|
241
|
+
]);
|
|
242
|
+
var collectionKindSchema = z.enum(["document", "asset"]);
|
|
243
|
+
var collectionFormMetaSchema = z.object({
|
|
244
|
+
/** Plural human label, e.g. "Blog Posts". */
|
|
245
|
+
label: z.string().optional(),
|
|
246
|
+
/** Singular human label, e.g. "Blog Post". */
|
|
247
|
+
labelSingular: z.string().optional(),
|
|
248
|
+
description: z.string().optional(),
|
|
249
|
+
/** Declared ordering of form groups (fieldsets). */
|
|
250
|
+
groups: z.array(z.string()).optional(),
|
|
251
|
+
/** Declared ordering of form tabs. */
|
|
252
|
+
tabs: z.array(z.string()).optional()
|
|
253
|
+
});
|
|
254
|
+
z.object({
|
|
255
|
+
id: z.string().min(1),
|
|
256
|
+
kind: collectionKindSchema.default("document"),
|
|
257
|
+
fields: z.array(fieldDefSchema).default([]),
|
|
258
|
+
form: collectionFormMetaSchema.optional(),
|
|
259
|
+
/** When true, documents in this collection support localized variants (EC-018). */
|
|
260
|
+
localized: z.boolean().optional(),
|
|
261
|
+
/** Name of the field used as the document's display title (defaults to `title`). */
|
|
262
|
+
titleField: z.string().optional(),
|
|
263
|
+
/**
|
|
264
|
+
* When true, this collection holds exactly ONE fixed document — Settings,
|
|
265
|
+
* Menus, Footer, Homepage SEO… (EC-217). The studio skips the list view and
|
|
266
|
+
* opens the detail editor directly on the single document. Pure authoring
|
|
267
|
+
* sugar: internally identical to a `document` collection, so the engine,
|
|
268
|
+
* delivery, and validation paths are unchanged. Meaningless on an `asset`
|
|
269
|
+
* collection (flagged as `invalid-collection-config`).
|
|
270
|
+
*/
|
|
271
|
+
singleton: z.boolean().optional()
|
|
272
|
+
});
|
|
273
|
+
function fieldOf(collection, name) {
|
|
274
|
+
return collection.fields.find((f) => f.name === name);
|
|
275
|
+
}
|
|
276
|
+
var localeSchema = z.string().min(1);
|
|
277
|
+
var documentRefSchema = z.object({
|
|
278
|
+
collection: z.string().min(1),
|
|
279
|
+
id: z.string().min(1)
|
|
280
|
+
});
|
|
281
|
+
documentRefSchema.extend({
|
|
282
|
+
locale: localeSchema
|
|
283
|
+
});
|
|
284
|
+
function documentKey(ref) {
|
|
285
|
+
return `${ref.collection}:${ref.id}`;
|
|
286
|
+
}
|
|
287
|
+
z.enum(["draft", "published"]);
|
|
288
|
+
var documentSchema = z.object({
|
|
289
|
+
collection: z.string().min(1),
|
|
290
|
+
id: z.string().min(1),
|
|
291
|
+
/** Non-localized field values shared across locales. */
|
|
292
|
+
values: z.record(z.string(), z.unknown()).default({}),
|
|
293
|
+
/** Per-locale overrides for localized fields: `{ [locale]: { [field]: value } }`. */
|
|
294
|
+
localized: z.record(localeSchema, z.record(z.string(), z.unknown())).optional(),
|
|
295
|
+
/** The default locale this document was authored in (EC-018 fallback source). */
|
|
296
|
+
defaultLocale: localeSchema.optional()
|
|
297
|
+
});
|
|
298
|
+
function refOf(doc) {
|
|
299
|
+
return { collection: doc.collection, id: doc.id };
|
|
300
|
+
}
|
|
301
|
+
var localeConfigSchema = z.object({
|
|
302
|
+
default: z.string().min(1),
|
|
303
|
+
locales: z.array(z.string().min(1)).min(1)
|
|
304
|
+
});
|
|
305
|
+
function isKnownLocale(config, locale) {
|
|
306
|
+
return config.locales.includes(locale);
|
|
307
|
+
}
|
|
308
|
+
function isFieldLocalized(collection, fieldName) {
|
|
309
|
+
return fieldOf(collection, fieldName)?.localized === true;
|
|
310
|
+
}
|
|
311
|
+
function documentDefaultLocale(doc, config) {
|
|
312
|
+
return doc.defaultLocale ?? config.default;
|
|
313
|
+
}
|
|
314
|
+
function resolveField(collection, doc, fieldName, locale, config) {
|
|
315
|
+
if (!isFieldLocalized(collection, fieldName)) {
|
|
316
|
+
return { value: doc.values[fieldName], sourceLocale: locale, fellBack: false };
|
|
317
|
+
}
|
|
318
|
+
const requested = doc.localized?.[locale];
|
|
319
|
+
if (requested && fieldName in requested) {
|
|
320
|
+
return { value: requested[fieldName], sourceLocale: locale, fellBack: false };
|
|
321
|
+
}
|
|
322
|
+
const fallbackLocale = documentDefaultLocale(doc, config);
|
|
323
|
+
if (fallbackLocale !== locale) {
|
|
324
|
+
const fallback = doc.localized?.[fallbackLocale];
|
|
325
|
+
if (fallback && fieldName in fallback) {
|
|
326
|
+
return { value: fallback[fieldName], sourceLocale: fallbackLocale, fellBack: true };
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (fieldName in doc.values) {
|
|
330
|
+
return { value: doc.values[fieldName], sourceLocale: fallbackLocale, fellBack: true };
|
|
331
|
+
}
|
|
332
|
+
return { value: void 0, sourceLocale: fallbackLocale, fellBack: fallbackLocale !== locale };
|
|
333
|
+
}
|
|
334
|
+
function validateLocale(config, locale, path = ["locale"]) {
|
|
335
|
+
if (isKnownLocale(config, locale)) return [];
|
|
336
|
+
return [
|
|
337
|
+
cmsIssue({
|
|
338
|
+
code: "unknown-locale",
|
|
339
|
+
message: `Locale "${locale}" is not in the configured locale set.`,
|
|
340
|
+
path,
|
|
341
|
+
meta: { locale, configured: config.locales }
|
|
342
|
+
})
|
|
343
|
+
];
|
|
344
|
+
}
|
|
345
|
+
var routeRecordSchema = z.object({
|
|
346
|
+
id: z.string().min(1),
|
|
347
|
+
/** Path pattern, leading slash, with optional `:param` segments. */
|
|
348
|
+
pattern: z.string().min(1),
|
|
349
|
+
/** Locale this route serves (EC-018). Omitted = locale-agnostic. */
|
|
350
|
+
locale: localeSchema.optional(),
|
|
351
|
+
/**
|
|
352
|
+
* The document this route resolves to: a `page`-collection document (whose
|
|
353
|
+
* `body` composition renders into a layout), or a content-collection document
|
|
354
|
+
* (rendered by a dev route component). The `id` may be a `:param` placeholder
|
|
355
|
+
* materialized from the matched URL (EC-166).
|
|
356
|
+
*/
|
|
357
|
+
document: z.object({ collection: z.string().min(1), id: z.string().min(1) }).optional()
|
|
358
|
+
});
|
|
359
|
+
var redirectRecordSchema = z.object({
|
|
360
|
+
id: z.string().min(1),
|
|
361
|
+
from: z.string().min(1),
|
|
362
|
+
to: z.string().min(1),
|
|
363
|
+
/** 301 permanent (default) or 302 temporary. */
|
|
364
|
+
permanent: z.boolean().default(true)
|
|
365
|
+
});
|
|
366
|
+
function splitPath(path) {
|
|
367
|
+
const [pathname] = path.split(/[?#]/, 1);
|
|
368
|
+
return (pathname ?? "").split("/").filter((s) => s.length > 0);
|
|
369
|
+
}
|
|
370
|
+
function compilePattern(pattern) {
|
|
371
|
+
return splitPath(pattern).map(
|
|
372
|
+
(seg) => seg.startsWith(":") ? { kind: "param", name: seg.slice(1) } : { kind: "static", value: seg }
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
function matchSegments(segments, urlSegments) {
|
|
376
|
+
if (segments.length !== urlSegments.length) return void 0;
|
|
377
|
+
const params = {};
|
|
378
|
+
for (let i = 0; i < segments.length; i++) {
|
|
379
|
+
const seg = segments[i];
|
|
380
|
+
const value = urlSegments[i];
|
|
381
|
+
if (seg === void 0 || value === void 0) return void 0;
|
|
382
|
+
if (seg.kind === "static") {
|
|
383
|
+
if (seg.value !== value) return void 0;
|
|
384
|
+
} else {
|
|
385
|
+
params[seg.name] = decodeURIComponent(value);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return params;
|
|
389
|
+
}
|
|
390
|
+
function specificity(segments) {
|
|
391
|
+
return segments.map((s) => s.kind === "static" ? "S" : "P").join("");
|
|
392
|
+
}
|
|
393
|
+
function normalizePath(path) {
|
|
394
|
+
const [pathname] = path.split(/[?#]/, 1);
|
|
395
|
+
const segs = splitPath(pathname ?? "");
|
|
396
|
+
return "/" + segs.join("/");
|
|
397
|
+
}
|
|
398
|
+
function createRouter(routes, redirects = [], options = {}) {
|
|
399
|
+
const issues = [];
|
|
400
|
+
const compiled = routes.map((route) => ({
|
|
401
|
+
route,
|
|
402
|
+
segments: compilePattern(route.pattern)
|
|
403
|
+
}));
|
|
404
|
+
const conflictKey = (c) => {
|
|
405
|
+
const locale = c.route.locale ?? "*";
|
|
406
|
+
const sig = c.segments.map((s) => s.kind === "static" ? `s:${s.value}` : "p").join("/");
|
|
407
|
+
return `${locale}|${sig}`;
|
|
408
|
+
};
|
|
409
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
410
|
+
for (const c of compiled) {
|
|
411
|
+
const key = conflictKey(c);
|
|
412
|
+
const existing = byKey.get(key);
|
|
413
|
+
if (existing) {
|
|
414
|
+
issues.push(
|
|
415
|
+
cmsIssue({
|
|
416
|
+
code: "route-conflict",
|
|
417
|
+
message: `Routes "${existing.route.id}" and "${c.route.id}" both match the same URLs (pattern "${c.route.pattern}").`,
|
|
418
|
+
path: ["routes", c.route.id],
|
|
419
|
+
meta: { conflictsWith: existing.route.id, pattern: c.route.pattern }
|
|
420
|
+
})
|
|
421
|
+
);
|
|
422
|
+
} else {
|
|
423
|
+
byKey.set(key, c);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const redirectMap = /* @__PURE__ */ new Map();
|
|
427
|
+
for (const r of redirects) {
|
|
428
|
+
redirectMap.set(normalizePath(r.from), r);
|
|
429
|
+
}
|
|
430
|
+
const followRedirects = (from) => {
|
|
431
|
+
const chain = [];
|
|
432
|
+
const seen = /* @__PURE__ */ new Set();
|
|
433
|
+
let current = normalizePath(from);
|
|
434
|
+
let permanent = true;
|
|
435
|
+
chain.push(current);
|
|
436
|
+
while (true) {
|
|
437
|
+
const hop = redirectMap.get(current);
|
|
438
|
+
if (!hop) {
|
|
439
|
+
return { target: current, chain, loop: false, permanent };
|
|
440
|
+
}
|
|
441
|
+
if (!hop.permanent) permanent = false;
|
|
442
|
+
const next = normalizePath(hop.to);
|
|
443
|
+
if (seen.has(next) || next === current) {
|
|
444
|
+
chain.push(next);
|
|
445
|
+
return { target: next, chain, loop: true, permanent };
|
|
446
|
+
}
|
|
447
|
+
seen.add(current);
|
|
448
|
+
current = next;
|
|
449
|
+
chain.push(current);
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
const loopReported = /* @__PURE__ */ new Set();
|
|
453
|
+
for (const r of redirects) {
|
|
454
|
+
const res = followRedirects(r.from);
|
|
455
|
+
if (res.loop && !loopReported.has(res.target)) {
|
|
456
|
+
loopReported.add(res.target);
|
|
457
|
+
issues.push(
|
|
458
|
+
cmsIssue({
|
|
459
|
+
code: "redirect-loop",
|
|
460
|
+
message: `Redirect chain starting at "${r.from}" loops (\u2026${res.chain.slice(-3).join(" -> ")}).`,
|
|
461
|
+
path: ["redirects", r.id],
|
|
462
|
+
meta: { chain: res.chain }
|
|
463
|
+
})
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const orderedMatches = (urlSegments, locale) => {
|
|
468
|
+
const matches = [];
|
|
469
|
+
for (const c of compiled) {
|
|
470
|
+
if (locale !== void 0 && c.route.locale !== void 0 && c.route.locale !== locale) {
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
const params = matchSegments(c.segments, urlSegments);
|
|
474
|
+
if (params) matches.push({ compiled: c, params });
|
|
475
|
+
}
|
|
476
|
+
matches.sort((a, b) => specificity(a.compiled.segments).localeCompare(specificity(b.compiled.segments)) * -1);
|
|
477
|
+
return matches;
|
|
478
|
+
};
|
|
479
|
+
const resolveUrl = (url, locale) => {
|
|
480
|
+
const path = normalizePath(url);
|
|
481
|
+
if (redirectMap.has(path)) {
|
|
482
|
+
const res = followRedirects(path);
|
|
483
|
+
return {
|
|
484
|
+
status: "redirect",
|
|
485
|
+
redirectTarget: res.target,
|
|
486
|
+
permanent: res.permanent,
|
|
487
|
+
dynamicParams: {},
|
|
488
|
+
fallback: false
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
const urlSegments = splitPath(path);
|
|
492
|
+
let fallback = false;
|
|
493
|
+
let matches = orderedMatches(urlSegments, locale);
|
|
494
|
+
if (matches.length === 0 && locale && options.defaultLocale && locale !== options.defaultLocale) {
|
|
495
|
+
const fallbackMatches = orderedMatches(urlSegments, options.defaultLocale);
|
|
496
|
+
if (fallbackMatches.length > 0) {
|
|
497
|
+
matches = fallbackMatches;
|
|
498
|
+
fallback = true;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (matches.length === 0 && locale === void 0) {
|
|
502
|
+
matches = orderedMatches(urlSegments, void 0);
|
|
503
|
+
}
|
|
504
|
+
const best = matches[0];
|
|
505
|
+
if (!best) {
|
|
506
|
+
return { status: "notFound", dynamicParams: {}, fallback };
|
|
507
|
+
}
|
|
508
|
+
const route = best.compiled.route;
|
|
509
|
+
return {
|
|
510
|
+
status: "ok",
|
|
511
|
+
route,
|
|
512
|
+
...route.document ? { documentRef: route.document } : {},
|
|
513
|
+
dynamicParams: best.params,
|
|
514
|
+
fallback
|
|
515
|
+
};
|
|
516
|
+
};
|
|
517
|
+
return { issues, resolveUrl, followRedirects };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// ../cms-core/src/hierarchy.ts
|
|
521
|
+
var DEFAULT_PARENT_FIELD = "parent";
|
|
522
|
+
var DEFAULT_SLUG_FIELD = "slug";
|
|
523
|
+
function hierarchyFields(config = {}) {
|
|
524
|
+
return {
|
|
525
|
+
parentField: config.parentField ?? DEFAULT_PARENT_FIELD,
|
|
526
|
+
slugField: config.slugField ?? DEFAULT_SLUG_FIELD
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
function parentRelationField(collection, config = {}) {
|
|
530
|
+
const { parentField } = hierarchyFields(config);
|
|
531
|
+
const field = collection.fields.find((f) => f.name === parentField);
|
|
532
|
+
if (field && field.type === "relation" && field.target === collection.id && field.cardinality === "one") {
|
|
533
|
+
return field;
|
|
534
|
+
}
|
|
535
|
+
return void 0;
|
|
536
|
+
}
|
|
537
|
+
function isHierarchical(collection, config = {}) {
|
|
538
|
+
return parentRelationField(collection, config) !== void 0;
|
|
539
|
+
}
|
|
540
|
+
function readParentId(doc, parentField) {
|
|
541
|
+
const value = doc.values[parentField];
|
|
542
|
+
if (value == null) return void 0;
|
|
543
|
+
if (typeof value === "string") return value.length > 0 ? value : void 0;
|
|
544
|
+
if (typeof value === "object" && "id" in value) {
|
|
545
|
+
const id = value.id;
|
|
546
|
+
return typeof id === "string" && id.length > 0 ? id : void 0;
|
|
547
|
+
}
|
|
548
|
+
return void 0;
|
|
549
|
+
}
|
|
550
|
+
function readSlug(doc, slugField) {
|
|
551
|
+
const value = doc.values[slugField];
|
|
552
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
553
|
+
}
|
|
554
|
+
function composePath(doc, getById, config = {}) {
|
|
555
|
+
const { parentField, slugField } = hierarchyFields(config);
|
|
556
|
+
const segments = [];
|
|
557
|
+
const seen = /* @__PURE__ */ new Set([doc.id]);
|
|
558
|
+
let cycle = false;
|
|
559
|
+
let missingSlug = false;
|
|
560
|
+
let danglingParent = false;
|
|
561
|
+
let current = doc;
|
|
562
|
+
while (current) {
|
|
563
|
+
const slug = readSlug(current, slugField);
|
|
564
|
+
if (slug === void 0) missingSlug = true;
|
|
565
|
+
else segments.unshift(slug);
|
|
566
|
+
const parentId = readParentId(current, parentField);
|
|
567
|
+
if (parentId === void 0) break;
|
|
568
|
+
const parent = getById(parentId);
|
|
569
|
+
if (!parent) {
|
|
570
|
+
danglingParent = true;
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
if (seen.has(parent.id)) {
|
|
574
|
+
cycle = true;
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
seen.add(parent.id);
|
|
578
|
+
current = parent;
|
|
579
|
+
}
|
|
580
|
+
return { segments, ok: !cycle && !missingSlug && !danglingParent, cycle, missingSlug, danglingParent };
|
|
581
|
+
}
|
|
582
|
+
function isHierarchyTemplate(route) {
|
|
583
|
+
if (!route.document) return false;
|
|
584
|
+
const segments = splitPath(route.pattern);
|
|
585
|
+
const last = segments[segments.length - 1];
|
|
586
|
+
return last !== void 0 && last.startsWith(":") && last.endsWith("*");
|
|
587
|
+
}
|
|
588
|
+
function hierarchyMount(route) {
|
|
589
|
+
if (!isHierarchyTemplate(route) || !route.document) return void 0;
|
|
590
|
+
const prefix = splitPath(route.pattern).slice(0, -1);
|
|
591
|
+
if (prefix.some((segment) => segment.startsWith(":"))) return void 0;
|
|
592
|
+
return {
|
|
593
|
+
collection: route.document.collection,
|
|
594
|
+
prefix,
|
|
595
|
+
...route.locale ? { locale: route.locale } : {}
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
function resolveByPath(segments, docs, config = {}) {
|
|
599
|
+
if (segments.length === 0) return void 0;
|
|
600
|
+
const byId = new Map(docs.map((d) => [d.id, d]));
|
|
601
|
+
const getById = (id) => byId.get(id);
|
|
602
|
+
const target = segments.join("/");
|
|
603
|
+
let match;
|
|
604
|
+
for (const doc of docs) {
|
|
605
|
+
const composed = composePath(doc, getById, config);
|
|
606
|
+
if (!composed.ok) continue;
|
|
607
|
+
if (composed.segments.join("/") === target && (!match || doc.id < match.id)) {
|
|
608
|
+
match = doc;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return match;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// ../cms-core/src/validate-document.ts
|
|
615
|
+
function isEmpty(value) {
|
|
616
|
+
if (value === void 0 || value === null) return true;
|
|
617
|
+
if (typeof value === "string") return value.length === 0;
|
|
618
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
619
|
+
if (isPlainObject(value)) return Object.keys(value).length === 0;
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
function isPlainObject(value) {
|
|
623
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
624
|
+
}
|
|
625
|
+
function checkScalarType(field, value) {
|
|
626
|
+
switch (field.type) {
|
|
627
|
+
case "text":
|
|
628
|
+
return typeof value === "string" ? null : "expects a string";
|
|
629
|
+
case "richText":
|
|
630
|
+
return typeof value === "string" || typeof value === "object" && value !== null ? null : "expects rich-text content (a string or a rich-text value)";
|
|
631
|
+
case "number":
|
|
632
|
+
return typeof value === "number" && !Number.isNaN(value) ? null : "expects a number";
|
|
633
|
+
case "boolean":
|
|
634
|
+
return typeof value === "boolean" ? null : "expects a boolean";
|
|
635
|
+
case "date":
|
|
636
|
+
if (typeof value === "string") {
|
|
637
|
+
return Number.isNaN(Date.parse(value)) ? "expects a valid date string" : null;
|
|
638
|
+
}
|
|
639
|
+
return typeof value === "number" ? null : "expects a date (ISO string or epoch number)";
|
|
640
|
+
case "select": {
|
|
641
|
+
const allowed = new Set(field.options.map((o) => o.value));
|
|
642
|
+
const values = field.multiple ? Array.isArray(value) ? value : [value] : [value];
|
|
643
|
+
for (const v of values) {
|
|
644
|
+
if (typeof v !== "string" || !allowed.has(v)) {
|
|
645
|
+
return `has value "${String(v)}" not among its options`;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
case "object":
|
|
651
|
+
if (field.cardinality === "many") {
|
|
652
|
+
return Array.isArray(value) && value.every((item) => isPlainObject(item)) ? null : "expects a list of objects";
|
|
653
|
+
}
|
|
654
|
+
return isPlainObject(value) ? null : "expects an object";
|
|
655
|
+
case "relation":
|
|
656
|
+
case "asset":
|
|
657
|
+
case "blocks":
|
|
658
|
+
return null;
|
|
659
|
+
default: {
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
function checkConstraints(field, value) {
|
|
665
|
+
const out = [];
|
|
666
|
+
const rules = field.validation;
|
|
667
|
+
if (!rules) return out;
|
|
668
|
+
if (typeof value === "string") {
|
|
669
|
+
if (rules.min !== void 0 && value.length < rules.min) out.push(`is shorter than min length ${rules.min}`);
|
|
670
|
+
if (rules.max !== void 0 && value.length > rules.max) out.push(`is longer than max length ${rules.max}`);
|
|
671
|
+
if (rules.pattern !== void 0) {
|
|
672
|
+
let re;
|
|
673
|
+
try {
|
|
674
|
+
re = new RegExp(rules.pattern);
|
|
675
|
+
} catch {
|
|
676
|
+
re = void 0;
|
|
677
|
+
}
|
|
678
|
+
if (re && !re.test(value)) out.push(`does not match pattern ${rules.pattern}`);
|
|
679
|
+
}
|
|
680
|
+
} else if (typeof value === "number") {
|
|
681
|
+
if (rules.min !== void 0 && value < rules.min) out.push(`is less than min ${rules.min}`);
|
|
682
|
+
if (rules.max !== void 0 && value > rules.max) out.push(`is greater than max ${rules.max}`);
|
|
683
|
+
} else if (Array.isArray(value)) {
|
|
684
|
+
if (rules.min !== void 0 && value.length < rules.min) out.push(`has fewer than ${rules.min} items`);
|
|
685
|
+
if (rules.max !== void 0 && value.length > rules.max) out.push(`has more than ${rules.max} items`);
|
|
686
|
+
}
|
|
687
|
+
return out;
|
|
688
|
+
}
|
|
689
|
+
function fieldValueValidator(field) {
|
|
690
|
+
return (value) => {
|
|
691
|
+
if (isEmpty(value)) return true;
|
|
692
|
+
return checkScalarType(field, value) === null && checkConstraints(field, value).length === 0;
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
z.object({
|
|
696
|
+
/** Monotonic version number, starting at 1. */
|
|
697
|
+
version: z.number().int().positive(),
|
|
698
|
+
/** Full document snapshot at this version. */
|
|
699
|
+
snapshot: documentSchema,
|
|
700
|
+
/** When the snapshot was recorded (epoch ms). */
|
|
701
|
+
createdAt: z.number().int().nonnegative(),
|
|
702
|
+
/** Optional label, e.g. "published" or an author note. */
|
|
703
|
+
label: z.string().optional()
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
// ../cms-core/src/fixtures.ts
|
|
707
|
+
var assetCollection = {
|
|
708
|
+
id: "asset",
|
|
709
|
+
kind: "asset",
|
|
710
|
+
titleField: "filename",
|
|
711
|
+
fields: [
|
|
712
|
+
{ name: "filename", type: "text", validation: { required: true } },
|
|
713
|
+
{ name: "alt", type: "text", localized: true },
|
|
714
|
+
{ name: "url", type: "text", validation: { required: true } }
|
|
715
|
+
]
|
|
716
|
+
};
|
|
717
|
+
var authorCollection = {
|
|
718
|
+
id: "author",
|
|
719
|
+
kind: "document",
|
|
720
|
+
titleField: "name",
|
|
721
|
+
fields: [
|
|
722
|
+
{ name: "name", type: "text", validation: { required: true } },
|
|
723
|
+
{ name: "bio", type: "richText" }
|
|
724
|
+
]
|
|
725
|
+
};
|
|
726
|
+
var postCollection = {
|
|
727
|
+
id: "post",
|
|
728
|
+
kind: "document",
|
|
729
|
+
localized: true,
|
|
730
|
+
titleField: "title",
|
|
731
|
+
form: {
|
|
732
|
+
label: "Posts",
|
|
733
|
+
labelSingular: "Post",
|
|
734
|
+
groups: ["content", "meta"],
|
|
735
|
+
tabs: ["main", "seo"]
|
|
736
|
+
},
|
|
737
|
+
fields: [
|
|
738
|
+
{
|
|
739
|
+
name: "title",
|
|
740
|
+
type: "text",
|
|
741
|
+
localized: true,
|
|
742
|
+
validation: { required: true, min: 1, max: 120 },
|
|
743
|
+
form: { label: "Title", group: "content", tab: "main", order: 1 }
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
name: "slug",
|
|
747
|
+
type: "text",
|
|
748
|
+
validation: { required: true, pattern: "^[a-z0-9-]+$", unique: true },
|
|
749
|
+
form: { label: "Slug", group: "content", tab: "main", order: 2 }
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
name: "body",
|
|
753
|
+
type: "richText",
|
|
754
|
+
localized: true,
|
|
755
|
+
form: { label: "Body", group: "content", tab: "main", order: 3 }
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
name: "views",
|
|
759
|
+
type: "number",
|
|
760
|
+
validation: { min: 0 },
|
|
761
|
+
form: { label: "Views", group: "meta", tab: "main" }
|
|
762
|
+
},
|
|
763
|
+
{
|
|
764
|
+
name: "featured",
|
|
765
|
+
type: "boolean",
|
|
766
|
+
form: { label: "Featured", group: "meta", tab: "main" }
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
name: "publishedAt",
|
|
770
|
+
type: "date",
|
|
771
|
+
form: { label: "Published at", group: "meta", tab: "main" }
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
name: "status",
|
|
775
|
+
type: "select",
|
|
776
|
+
options: [
|
|
777
|
+
{ value: "news", label: "News" },
|
|
778
|
+
{ value: "guide", label: "Guide" }
|
|
779
|
+
],
|
|
780
|
+
form: { label: "Status", group: "meta", tab: "main" }
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
name: "author",
|
|
784
|
+
type: "relation",
|
|
785
|
+
target: "author",
|
|
786
|
+
cardinality: "one",
|
|
787
|
+
validation: { required: true },
|
|
788
|
+
form: { label: "Author", group: "meta", tab: "main" }
|
|
789
|
+
},
|
|
790
|
+
{
|
|
791
|
+
name: "related",
|
|
792
|
+
type: "relation",
|
|
793
|
+
target: "post",
|
|
794
|
+
cardinality: "many",
|
|
795
|
+
form: { label: "Related posts", group: "meta", tab: "main" }
|
|
796
|
+
},
|
|
797
|
+
{
|
|
798
|
+
name: "cover",
|
|
799
|
+
type: "asset",
|
|
800
|
+
cardinality: "one",
|
|
801
|
+
accept: ["image"],
|
|
802
|
+
form: { label: "Cover image", group: "content", tab: "main" }
|
|
803
|
+
}
|
|
804
|
+
]
|
|
805
|
+
};
|
|
806
|
+
var pageCollection = {
|
|
807
|
+
id: "page",
|
|
808
|
+
kind: "document",
|
|
809
|
+
titleField: "name",
|
|
810
|
+
form: {
|
|
811
|
+
label: "Pages",
|
|
812
|
+
labelSingular: "Page",
|
|
813
|
+
tabs: ["main", "canvas", "seo"]
|
|
814
|
+
},
|
|
815
|
+
fields: [
|
|
816
|
+
{
|
|
817
|
+
name: "name",
|
|
818
|
+
type: "text",
|
|
819
|
+
validation: { required: true },
|
|
820
|
+
form: { tab: "main" }
|
|
821
|
+
},
|
|
822
|
+
{
|
|
823
|
+
name: "body",
|
|
824
|
+
type: "blocks",
|
|
825
|
+
allow: [
|
|
826
|
+
"base.primitives.Heading",
|
|
827
|
+
"base.primitives.Text",
|
|
828
|
+
"base.primitives.Image",
|
|
829
|
+
"base.primitives.Button",
|
|
830
|
+
"base.primitives.Stack",
|
|
831
|
+
"project.MarketingHero",
|
|
832
|
+
"project.FeatureCard",
|
|
833
|
+
"project.PricingCallout"
|
|
834
|
+
],
|
|
835
|
+
cardinality: "many",
|
|
836
|
+
form: { label: "Canvas", tab: "canvas" }
|
|
837
|
+
},
|
|
838
|
+
{ name: "layoutId", type: "text", form: { tab: "main" } },
|
|
839
|
+
{ name: "seoTitle", type: "text", form: { tab: "seo" } },
|
|
840
|
+
{ name: "seoDescription", type: "text", form: { tab: "seo" } },
|
|
841
|
+
{ name: "seoCanonical", type: "text", form: { tab: "seo" } },
|
|
842
|
+
{ name: "seoOgTitle", type: "text", form: { tab: "seo" } },
|
|
843
|
+
{ name: "seoOgImage", type: "text", form: { tab: "seo" } },
|
|
844
|
+
{ name: "seoNoindex", type: "boolean", form: { tab: "seo" } }
|
|
845
|
+
]
|
|
846
|
+
};
|
|
847
|
+
var sampleCollections = [
|
|
848
|
+
assetCollection,
|
|
849
|
+
authorCollection,
|
|
850
|
+
postCollection,
|
|
851
|
+
pageCollection
|
|
852
|
+
];
|
|
853
|
+
var sampleFilterableFields = {
|
|
854
|
+
author: ["name"],
|
|
855
|
+
post: ["slug", "views", "featured", "publishedAt", "status", "author"]
|
|
856
|
+
};
|
|
857
|
+
function withSampleFilterable(collection) {
|
|
858
|
+
const filterable = new Set(sampleFilterableFields[collection.id] ?? []);
|
|
859
|
+
if (filterable.size === 0) return collection;
|
|
860
|
+
return {
|
|
861
|
+
...collection,
|
|
862
|
+
fields: collection.fields.map(
|
|
863
|
+
(field) => filterable.has(field.name) ? { ...field, filterable: true } : field
|
|
864
|
+
)
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
sampleCollections.map(withSampleFilterable);
|
|
868
|
+
var coverAsset = {
|
|
869
|
+
collection: "asset",
|
|
870
|
+
id: "asset-cover",
|
|
871
|
+
values: { filename: "cover.png", url: "https://cdn/cover.png" },
|
|
872
|
+
localized: { en: { alt: "A cover" }, de: { alt: "Ein Titelbild" } },
|
|
873
|
+
defaultLocale: "en"
|
|
874
|
+
};
|
|
875
|
+
var libraryCoverAsset = {
|
|
876
|
+
collection: "asset",
|
|
877
|
+
id: "asset_cover",
|
|
878
|
+
values: { filename: "cover.jpg", url: "https://cdn/cover.jpg" },
|
|
879
|
+
localized: { en: { alt: "Cover image" }, de: { alt: "Titelbild" } },
|
|
880
|
+
defaultLocale: "en"
|
|
881
|
+
};
|
|
882
|
+
var authorAda = {
|
|
883
|
+
collection: "author",
|
|
884
|
+
id: "author-ada",
|
|
885
|
+
values: { name: "Ada Lovelace", bio: "Pioneer." }
|
|
886
|
+
};
|
|
887
|
+
var validPost = {
|
|
888
|
+
collection: "post",
|
|
889
|
+
id: "post-hello",
|
|
890
|
+
values: {
|
|
891
|
+
slug: "hello-world",
|
|
892
|
+
views: 10,
|
|
893
|
+
featured: true,
|
|
894
|
+
publishedAt: "2026-01-02T00:00:00.000Z",
|
|
895
|
+
status: "news",
|
|
896
|
+
author: { collection: "author", id: "author-ada" },
|
|
897
|
+
related: [],
|
|
898
|
+
cover: "asset-cover"
|
|
899
|
+
},
|
|
900
|
+
localized: {
|
|
901
|
+
en: { title: "Hello World", body: "<p>Hi</p>" },
|
|
902
|
+
de: { title: "Hallo Welt", body: "<p>Hallo</p>" }
|
|
903
|
+
},
|
|
904
|
+
defaultLocale: "en"
|
|
905
|
+
};
|
|
906
|
+
var partiallyLocalizedPost = {
|
|
907
|
+
collection: "post",
|
|
908
|
+
id: "post-fallback",
|
|
909
|
+
values: {
|
|
910
|
+
slug: "fallback",
|
|
911
|
+
author: { collection: "author", id: "author-ada" }
|
|
912
|
+
},
|
|
913
|
+
localized: {
|
|
914
|
+
en: { title: "Only English", body: "<p>en</p>" }
|
|
915
|
+
// no `de` variant — `de` requests must fall back to `en`
|
|
916
|
+
},
|
|
917
|
+
defaultLocale: "en"
|
|
918
|
+
};
|
|
919
|
+
var samplePageHome = {
|
|
920
|
+
collection: "page",
|
|
921
|
+
id: "home",
|
|
922
|
+
values: {
|
|
923
|
+
name: "Home",
|
|
924
|
+
layoutId: "layout.main",
|
|
925
|
+
seoTitle: "Welcome to Elytra",
|
|
926
|
+
seoDescription: "A code-first website builder.",
|
|
927
|
+
body: [
|
|
928
|
+
{
|
|
929
|
+
id: "p-home-heading",
|
|
930
|
+
componentId: "base.primitives.Heading",
|
|
931
|
+
props: {
|
|
932
|
+
level: { kind: "static", value: 1 },
|
|
933
|
+
text: { kind: "static", value: "Welcome to Elytra" }
|
|
934
|
+
}
|
|
935
|
+
},
|
|
936
|
+
{
|
|
937
|
+
id: "p-home-intro",
|
|
938
|
+
componentId: "base.primitives.Text",
|
|
939
|
+
props: { value: { kind: "static", value: "A code-first website builder." } }
|
|
940
|
+
}
|
|
941
|
+
]
|
|
942
|
+
},
|
|
943
|
+
defaultLocale: "en"
|
|
944
|
+
};
|
|
945
|
+
var sampleDocuments = [
|
|
946
|
+
coverAsset,
|
|
947
|
+
authorAda,
|
|
948
|
+
validPost,
|
|
949
|
+
partiallyLocalizedPost,
|
|
950
|
+
libraryCoverAsset,
|
|
951
|
+
samplePageHome
|
|
952
|
+
];
|
|
953
|
+
sampleDocuments.map(
|
|
954
|
+
(doc) => documentKey(refOf(doc))
|
|
955
|
+
);
|
|
956
|
+
|
|
957
|
+
// ../component-registry/src/manifest.ts
|
|
958
|
+
function defineComponent(manifest) {
|
|
959
|
+
return manifest;
|
|
960
|
+
}
|
|
961
|
+
function manifestPropField(manifest, name) {
|
|
962
|
+
const field = manifest.props[name];
|
|
963
|
+
return field ? { ...field, name } : void 0;
|
|
964
|
+
}
|
|
965
|
+
function manifestSlots(manifest) {
|
|
966
|
+
const compositionValueProp = manifest.composition?.valueProp;
|
|
967
|
+
const fromBlocksProps = [];
|
|
968
|
+
for (const [name, field] of Object.entries(manifest.props)) {
|
|
969
|
+
if (field.type !== "blocks") continue;
|
|
970
|
+
if (name === compositionValueProp) continue;
|
|
971
|
+
fromBlocksProps.push({
|
|
972
|
+
name,
|
|
973
|
+
...field.form?.label ? { label: field.form.label } : {},
|
|
974
|
+
...field.validation?.required ? { required: true } : {},
|
|
975
|
+
...field.allow ? { allow: field.allow } : {}
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
return fromBlocksProps.length === 0 ? manifest.slots : [...manifest.slots, ...fromBlocksProps];
|
|
979
|
+
}
|
|
980
|
+
function propValueValidator(manifest, name) {
|
|
981
|
+
const field = manifestPropField(manifest, name);
|
|
982
|
+
return field ? fieldValueValidator(field) : void 0;
|
|
983
|
+
}
|
|
984
|
+
function namespaceOf(id) {
|
|
985
|
+
if (id.startsWith("base.primitives.")) return "base.primitives";
|
|
986
|
+
const first = id.split(".")[0];
|
|
987
|
+
return first ?? "";
|
|
988
|
+
}
|
|
989
|
+
function isPrimitive(id) {
|
|
990
|
+
return id.startsWith("base.primitives.");
|
|
991
|
+
}
|
|
992
|
+
function toManifestView(manifest) {
|
|
993
|
+
const props = {};
|
|
994
|
+
for (const [name, field] of Object.entries(manifest.props)) {
|
|
995
|
+
props[name] = {
|
|
996
|
+
// EC-190: props are static; the binding path retires in slice 5. Until then
|
|
997
|
+
// every prop reports bindable so the graph validator's binding branch stays
|
|
998
|
+
// inert (no false `binding-not-allowed`). `validate` is the field-def value
|
|
999
|
+
// validator — the SAME check the CMS uses for a document field value.
|
|
1000
|
+
bindable: true,
|
|
1001
|
+
validate: fieldValueValidator({ ...field})
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
return {
|
|
1005
|
+
id: manifest.id,
|
|
1006
|
+
props,
|
|
1007
|
+
// EC-191: carry canvas-eligibility so `validateCompositionValue` can reject a
|
|
1008
|
+
// server-only component placed in a composition.
|
|
1009
|
+
...manifest.clientRenderable === false ? { clientRenderable: false } : {},
|
|
1010
|
+
// EC-186: carry the slot's placement vocabulary (`allow`) through to the
|
|
1011
|
+
// graph validator so it can enforce `forbidden-component` — previously
|
|
1012
|
+
// dropped here, leaving slot `allow` declared-but-unenforced. EC-190:
|
|
1013
|
+
// `manifestSlots` folds in `blocks`-type props as slots, so a blocks prop's
|
|
1014
|
+
// `allow` leash is enforced through the same path.
|
|
1015
|
+
slots: manifestSlots(manifest).map((s) => ({
|
|
1016
|
+
name: s.name,
|
|
1017
|
+
required: s.required,
|
|
1018
|
+
...s.allow ? { allow: s.allow } : {}
|
|
1019
|
+
}))
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
var jsonValueSchema = z.lazy(
|
|
1023
|
+
() => z.union([
|
|
1024
|
+
z.string(),
|
|
1025
|
+
z.number(),
|
|
1026
|
+
z.boolean(),
|
|
1027
|
+
z.null(),
|
|
1028
|
+
z.array(jsonValueSchema),
|
|
1029
|
+
z.record(z.string(), jsonValueSchema)
|
|
1030
|
+
])
|
|
1031
|
+
);
|
|
1032
|
+
var idSchema = z.string().min(1);
|
|
1033
|
+
var COMPONENT_ID_PATTERN = /^(base\.primitives|project)\.[A-Za-z][A-Za-z0-9]*(\.[A-Za-z][A-Za-z0-9]*)*$/;
|
|
1034
|
+
function isValidComponentId(componentId) {
|
|
1035
|
+
return COMPONENT_ID_PATTERN.test(componentId);
|
|
1036
|
+
}
|
|
1037
|
+
var DEFAULT_SLOT = "children";
|
|
1038
|
+
var OUTLET_SLOT = "outlet";
|
|
1039
|
+
var bindingModeSchema = z.enum(["value", "object", "spread", "repeaterItem", "condition"]);
|
|
1040
|
+
var bindingReferenceSchema = z.object({
|
|
1041
|
+
/** Id of a data source (REST / CMS query / Convex query). */
|
|
1042
|
+
sourceId: idSchema,
|
|
1043
|
+
/** Stable path token (JSON Pointer / JSONPath) that survives UI label changes. */
|
|
1044
|
+
token: z.string().min(1),
|
|
1045
|
+
mode: bindingModeSchema
|
|
1046
|
+
});
|
|
1047
|
+
var conditionOperatorSchema = z.enum([
|
|
1048
|
+
"truthy",
|
|
1049
|
+
"exists",
|
|
1050
|
+
"eq",
|
|
1051
|
+
"neq",
|
|
1052
|
+
"gt",
|
|
1053
|
+
"gte",
|
|
1054
|
+
"lt",
|
|
1055
|
+
"lte"
|
|
1056
|
+
]);
|
|
1057
|
+
var conditionSchema = z.object({
|
|
1058
|
+
source: bindingReferenceSchema,
|
|
1059
|
+
operator: conditionOperatorSchema,
|
|
1060
|
+
/** Right-hand comparison value for binary operators (omitted for truthy/exists). */
|
|
1061
|
+
value: jsonValueSchema.optional()
|
|
1062
|
+
});
|
|
1063
|
+
var propValueSchema = z.discriminatedUnion("kind", [
|
|
1064
|
+
z.object({ kind: z.literal("static"), value: jsonValueSchema }),
|
|
1065
|
+
z.object({ kind: z.literal("binding"), binding: bindingReferenceSchema })
|
|
1066
|
+
]);
|
|
1067
|
+
var componentNodeSchema = z.lazy(
|
|
1068
|
+
() => z.object({
|
|
1069
|
+
id: idSchema,
|
|
1070
|
+
componentId: idSchema,
|
|
1071
|
+
props: z.record(z.string(), propValueSchema).optional(),
|
|
1072
|
+
slots: z.record(z.string(), z.array(componentNodeSchema)).optional(),
|
|
1073
|
+
condition: conditionSchema.optional()
|
|
1074
|
+
})
|
|
1075
|
+
);
|
|
1076
|
+
var layoutSchema = z.object({
|
|
1077
|
+
id: idSchema,
|
|
1078
|
+
name: z.string().min(1),
|
|
1079
|
+
root: componentNodeSchema
|
|
1080
|
+
});
|
|
1081
|
+
var PROJECT_GRAPH_SCHEMA_VERSION = 2;
|
|
1082
|
+
var projectGraphSchema = z.object({
|
|
1083
|
+
id: idSchema,
|
|
1084
|
+
schemaVersion: z.number().int().nonnegative(),
|
|
1085
|
+
metadata: z.object({ name: z.string().optional() }).optional(),
|
|
1086
|
+
/** Link to project settings owned by studio-core. */
|
|
1087
|
+
settingsRef: idSchema.optional(),
|
|
1088
|
+
layouts: z.array(layoutSchema).default([])
|
|
1089
|
+
});
|
|
1090
|
+
var STACK_COMPONENT_ID = "base.primitives.Stack";
|
|
1091
|
+
var GRID_COMPONENT_ID = "base.primitives.Grid";
|
|
1092
|
+
var SPACING_TOKEN_NAMES = ["px", "1", "2", "3", "4", "6", "8"];
|
|
1093
|
+
var spacingTokenNameSchema = z.enum(SPACING_TOKEN_NAMES);
|
|
1094
|
+
var SPACING_TOKEN_CSS = {
|
|
1095
|
+
px: "1px",
|
|
1096
|
+
"1": "0.25rem",
|
|
1097
|
+
"2": "0.5rem",
|
|
1098
|
+
"3": "0.75rem",
|
|
1099
|
+
"4": "1rem",
|
|
1100
|
+
"6": "1.5rem",
|
|
1101
|
+
"8": "2rem"
|
|
1102
|
+
};
|
|
1103
|
+
function isSpacingTokenName(value) {
|
|
1104
|
+
return typeof value === "string" && SPACING_TOKEN_NAMES.includes(value);
|
|
1105
|
+
}
|
|
1106
|
+
function spacingTokenCss(name) {
|
|
1107
|
+
return isSpacingTokenName(name) ? SPACING_TOKEN_CSS[name] : void 0;
|
|
1108
|
+
}
|
|
1109
|
+
z.boolean();
|
|
1110
|
+
var STACK_DIRECTIONS = ["vertical", "horizontal"];
|
|
1111
|
+
z.enum(STACK_DIRECTIONS);
|
|
1112
|
+
var CONTAINER_ALIGNMENTS = ["start", "center", "end", "stretch"];
|
|
1113
|
+
z.enum(CONTAINER_ALIGNMENTS);
|
|
1114
|
+
var GRID_MIN_COLUMNS = 1;
|
|
1115
|
+
var GRID_MAX_COLUMNS = 12;
|
|
1116
|
+
z.number().int().min(GRID_MIN_COLUMNS).max(GRID_MAX_COLUMNS);
|
|
1117
|
+
z.union([
|
|
1118
|
+
spacingTokenNameSchema,
|
|
1119
|
+
z.number().int().nonnegative()
|
|
1120
|
+
]);
|
|
1121
|
+
function staticProps(values) {
|
|
1122
|
+
const entries = Object.entries(values).filter(([, v]) => v !== void 0);
|
|
1123
|
+
if (entries.length === 0) return void 0;
|
|
1124
|
+
return Object.fromEntries(
|
|
1125
|
+
entries.map(([name, value]) => [name, { kind: "static", value }])
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
function stackContainerNode(id, options, children) {
|
|
1129
|
+
const props = staticProps({
|
|
1130
|
+
direction: options.direction,
|
|
1131
|
+
gap: options.gap,
|
|
1132
|
+
align: options.align
|
|
1133
|
+
});
|
|
1134
|
+
return {
|
|
1135
|
+
id,
|
|
1136
|
+
componentId: STACK_COMPONENT_ID,
|
|
1137
|
+
...props ? { props } : {},
|
|
1138
|
+
slots: { [DEFAULT_SLOT]: children }
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
function gridContainerNode(id, options, children) {
|
|
1142
|
+
const props = staticProps({
|
|
1143
|
+
columns: options.columns,
|
|
1144
|
+
gap: options.gap,
|
|
1145
|
+
align: options.align,
|
|
1146
|
+
stackOnMobile: options.stackOnMobile
|
|
1147
|
+
});
|
|
1148
|
+
return {
|
|
1149
|
+
id,
|
|
1150
|
+
componentId: GRID_COMPONENT_ID,
|
|
1151
|
+
...props ? { props } : {},
|
|
1152
|
+
slots: { [DEFAULT_SLOT]: children }
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
var issueCodeSchema = z.enum([
|
|
1156
|
+
"invalid-component-name",
|
|
1157
|
+
"unknown-component",
|
|
1158
|
+
"duplicate-node-id",
|
|
1159
|
+
"unknown-prop",
|
|
1160
|
+
"invalid-prop",
|
|
1161
|
+
"binding-not-allowed",
|
|
1162
|
+
"unresolved-binding",
|
|
1163
|
+
"missing-required-slot",
|
|
1164
|
+
"unknown-slot",
|
|
1165
|
+
// Placement constraints (EC-186): a component appears where its slot's `allow`
|
|
1166
|
+
// vocabulary — or a composition field's `allow` — forbids it.
|
|
1167
|
+
"forbidden-component",
|
|
1168
|
+
// Canvas-eligibility (EC-191): a server-only (RSC / non-client-renderable)
|
|
1169
|
+
// component was placed in an editor composition, which is client-rendered.
|
|
1170
|
+
// Such components belong in dev frames (page.tsx / layout.tsx), not the canvas.
|
|
1171
|
+
"server-only-component",
|
|
1172
|
+
"unknown-layout",
|
|
1173
|
+
"route-conflict",
|
|
1174
|
+
// Layout containers (EC-161): malformed Stack/Grid canonical props.
|
|
1175
|
+
"invalid-container-direction",
|
|
1176
|
+
"invalid-container-alignment",
|
|
1177
|
+
"invalid-container-gap",
|
|
1178
|
+
"invalid-container-columns",
|
|
1179
|
+
// Responsive defaults (EC-163): malformed Grid stack-on-mobile override.
|
|
1180
|
+
"invalid-container-responsive"
|
|
1181
|
+
]);
|
|
1182
|
+
var issueSeveritySchema = z.enum(["error", "warning"]);
|
|
1183
|
+
z.object({
|
|
1184
|
+
code: issueCodeSchema,
|
|
1185
|
+
severity: issueSeveritySchema,
|
|
1186
|
+
message: z.string(),
|
|
1187
|
+
/** JSON path into the graph, e.g. ['pages', 0, 'root', 'slots', 'children', 1]. */
|
|
1188
|
+
path: z.array(z.union([z.string(), z.number()])),
|
|
1189
|
+
/** The offending node id, when applicable. */
|
|
1190
|
+
nodeId: idSchema.optional(),
|
|
1191
|
+
meta: z.record(z.string(), jsonValueSchema).optional()
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
// ../project-graph/src/serialize.ts
|
|
1195
|
+
function parseProjectGraph(input) {
|
|
1196
|
+
let json;
|
|
1197
|
+
try {
|
|
1198
|
+
json = typeof input === "string" ? JSON.parse(input) : input;
|
|
1199
|
+
} catch {
|
|
1200
|
+
json = void 0;
|
|
1201
|
+
}
|
|
1202
|
+
const result = projectGraphSchema.safeParse(json);
|
|
1203
|
+
return result.success ? { ok: true, graph: result.data } : { ok: false, error: result.error };
|
|
1204
|
+
}
|
|
1205
|
+
var textNode = (id, value) => ({
|
|
1206
|
+
id,
|
|
1207
|
+
componentId: "base.primitives.Text",
|
|
1208
|
+
props: { value: { kind: "static", value } }
|
|
1209
|
+
});
|
|
1210
|
+
function containerGraph(id, root) {
|
|
1211
|
+
return {
|
|
1212
|
+
id,
|
|
1213
|
+
schemaVersion: PROJECT_GRAPH_SCHEMA_VERSION,
|
|
1214
|
+
layouts: [{ id: `${id}.layout`, name: "Layout", root }]
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
containerGraph(
|
|
1218
|
+
"proj-containers",
|
|
1219
|
+
stackContainerNode("n-root", { direction: "vertical", gap: "4", align: "start" }, [
|
|
1220
|
+
gridContainerNode("n-grid", { columns: 2, gap: "2", align: "stretch" }, [
|
|
1221
|
+
textNode("n-cell-1", "Cell one"),
|
|
1222
|
+
{
|
|
1223
|
+
id: "n-cell-2",
|
|
1224
|
+
componentId: "base.primitives.Heading",
|
|
1225
|
+
props: { level: { kind: "static", value: 2 }, text: { kind: "static", value: "Cell two" } }
|
|
1226
|
+
}
|
|
1227
|
+
]),
|
|
1228
|
+
stackContainerNode("n-row", { direction: "horizontal", gap: "2", align: "center" }, [
|
|
1229
|
+
textNode("n-row-a", "Left"),
|
|
1230
|
+
textNode("n-row-b", "Right")
|
|
1231
|
+
])
|
|
1232
|
+
])
|
|
1233
|
+
);
|
|
1234
|
+
containerGraph(
|
|
1235
|
+
"proj-soup",
|
|
1236
|
+
stackContainerNode("n-root", { gap: "4" }, [
|
|
1237
|
+
stackContainerNode("n-wrap-1", { gap: "2" }, [
|
|
1238
|
+
stackContainerNode("n-wrap-2", {}, [
|
|
1239
|
+
gridContainerNode("n-wrap-3", { columns: 2 }, [textNode("n-deep", "Deeply wrapped")])
|
|
1240
|
+
])
|
|
1241
|
+
]),
|
|
1242
|
+
gridContainerNode("n-empty-grid", { columns: 3, gap: "2" }, []),
|
|
1243
|
+
textNode("n-plain", "Plain sibling")
|
|
1244
|
+
])
|
|
1245
|
+
);
|
|
1246
|
+
|
|
1247
|
+
// ../component-registry/src/registry.ts
|
|
1248
|
+
var ComponentRegistry = class {
|
|
1249
|
+
byId = /* @__PURE__ */ new Map();
|
|
1250
|
+
viewCache = /* @__PURE__ */ new Map();
|
|
1251
|
+
issues = [];
|
|
1252
|
+
constructor(manifests = []) {
|
|
1253
|
+
for (const manifest of manifests) this.register(manifest);
|
|
1254
|
+
}
|
|
1255
|
+
register(manifest) {
|
|
1256
|
+
if (!isValidComponentId(manifest.id)) {
|
|
1257
|
+
this.issues.push({
|
|
1258
|
+
code: "invalid-id",
|
|
1259
|
+
componentId: manifest.id,
|
|
1260
|
+
message: `Component id "${manifest.id}" is not namespaced under base.primitives.* or project.*.`
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
if (this.byId.has(manifest.id)) {
|
|
1264
|
+
this.issues.push({
|
|
1265
|
+
code: "duplicate-id",
|
|
1266
|
+
componentId: manifest.id,
|
|
1267
|
+
message: `Duplicate component id "${manifest.id}" \u2014 keeping the first registration.`
|
|
1268
|
+
});
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
this.byId.set(manifest.id, manifest);
|
|
1272
|
+
}
|
|
1273
|
+
has(id) {
|
|
1274
|
+
return this.byId.has(id);
|
|
1275
|
+
}
|
|
1276
|
+
getManifest(id) {
|
|
1277
|
+
return this.byId.get(id);
|
|
1278
|
+
}
|
|
1279
|
+
/** Required lookup — throws when the component is not registered. */
|
|
1280
|
+
require(id) {
|
|
1281
|
+
const manifest = this.byId.get(id);
|
|
1282
|
+
if (!manifest) throw new Error(`Unknown component "${id}".`);
|
|
1283
|
+
return manifest;
|
|
1284
|
+
}
|
|
1285
|
+
list(filter = {}) {
|
|
1286
|
+
const q = filter.query?.toLowerCase();
|
|
1287
|
+
return [...this.byId.values()].filter((m) => {
|
|
1288
|
+
if (filter.namespace && namespaceOf(m.id) !== filter.namespace) return false;
|
|
1289
|
+
if (filter.category && m.category !== filter.category) return false;
|
|
1290
|
+
if (q) {
|
|
1291
|
+
const hay = `${m.id} ${m.title ?? ""} ${m.category ?? ""}`.toLowerCase();
|
|
1292
|
+
if (!hay.includes(q)) return false;
|
|
1293
|
+
}
|
|
1294
|
+
return true;
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
/** Platform primitives (`base.primitives.*`). */
|
|
1298
|
+
primitives() {
|
|
1299
|
+
return this.list().filter((m) => isPrimitive(m.id));
|
|
1300
|
+
}
|
|
1301
|
+
/** Project-level components (everything not under `base.primitives.*`). */
|
|
1302
|
+
projectComponents() {
|
|
1303
|
+
return this.list().filter((m) => !isPrimitive(m.id));
|
|
1304
|
+
}
|
|
1305
|
+
get size() {
|
|
1306
|
+
return this.byId.size;
|
|
1307
|
+
}
|
|
1308
|
+
/** A `ComponentLookup` view for the project-graph validator. */
|
|
1309
|
+
get lookup() {
|
|
1310
|
+
return {
|
|
1311
|
+
has: (id) => this.byId.has(id),
|
|
1312
|
+
get: (id) => {
|
|
1313
|
+
if (!this.byId.has(id)) return void 0;
|
|
1314
|
+
let view = this.viewCache.get(id);
|
|
1315
|
+
if (!view) {
|
|
1316
|
+
view = toManifestView(this.byId.get(id));
|
|
1317
|
+
this.viewCache.set(id, view);
|
|
1318
|
+
}
|
|
1319
|
+
return view;
|
|
1320
|
+
}
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
};
|
|
1324
|
+
|
|
1325
|
+
// ../runtime-renderer/src/context.ts
|
|
1326
|
+
function getImplementation(implementations, id) {
|
|
1327
|
+
if (implementations instanceof Map) return implementations.get(id);
|
|
1328
|
+
return implementations[id];
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// ../runtime-renderer/src/condition.ts
|
|
1332
|
+
function evaluateCondition(condition, resolveBinding, item) {
|
|
1333
|
+
let left;
|
|
1334
|
+
try {
|
|
1335
|
+
left = resolveBinding(condition.source, item);
|
|
1336
|
+
} catch {
|
|
1337
|
+
return false;
|
|
1338
|
+
}
|
|
1339
|
+
switch (condition.operator) {
|
|
1340
|
+
case "truthy":
|
|
1341
|
+
return Boolean(left);
|
|
1342
|
+
case "exists":
|
|
1343
|
+
return left !== void 0 && left !== null;
|
|
1344
|
+
case "eq":
|
|
1345
|
+
return left === condition.value;
|
|
1346
|
+
case "neq":
|
|
1347
|
+
return left !== condition.value;
|
|
1348
|
+
case "gt":
|
|
1349
|
+
return compare(left, condition.value, (a, b) => a > b);
|
|
1350
|
+
case "gte":
|
|
1351
|
+
return compare(left, condition.value, (a, b) => a >= b);
|
|
1352
|
+
case "lt":
|
|
1353
|
+
return compare(left, condition.value, (a, b) => a < b);
|
|
1354
|
+
case "lte":
|
|
1355
|
+
return compare(left, condition.value, (a, b) => a <= b);
|
|
1356
|
+
default: {
|
|
1357
|
+
const _never = condition.operator;
|
|
1358
|
+
return Boolean(_never);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
function compare(left, right, op) {
|
|
1363
|
+
if (typeof left !== "number" || typeof right !== "number") return false;
|
|
1364
|
+
return op(left, right);
|
|
1365
|
+
}
|
|
1366
|
+
var ContentClientContext = createContext(null);
|
|
1367
|
+
function ContentClientProvider({
|
|
1368
|
+
client,
|
|
1369
|
+
children
|
|
1370
|
+
}) {
|
|
1371
|
+
return createElement(ContentClientContext.Provider, { value: client }, children);
|
|
1372
|
+
}
|
|
1373
|
+
function MissingComponentFallback(props) {
|
|
1374
|
+
return /* @__PURE__ */ jsx(
|
|
1375
|
+
"div",
|
|
1376
|
+
{
|
|
1377
|
+
"data-ec-fallback": "missing-component",
|
|
1378
|
+
"data-ec-node-id": props.nodeId,
|
|
1379
|
+
"data-ec-component-id": props.componentId,
|
|
1380
|
+
children: `Missing component "${props.componentId}" (node ${props.nodeId})`
|
|
1381
|
+
}
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
function MissingSlotPlaceholder(props) {
|
|
1385
|
+
return /* @__PURE__ */ jsx("div", { "data-ec-fallback": "missing-slot", "data-ec-node-id": props.nodeId, "data-ec-slot": props.slot, children: `Missing required slot "${props.slot}" (node ${props.nodeId})` });
|
|
1386
|
+
}
|
|
1387
|
+
function RenderErrorFallback(props) {
|
|
1388
|
+
return /* @__PURE__ */ jsx("div", { "data-ec-fallback": "render-error", "data-ec-node-id": props.nodeId, children: props.message ? `Render error in node ${props.nodeId}: ${props.message}` : `Render error in node ${props.nodeId}` });
|
|
1389
|
+
}
|
|
1390
|
+
var RenderErrorBoundary = class extends Component {
|
|
1391
|
+
constructor(props) {
|
|
1392
|
+
super(props);
|
|
1393
|
+
this.state = { error: null };
|
|
1394
|
+
}
|
|
1395
|
+
static getDerivedStateFromError(error) {
|
|
1396
|
+
return { error };
|
|
1397
|
+
}
|
|
1398
|
+
componentDidCatch(_error, _info) {
|
|
1399
|
+
}
|
|
1400
|
+
render() {
|
|
1401
|
+
if (this.state.error) {
|
|
1402
|
+
return /* @__PURE__ */ jsx(RenderErrorFallback, { nodeId: this.props.nodeId, message: this.state.error.message });
|
|
1403
|
+
}
|
|
1404
|
+
return this.props.children;
|
|
1405
|
+
}
|
|
1406
|
+
};
|
|
1407
|
+
function renderNode(node, ctx) {
|
|
1408
|
+
if (node.condition && !evaluateCondition(node.condition, ctx.resolveBinding, ctx.item)) {
|
|
1409
|
+
return null;
|
|
1410
|
+
}
|
|
1411
|
+
const manifest = ctx.registry.getManifest(node.componentId);
|
|
1412
|
+
const implementation = getImplementation(ctx.implementations, node.componentId);
|
|
1413
|
+
if (!manifest || !implementation) {
|
|
1414
|
+
return decorate(
|
|
1415
|
+
ctx,
|
|
1416
|
+
node,
|
|
1417
|
+
/* @__PURE__ */ jsx(MissingComponentFallback, { nodeId: node.id, componentId: node.componentId })
|
|
1418
|
+
);
|
|
1419
|
+
}
|
|
1420
|
+
let resolved;
|
|
1421
|
+
try {
|
|
1422
|
+
resolved = resolveRender(node, manifest, ctx);
|
|
1423
|
+
} catch (error) {
|
|
1424
|
+
return decorate(ctx, node, /* @__PURE__ */ jsx(RenderErrorFallback, { nodeId: node.id, message: messageOf(error) }));
|
|
1425
|
+
}
|
|
1426
|
+
const element = createElement(
|
|
1427
|
+
implementation,
|
|
1428
|
+
{ ...resolved.props, ...resolved.slotProps, key: node.id },
|
|
1429
|
+
resolved.children
|
|
1430
|
+
);
|
|
1431
|
+
return decorate(
|
|
1432
|
+
ctx,
|
|
1433
|
+
node,
|
|
1434
|
+
/* @__PURE__ */ jsx(RenderErrorBoundary, { nodeId: node.id, children: element }, node.id)
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
function decorate(ctx, node, rendered) {
|
|
1438
|
+
return ctx.decorateNode ? ctx.decorateNode(node, rendered) : rendered;
|
|
1439
|
+
}
|
|
1440
|
+
function createEmbedRenderer(ctx) {
|
|
1441
|
+
return (node) => renderComposition(node, ctx);
|
|
1442
|
+
}
|
|
1443
|
+
function manifestConsumesEmbeds(manifest) {
|
|
1444
|
+
return Object.values(manifest.props).some((field) => field.type === "richText");
|
|
1445
|
+
}
|
|
1446
|
+
function createBlocksRenderer(ctx) {
|
|
1447
|
+
return (value) => renderComposition(value, ctx);
|
|
1448
|
+
}
|
|
1449
|
+
function manifestConsumesRelations(manifest) {
|
|
1450
|
+
return Object.values(manifest.props).some((field) => field.type === "relation");
|
|
1451
|
+
}
|
|
1452
|
+
function relationRefOf(value) {
|
|
1453
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1454
|
+
const { collection, id } = value;
|
|
1455
|
+
return typeof collection === "string" && collection !== "" && typeof id === "string" && id !== "" ? { collection, id } : null;
|
|
1456
|
+
}
|
|
1457
|
+
function relationKey(ref) {
|
|
1458
|
+
return `${ref.collection}:${ref.id}`;
|
|
1459
|
+
}
|
|
1460
|
+
function nodeRelationKeys(node, manifest) {
|
|
1461
|
+
const keys = [];
|
|
1462
|
+
for (const [name, value] of Object.entries(node.props ?? {})) {
|
|
1463
|
+
if (value.kind !== "static") continue;
|
|
1464
|
+
const spec = manifest.props[name];
|
|
1465
|
+
if (spec?.type !== "relation") continue;
|
|
1466
|
+
const refs = spec.cardinality === "many" ? Array.isArray(value.value) ? value.value : [] : [value.value];
|
|
1467
|
+
for (const raw of refs) {
|
|
1468
|
+
const ref = relationRefOf(raw);
|
|
1469
|
+
if (ref) keys.push(relationKey(ref));
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
return keys;
|
|
1473
|
+
}
|
|
1474
|
+
function resolveRelationProp(cardinality, value, ctx) {
|
|
1475
|
+
const resolveOne = (raw) => {
|
|
1476
|
+
const ref = relationRefOf(raw);
|
|
1477
|
+
if (!ref) return null;
|
|
1478
|
+
if (ctx.transcludePath?.includes(relationKey(ref))) return null;
|
|
1479
|
+
return ctx.resolveRelation?.(ref) ?? null;
|
|
1480
|
+
};
|
|
1481
|
+
if (cardinality === "many") {
|
|
1482
|
+
const refs = Array.isArray(value) ? value : [];
|
|
1483
|
+
const out = [];
|
|
1484
|
+
for (const raw of refs) {
|
|
1485
|
+
const target = resolveOne(raw);
|
|
1486
|
+
if (target) out.push(target);
|
|
1487
|
+
}
|
|
1488
|
+
return out;
|
|
1489
|
+
}
|
|
1490
|
+
return resolveOne(value) ?? void 0;
|
|
1491
|
+
}
|
|
1492
|
+
function withContent(ctx, rendered) {
|
|
1493
|
+
if (ctx.content)
|
|
1494
|
+
return /* @__PURE__ */ jsx(ContentClientProvider, { client: ctx.content, children: rendered });
|
|
1495
|
+
return rendered;
|
|
1496
|
+
}
|
|
1497
|
+
function resolveRender(node, manifest, ctx) {
|
|
1498
|
+
const props = resolveProps(node, manifest, ctx);
|
|
1499
|
+
if (manifestConsumesEmbeds(manifest)) props.renderEmbed = createEmbedRenderer(ctx);
|
|
1500
|
+
if (manifestConsumesRelations(manifest)) {
|
|
1501
|
+
props.renderBlocks = createBlocksRenderer({
|
|
1502
|
+
...ctx,
|
|
1503
|
+
transcludePath: [...ctx.transcludePath ?? [], ...nodeRelationKeys(node, manifest)]
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
if (manifest.listing) {
|
|
1507
|
+
const query = listingQueryOf(node, manifest.listing);
|
|
1508
|
+
props[manifest.listing.prop] = query && ctx.resolveListing?.(query) || [];
|
|
1509
|
+
}
|
|
1510
|
+
if (manifest.iterate) {
|
|
1511
|
+
return resolveRepeater(node, manifest, manifest.iterate, props, ctx);
|
|
1512
|
+
}
|
|
1513
|
+
if (manifest.composition) {
|
|
1514
|
+
return resolveComposition(manifest.composition, props, ctx);
|
|
1515
|
+
}
|
|
1516
|
+
const { children, slotProps } = renderSlots(node, manifest, ctx);
|
|
1517
|
+
return { props, slotProps, children };
|
|
1518
|
+
}
|
|
1519
|
+
function resolveComposition(spec, props, ctx) {
|
|
1520
|
+
return { props, slotProps: {}, children: renderComposition(props[spec.valueProp], ctx) };
|
|
1521
|
+
}
|
|
1522
|
+
function resolveProps(node, manifest, ctx) {
|
|
1523
|
+
const out = {};
|
|
1524
|
+
const entries = Object.entries(node.props ?? {});
|
|
1525
|
+
const isSpread = (value) => value.kind === "binding" && value.binding.mode === "spread";
|
|
1526
|
+
for (const [name, value] of entries)
|
|
1527
|
+
if (isSpread(value)) resolveProp(name, value, manifest, ctx, out);
|
|
1528
|
+
for (const [name, value] of entries)
|
|
1529
|
+
if (!isSpread(value)) resolveProp(name, value, manifest, ctx, out);
|
|
1530
|
+
return out;
|
|
1531
|
+
}
|
|
1532
|
+
function isUrlLike(value) {
|
|
1533
|
+
return /^(https?:)?\/\//.test(value) || value.startsWith("/") || value.startsWith("data:") || value.startsWith("blob:");
|
|
1534
|
+
}
|
|
1535
|
+
function collectReferencedAssetIds(node, registry, into = /* @__PURE__ */ new Set()) {
|
|
1536
|
+
const manifest = registry.getManifest(node.componentId);
|
|
1537
|
+
if (manifest) {
|
|
1538
|
+
for (const [name, value] of Object.entries(node.props ?? {})) {
|
|
1539
|
+
if (value.kind !== "static") continue;
|
|
1540
|
+
const spec = manifest.props[name];
|
|
1541
|
+
if (spec?.type === "asset") {
|
|
1542
|
+
const raw = value.value;
|
|
1543
|
+
if (typeof raw === "string" && raw !== "" && !isUrlLike(raw)) into.add(raw);
|
|
1544
|
+
} else if (spec?.type === "object") {
|
|
1545
|
+
collectObjectPropAssetIds(spec.fields, spec.cardinality, value.value, into);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
for (const children of Object.values(node.slots ?? {})) {
|
|
1550
|
+
for (const child of children) collectReferencedAssetIds(child, registry, into);
|
|
1551
|
+
}
|
|
1552
|
+
return into;
|
|
1553
|
+
}
|
|
1554
|
+
function collectReferencedRelations(nodes, registry, resolveRelation, into = {}, refKeys) {
|
|
1555
|
+
for (const node of nodes) {
|
|
1556
|
+
const manifest = registry.getManifest(node.componentId);
|
|
1557
|
+
if (manifest) {
|
|
1558
|
+
for (const [name, value] of Object.entries(node.props ?? {})) {
|
|
1559
|
+
if (value.kind !== "static") continue;
|
|
1560
|
+
const spec = manifest.props[name];
|
|
1561
|
+
if (spec?.type === "relation") {
|
|
1562
|
+
const refs = spec.cardinality === "many" ? Array.isArray(value.value) ? value.value : [] : [value.value];
|
|
1563
|
+
for (const raw of refs) {
|
|
1564
|
+
const ref = relationRefOf(raw);
|
|
1565
|
+
if (!ref) continue;
|
|
1566
|
+
const key = relationKey(ref);
|
|
1567
|
+
refKeys?.add(key);
|
|
1568
|
+
if (key in into) continue;
|
|
1569
|
+
const target = resolveRelation(ref);
|
|
1570
|
+
if (!target) continue;
|
|
1571
|
+
into[key] = target;
|
|
1572
|
+
for (const fieldValue of Object.values(target)) {
|
|
1573
|
+
const childNodes = compositionRoots(fieldValue);
|
|
1574
|
+
if (childNodes.length > 0) {
|
|
1575
|
+
collectReferencedRelations(childNodes, registry, resolveRelation, into, refKeys);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
} else if (spec?.type === "object" && value.value != null) {
|
|
1580
|
+
collectObjectPropRelations(
|
|
1581
|
+
spec.fields,
|
|
1582
|
+
spec.cardinality,
|
|
1583
|
+
value.value,
|
|
1584
|
+
resolveRelation,
|
|
1585
|
+
into,
|
|
1586
|
+
refKeys
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
if (manifest.composition) {
|
|
1591
|
+
const composed = node.props?.[manifest.composition.valueProp];
|
|
1592
|
+
if (composed?.kind === "static") {
|
|
1593
|
+
collectReferencedRelations(
|
|
1594
|
+
compositionRoots(composed.value),
|
|
1595
|
+
registry,
|
|
1596
|
+
resolveRelation,
|
|
1597
|
+
into,
|
|
1598
|
+
refKeys
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
for (const children of Object.values(node.slots ?? {})) {
|
|
1604
|
+
collectReferencedRelations(children, registry, resolveRelation, into, refKeys);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
return into;
|
|
1608
|
+
}
|
|
1609
|
+
function listingQueryOf(node, spec) {
|
|
1610
|
+
const pick = node.props?.[spec.filter.fromProp];
|
|
1611
|
+
if (!pick || pick.kind !== "static") return null;
|
|
1612
|
+
const ref = relationRefOf(pick.value);
|
|
1613
|
+
const id = ref ? ref.id : typeof pick.value === "string" && pick.value !== "" ? pick.value : null;
|
|
1614
|
+
if (id === null) return null;
|
|
1615
|
+
const query = { collection: spec.collection, where: { [spec.filter.field]: id } };
|
|
1616
|
+
if (spec.sort) query.sort = spec.sort;
|
|
1617
|
+
if (spec.limit !== void 0) query.limit = spec.limit;
|
|
1618
|
+
return query;
|
|
1619
|
+
}
|
|
1620
|
+
function serializeListingQuery(query) {
|
|
1621
|
+
const where = {};
|
|
1622
|
+
for (const key of Object.keys(query.where).sort()) {
|
|
1623
|
+
const value = query.where[key];
|
|
1624
|
+
if (value !== void 0) where[key] = value;
|
|
1625
|
+
}
|
|
1626
|
+
return JSON.stringify({
|
|
1627
|
+
collection: query.collection,
|
|
1628
|
+
where,
|
|
1629
|
+
sort: query.sort ? { field: query.sort.field, direction: query.sort.direction ?? "asc" } : null,
|
|
1630
|
+
limit: query.limit ?? null
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
function collectReferencedListings(nodes, registry, runListing, resolveRelation, into = {}, visitedRelations = /* @__PURE__ */ new Set()) {
|
|
1634
|
+
for (const node of nodes) {
|
|
1635
|
+
const manifest = registry.getManifest(node.componentId);
|
|
1636
|
+
if (manifest?.listing) {
|
|
1637
|
+
const query = listingQueryOf(node, manifest.listing);
|
|
1638
|
+
if (query) {
|
|
1639
|
+
const key = serializeListingQuery(query);
|
|
1640
|
+
if (!(key in into)) into[key] = runListing(query);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
if (manifest) {
|
|
1644
|
+
for (const [name, value] of Object.entries(node.props ?? {})) {
|
|
1645
|
+
if (value.kind !== "static") continue;
|
|
1646
|
+
const spec = manifest.props[name];
|
|
1647
|
+
if (spec?.type !== "relation") continue;
|
|
1648
|
+
const refs = spec.cardinality === "many" ? Array.isArray(value.value) ? value.value : [] : [value.value];
|
|
1649
|
+
for (const raw of refs) {
|
|
1650
|
+
const ref = relationRefOf(raw);
|
|
1651
|
+
if (!ref) continue;
|
|
1652
|
+
const relKey = relationKey(ref);
|
|
1653
|
+
if (visitedRelations.has(relKey)) continue;
|
|
1654
|
+
visitedRelations.add(relKey);
|
|
1655
|
+
const target = resolveRelation(ref);
|
|
1656
|
+
if (!target) continue;
|
|
1657
|
+
for (const fieldValue of Object.values(target)) {
|
|
1658
|
+
const childNodes = compositionRoots(fieldValue);
|
|
1659
|
+
if (childNodes.length > 0) {
|
|
1660
|
+
collectReferencedListings(
|
|
1661
|
+
childNodes,
|
|
1662
|
+
registry,
|
|
1663
|
+
runListing,
|
|
1664
|
+
resolveRelation,
|
|
1665
|
+
into,
|
|
1666
|
+
visitedRelations
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
if (manifest?.composition) {
|
|
1674
|
+
const composed = node.props?.[manifest.composition.valueProp];
|
|
1675
|
+
if (composed?.kind === "static") {
|
|
1676
|
+
collectReferencedListings(
|
|
1677
|
+
compositionRoots(composed.value),
|
|
1678
|
+
registry,
|
|
1679
|
+
runListing,
|
|
1680
|
+
resolveRelation,
|
|
1681
|
+
into,
|
|
1682
|
+
visitedRelations
|
|
1683
|
+
);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
for (const children of Object.values(node.slots ?? {})) {
|
|
1687
|
+
collectReferencedListings(children, registry, runListing, resolveRelation, into, visitedRelations);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
return into;
|
|
1691
|
+
}
|
|
1692
|
+
function collectObjectPropAssetIds(fields, cardinality, value, into) {
|
|
1693
|
+
const items = cardinality === "many" ? Array.isArray(value) ? value : [] : [value];
|
|
1694
|
+
for (const item of items) {
|
|
1695
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
|
1696
|
+
const record = item;
|
|
1697
|
+
for (const sub of fields) {
|
|
1698
|
+
const sv = record[sub.name];
|
|
1699
|
+
if (sub.type === "asset" && typeof sv === "string" && sv !== "" && !isUrlLike(sv)) into.add(sv);
|
|
1700
|
+
else if (sub.type === "object" && sv != null)
|
|
1701
|
+
collectObjectPropAssetIds(sub.fields, sub.cardinality, sv, into);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
function collectObjectPropRelations(fields, cardinality, value, resolveRelation, into, refKeys) {
|
|
1706
|
+
const items = cardinality === "many" ? Array.isArray(value) ? value : [] : [value];
|
|
1707
|
+
for (const item of items) {
|
|
1708
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
|
1709
|
+
const record = item;
|
|
1710
|
+
for (const sub of fields) {
|
|
1711
|
+
const sv = record[sub.name];
|
|
1712
|
+
if (sub.type === "relation" && sv != null) {
|
|
1713
|
+
const refs = sub.cardinality === "many" ? Array.isArray(sv) ? sv : [] : [sv];
|
|
1714
|
+
for (const raw of refs) {
|
|
1715
|
+
const ref = relationRefOf(raw);
|
|
1716
|
+
if (!ref) continue;
|
|
1717
|
+
const key = relationKey(ref);
|
|
1718
|
+
refKeys?.add(key);
|
|
1719
|
+
if (key in into) continue;
|
|
1720
|
+
const target = resolveRelation(ref);
|
|
1721
|
+
if (target) into[key] = target;
|
|
1722
|
+
}
|
|
1723
|
+
} else if (sub.type === "object" && sv != null) {
|
|
1724
|
+
collectObjectPropRelations(sub.fields, sub.cardinality, sv, resolveRelation, into, refKeys);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
function resolveObjectProps(fields, cardinality, value, ctx) {
|
|
1730
|
+
const items = cardinality === "many" ? Array.isArray(value) ? value : [] : [value];
|
|
1731
|
+
const resolvedItems = items.map((item) => {
|
|
1732
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return item;
|
|
1733
|
+
const next = { ...item };
|
|
1734
|
+
for (const sub of fields) {
|
|
1735
|
+
const sv = next[sub.name];
|
|
1736
|
+
if (sub.type === "asset" && typeof sv === "string") {
|
|
1737
|
+
if (sv === "" || isUrlLike(sv)) continue;
|
|
1738
|
+
const resolved = ctx.resolveAsset?.(sv);
|
|
1739
|
+
if (resolved) next[sub.name] = resolved;
|
|
1740
|
+
else delete next[sub.name];
|
|
1741
|
+
} else if (sub.type === "relation" && sv != null) {
|
|
1742
|
+
if (!ctx.resolveRelation) continue;
|
|
1743
|
+
const resolved = resolveRelationProp(sub.cardinality, sv, ctx);
|
|
1744
|
+
if (resolved !== void 0) next[sub.name] = resolved;
|
|
1745
|
+
else delete next[sub.name];
|
|
1746
|
+
} else if (sub.type === "object" && sv != null) {
|
|
1747
|
+
next[sub.name] = resolveObjectProps(sub.fields, sub.cardinality, sv, ctx);
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
return next;
|
|
1751
|
+
});
|
|
1752
|
+
return cardinality === "many" ? resolvedItems : resolvedItems[0];
|
|
1753
|
+
}
|
|
1754
|
+
function resolveProp(name, value, manifest, ctx, out) {
|
|
1755
|
+
const spec = manifest.props[name];
|
|
1756
|
+
if (value.kind === "static") {
|
|
1757
|
+
const validate = propValueValidator(manifest, name);
|
|
1758
|
+
if (validate && !validate(value.value)) {
|
|
1759
|
+
if (spec?.default !== void 0) out[name] = spec.default;
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
if (spec?.type === "asset" && typeof value.value === "string") {
|
|
1763
|
+
const raw = value.value;
|
|
1764
|
+
if (raw === "" || isUrlLike(raw)) {
|
|
1765
|
+
out[name] = raw;
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
const resolved2 = ctx.resolveAsset?.(raw);
|
|
1769
|
+
if (resolved2) out[name] = resolved2;
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
if (spec?.type === "object" && value.value != null) {
|
|
1773
|
+
out[name] = resolveObjectProps(spec.fields, spec.cardinality, value.value, ctx);
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
if (spec?.type === "relation") {
|
|
1777
|
+
if (!ctx.resolveRelation) {
|
|
1778
|
+
out[name] = value.value;
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
const resolved2 = resolveRelationProp(spec.cardinality, value.value, ctx);
|
|
1782
|
+
if (resolved2 !== void 0) out[name] = resolved2;
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
out[name] = value.value;
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
const resolved = ctx.resolveBinding(value.binding, ctx.item);
|
|
1789
|
+
if (value.binding.mode === "spread") {
|
|
1790
|
+
if (resolved && typeof resolved === "object" && !Array.isArray(resolved)) {
|
|
1791
|
+
Object.assign(out, resolved);
|
|
1792
|
+
}
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
out[name] = resolved;
|
|
1796
|
+
}
|
|
1797
|
+
function renderSlots(node, manifest, ctx) {
|
|
1798
|
+
const slots = node.slots ?? {};
|
|
1799
|
+
const slotProps = {};
|
|
1800
|
+
let children = renderSlotChildren(slots[DEFAULT_SLOT], ctx);
|
|
1801
|
+
const declaredSlots = manifestSlots(manifest);
|
|
1802
|
+
for (const slotSpec of declaredSlots) {
|
|
1803
|
+
if (slotSpec.name === DEFAULT_SLOT) continue;
|
|
1804
|
+
const nodes = slots[slotSpec.name];
|
|
1805
|
+
if ((!nodes || nodes.length === 0) && slotSpec.required) {
|
|
1806
|
+
slotProps[slotSpec.name] = /* @__PURE__ */ jsx(MissingSlotPlaceholder, { nodeId: node.id, slot: slotSpec.name });
|
|
1807
|
+
continue;
|
|
1808
|
+
}
|
|
1809
|
+
slotProps[slotSpec.name] = renderSlotChildren(nodes, ctx);
|
|
1810
|
+
}
|
|
1811
|
+
const declared = new Set(declaredSlots.map((s) => s.name));
|
|
1812
|
+
for (const [name, nodes] of Object.entries(slots)) {
|
|
1813
|
+
if (name === DEFAULT_SLOT || declared.has(name)) continue;
|
|
1814
|
+
slotProps[name] = renderSlotChildren(nodes, ctx);
|
|
1815
|
+
}
|
|
1816
|
+
const defaultSpec = declaredSlots.find((s) => s.name === DEFAULT_SLOT);
|
|
1817
|
+
const defaultNodes = slots[DEFAULT_SLOT];
|
|
1818
|
+
if (defaultSpec?.required && (!defaultNodes || defaultNodes.length === 0)) {
|
|
1819
|
+
children = /* @__PURE__ */ jsx(MissingSlotPlaceholder, { nodeId: node.id, slot: DEFAULT_SLOT });
|
|
1820
|
+
}
|
|
1821
|
+
return { children, slotProps };
|
|
1822
|
+
}
|
|
1823
|
+
function renderSlotChildren(nodes, ctx) {
|
|
1824
|
+
if (!nodes || nodes.length === 0) return null;
|
|
1825
|
+
return /* @__PURE__ */ jsx(Fragment$1, { children: nodes.map((child) => /* @__PURE__ */ jsx(Fragment$1, { children: renderNode(child, ctx) }, child.id)) });
|
|
1826
|
+
}
|
|
1827
|
+
function resolveRepeater(node, manifest, iterate, props, ctx) {
|
|
1828
|
+
const itemsValue = props[iterate.itemsProp];
|
|
1829
|
+
const items = Array.isArray(itemsValue) ? itemsValue : [];
|
|
1830
|
+
const template = node.slots?.[iterate.itemSlot];
|
|
1831
|
+
const slotSpec = manifest.slots.find((s) => s.name === iterate.itemSlot);
|
|
1832
|
+
if ((!template || template.length === 0) && slotSpec?.required) {
|
|
1833
|
+
return {
|
|
1834
|
+
props,
|
|
1835
|
+
slotProps: {},
|
|
1836
|
+
children: /* @__PURE__ */ jsx(MissingSlotPlaceholder, { nodeId: node.id, slot: iterate.itemSlot })
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
const rendered = items.map((item, index) => {
|
|
1840
|
+
const itemCtx = { ...ctx, item: { item, index } };
|
|
1841
|
+
return /* @__PURE__ */ jsx(Fragment$1, { children: (template ?? []).map((child) => (
|
|
1842
|
+
// Keyed per template node for the same reason as renderSlotChildren.
|
|
1843
|
+
/* @__PURE__ */ jsx(Fragment$1, { children: renderNode(child, itemCtx) }, child.id)
|
|
1844
|
+
)) }, index);
|
|
1845
|
+
});
|
|
1846
|
+
return { props, slotProps: {}, children: /* @__PURE__ */ jsx(Fragment$1, { children: rendered }) };
|
|
1847
|
+
}
|
|
1848
|
+
function renderComposition(value, ctx) {
|
|
1849
|
+
const roots = compositionRoots(value);
|
|
1850
|
+
if (roots.length === 0) return null;
|
|
1851
|
+
return withContent(
|
|
1852
|
+
ctx,
|
|
1853
|
+
/* @__PURE__ */ jsx(Fragment$1, { children: roots.map((root) => /* @__PURE__ */ jsx(Fragment$1, { children: renderNode(root, ctx) }, root.id)) })
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1856
|
+
function compositionRoots(value) {
|
|
1857
|
+
if (Array.isArray(value)) return value.filter(isComponentNode);
|
|
1858
|
+
if (isComponentNode(value)) return [value];
|
|
1859
|
+
return [];
|
|
1860
|
+
}
|
|
1861
|
+
function isComponentNode(value) {
|
|
1862
|
+
return typeof value === "object" && value !== null && typeof value.id === "string" && typeof value.componentId === "string";
|
|
1863
|
+
}
|
|
1864
|
+
function renderCompositionInLayout(graph, layoutId, value, ctx) {
|
|
1865
|
+
const layout = layoutId ? graph.layouts.find((l) => l.id === layoutId) : void 0;
|
|
1866
|
+
if (!layout) return withContent(ctx, renderComposition(value, ctx));
|
|
1867
|
+
const root = injectNodesIntoOutlet(layout.root, compositionRoots(value));
|
|
1868
|
+
return withContent(ctx, renderNode(root, ctx));
|
|
1869
|
+
}
|
|
1870
|
+
function injectNodesIntoOutlet(root, nodes) {
|
|
1871
|
+
return {
|
|
1872
|
+
...root,
|
|
1873
|
+
slots: {
|
|
1874
|
+
...root.slots ?? {},
|
|
1875
|
+
[OUTLET_SLOT]: nodes
|
|
1876
|
+
}
|
|
1877
|
+
};
|
|
1878
|
+
}
|
|
1879
|
+
function messageOf(error) {
|
|
1880
|
+
return error instanceof Error ? error.message : String(error);
|
|
1881
|
+
}
|
|
1882
|
+
var richTextAttrsSchema = z.record(z.string(), jsonValueSchema);
|
|
1883
|
+
var richTextMarkSchema = z.object({
|
|
1884
|
+
type: z.string().min(1),
|
|
1885
|
+
attrs: richTextAttrsSchema.optional()
|
|
1886
|
+
});
|
|
1887
|
+
var richTextNodeSchema = z.lazy(
|
|
1888
|
+
() => z.object({
|
|
1889
|
+
type: z.string().min(1),
|
|
1890
|
+
attrs: richTextAttrsSchema.optional(),
|
|
1891
|
+
content: z.array(richTextNodeSchema).optional(),
|
|
1892
|
+
marks: z.array(richTextMarkSchema).optional(),
|
|
1893
|
+
text: z.string().optional()
|
|
1894
|
+
})
|
|
1895
|
+
);
|
|
1896
|
+
var richTextDocSchema = z.object({
|
|
1897
|
+
type: z.literal("doc"),
|
|
1898
|
+
content: z.array(richTextNodeSchema).default([])
|
|
1899
|
+
});
|
|
1900
|
+
var richTextValueSchema = z.object({
|
|
1901
|
+
/** Schema version this value was written under. */
|
|
1902
|
+
version: z.number().int().nonnegative(),
|
|
1903
|
+
doc: richTextDocSchema
|
|
1904
|
+
});
|
|
1905
|
+
function parseRichTextValue(input) {
|
|
1906
|
+
let json;
|
|
1907
|
+
try {
|
|
1908
|
+
json = typeof input === "string" ? JSON.parse(input) : input;
|
|
1909
|
+
} catch {
|
|
1910
|
+
json = void 0;
|
|
1911
|
+
}
|
|
1912
|
+
const result = richTextValueSchema.safeParse(json);
|
|
1913
|
+
return result.success ? { ok: true, value: result.data } : { ok: false, error: result.error };
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
// ../rich-text/src/embed.ts
|
|
1917
|
+
var COMPONENT_EMBED_NODE_TYPE = "componentEmbed";
|
|
1918
|
+
function isComponentEmbedNode(node) {
|
|
1919
|
+
return node.type === COMPONENT_EMBED_NODE_TYPE;
|
|
1920
|
+
}
|
|
1921
|
+
function embeddedComponentNode(node) {
|
|
1922
|
+
if (!isComponentEmbedNode(node)) return void 0;
|
|
1923
|
+
const raw = node.attrs?.node;
|
|
1924
|
+
const parsed = componentNodeSchema.safeParse(raw);
|
|
1925
|
+
return parsed.success ? parsed.data : void 0;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
// ../rich-text/src/registry.tsx
|
|
1929
|
+
function createRichTextRegistry(definitions = []) {
|
|
1930
|
+
const byType = /* @__PURE__ */ new Map();
|
|
1931
|
+
for (const def of definitions) {
|
|
1932
|
+
byType.set(def.type, def);
|
|
1933
|
+
}
|
|
1934
|
+
return {
|
|
1935
|
+
has: (type) => byType.has(type),
|
|
1936
|
+
get: (type) => byType.get(type),
|
|
1937
|
+
isInline: (type) => byType.get(type)?.group === "inline"
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
var emptyRichTextRegistry = createRichTextRegistry();
|
|
1941
|
+
function attrString(value) {
|
|
1942
|
+
return typeof value === "string" ? value : void 0;
|
|
1943
|
+
}
|
|
1944
|
+
function attrNumber(value) {
|
|
1945
|
+
return typeof value === "number" ? value : void 0;
|
|
1946
|
+
}
|
|
1947
|
+
function applyMarks(content, marks, key) {
|
|
1948
|
+
if (!marks || marks.length === 0) return content;
|
|
1949
|
+
return marks.reduce((acc, mark, i) => {
|
|
1950
|
+
const markKey = `${key}-m${i}`;
|
|
1951
|
+
switch (mark.type) {
|
|
1952
|
+
case "bold":
|
|
1953
|
+
return createElement("strong", { key: markKey }, acc);
|
|
1954
|
+
case "italic":
|
|
1955
|
+
return createElement("em", { key: markKey }, acc);
|
|
1956
|
+
case "code":
|
|
1957
|
+
return createElement("code", { key: markKey }, acc);
|
|
1958
|
+
case "strike":
|
|
1959
|
+
return createElement("s", { key: markKey }, acc);
|
|
1960
|
+
case "underline":
|
|
1961
|
+
return createElement("u", { key: markKey }, acc);
|
|
1962
|
+
case "link": {
|
|
1963
|
+
const href = attrString(mark.attrs?.href) ?? "#";
|
|
1964
|
+
const target = attrString(mark.attrs?.target);
|
|
1965
|
+
const rel = target === "_blank" ? "noopener noreferrer" : attrString(mark.attrs?.rel);
|
|
1966
|
+
return createElement("a", { key: markKey, href, ...target ? { target } : {}, ...rel ? { rel } : {} }, acc);
|
|
1967
|
+
}
|
|
1968
|
+
default:
|
|
1969
|
+
return createElement("span", { key: markKey, "data-unknown-mark": mark.type }, acc);
|
|
1970
|
+
}
|
|
1971
|
+
}, content);
|
|
1972
|
+
}
|
|
1973
|
+
function invalidEmbedFallback(key) {
|
|
1974
|
+
return createElement(
|
|
1975
|
+
"div",
|
|
1976
|
+
{ key, "data-invalid-embed": "", className: "rich-text-invalid-embed", role: "note" },
|
|
1977
|
+
"Invalid embedded component."
|
|
1978
|
+
);
|
|
1979
|
+
}
|
|
1980
|
+
function renderChildren(nodes, ctx, path) {
|
|
1981
|
+
if (!nodes) return [];
|
|
1982
|
+
return nodes.map((child, i) => renderNode2(child, ctx, [...path, i]));
|
|
1983
|
+
}
|
|
1984
|
+
function unknownFallback(node, key) {
|
|
1985
|
+
return createElement(
|
|
1986
|
+
"div",
|
|
1987
|
+
{ key, "data-unknown-node": node.type, className: "rich-text-unknown", role: "note" },
|
|
1988
|
+
`Unknown content block: ${node.type}`
|
|
1989
|
+
);
|
|
1990
|
+
}
|
|
1991
|
+
function renderNode2(node, ctx, path) {
|
|
1992
|
+
const key = `n-${path.join("-")}`;
|
|
1993
|
+
if (node.type === "text") {
|
|
1994
|
+
return applyMarks(node.text ?? "", node.marks, key);
|
|
1995
|
+
}
|
|
1996
|
+
switch (node.type) {
|
|
1997
|
+
case "paragraph":
|
|
1998
|
+
return createElement("p", { key }, ...renderChildren(node.content, ctx, path));
|
|
1999
|
+
case "heading": {
|
|
2000
|
+
const level = attrNumber(node.attrs?.level);
|
|
2001
|
+
const clamped = level && level >= 1 && level <= 6 ? level : 1;
|
|
2002
|
+
return createElement(`h${clamped}`, { key }, ...renderChildren(node.content, ctx, path));
|
|
2003
|
+
}
|
|
2004
|
+
case "bulletList":
|
|
2005
|
+
return createElement("ul", { key }, ...renderChildren(node.content, ctx, path));
|
|
2006
|
+
case "orderedList":
|
|
2007
|
+
return createElement("ol", { key }, ...renderChildren(node.content, ctx, path));
|
|
2008
|
+
case "listItem":
|
|
2009
|
+
return createElement("li", { key }, ...renderChildren(node.content, ctx, path));
|
|
2010
|
+
case "blockquote":
|
|
2011
|
+
return createElement("blockquote", { key }, ...renderChildren(node.content, ctx, path));
|
|
2012
|
+
case "codeBlock": {
|
|
2013
|
+
const language = attrString(node.attrs?.language);
|
|
2014
|
+
return createElement(
|
|
2015
|
+
"pre",
|
|
2016
|
+
{ key },
|
|
2017
|
+
createElement(
|
|
2018
|
+
"code",
|
|
2019
|
+
language ? { className: `language-${language}` } : {},
|
|
2020
|
+
...renderChildren(node.content, ctx, path)
|
|
2021
|
+
)
|
|
2022
|
+
);
|
|
2023
|
+
}
|
|
2024
|
+
case "hardBreak":
|
|
2025
|
+
return createElement("br", { key });
|
|
2026
|
+
case "horizontalRule":
|
|
2027
|
+
return createElement("hr", { key });
|
|
2028
|
+
case "image": {
|
|
2029
|
+
const src = attrString(node.attrs?.src) ?? "";
|
|
2030
|
+
const alt = attrString(node.attrs?.alt) ?? "";
|
|
2031
|
+
const title = attrString(node.attrs?.title);
|
|
2032
|
+
return createElement("img", { key, src, alt, ...title ? { title } : {} });
|
|
2033
|
+
}
|
|
2034
|
+
case "embed": {
|
|
2035
|
+
const src = attrString(node.attrs?.src) ?? "";
|
|
2036
|
+
const title = attrString(node.attrs?.title) ?? "Embedded content";
|
|
2037
|
+
return createElement("iframe", {
|
|
2038
|
+
key,
|
|
2039
|
+
src,
|
|
2040
|
+
title,
|
|
2041
|
+
loading: "lazy",
|
|
2042
|
+
"data-embed-provider": attrString(node.attrs?.provider) ?? void 0
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
2045
|
+
case COMPONENT_EMBED_NODE_TYPE: {
|
|
2046
|
+
const embedded = embeddedComponentNode(node);
|
|
2047
|
+
if (!embedded) {
|
|
2048
|
+
ctx.onIssue?.({
|
|
2049
|
+
code: "invalid-embed",
|
|
2050
|
+
message: "Component embed is missing a valid embedded component node.",
|
|
2051
|
+
nodeType: node.type,
|
|
2052
|
+
path
|
|
2053
|
+
});
|
|
2054
|
+
return invalidEmbedFallback(key);
|
|
2055
|
+
}
|
|
2056
|
+
if (!ctx.renderEmbed) return invalidEmbedFallback(key);
|
|
2057
|
+
return createElement(Fragment$1, { key }, ctx.renderEmbed(embedded));
|
|
2058
|
+
}
|
|
2059
|
+
default: {
|
|
2060
|
+
const def = ctx.registry.get(node.type);
|
|
2061
|
+
if (def) {
|
|
2062
|
+
const children = node.content && node.content.length > 0 ? renderChildren(node.content, ctx, path) : void 0;
|
|
2063
|
+
const element = def.render({ node, children });
|
|
2064
|
+
return element ? createElement(Fragment$1, { key }, element) : null;
|
|
2065
|
+
}
|
|
2066
|
+
ctx.onIssue?.({
|
|
2067
|
+
code: "unknown-node",
|
|
2068
|
+
message: `No renderer registered for rich-text node type "${node.type}".`,
|
|
2069
|
+
nodeType: node.type,
|
|
2070
|
+
path
|
|
2071
|
+
});
|
|
2072
|
+
return unknownFallback(node, key);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
function renderRichTextDoc(doc, options = {}) {
|
|
2077
|
+
const ctx = {
|
|
2078
|
+
registry: options.registry ?? emptyRichTextRegistry,
|
|
2079
|
+
renderEmbed: options.renderEmbed,
|
|
2080
|
+
onIssue: options.onIssue
|
|
2081
|
+
};
|
|
2082
|
+
const children = renderChildren(doc.content, ctx, []);
|
|
2083
|
+
return createElement(
|
|
2084
|
+
"div",
|
|
2085
|
+
{ className: options.className ? `rich-text ${options.className}` : "rich-text" },
|
|
2086
|
+
...children
|
|
2087
|
+
);
|
|
2088
|
+
}
|
|
2089
|
+
function renderRichTextValue(value, options = {}) {
|
|
2090
|
+
return renderRichTextDoc(value.doc, options);
|
|
2091
|
+
}
|
|
2092
|
+
function str(value) {
|
|
2093
|
+
return typeof value === "string" ? value : "";
|
|
2094
|
+
}
|
|
2095
|
+
var calloutBlock = {
|
|
2096
|
+
type: "callout",
|
|
2097
|
+
group: "block",
|
|
2098
|
+
render: ({ node, children }) => createElement(
|
|
2099
|
+
"aside",
|
|
2100
|
+
{ className: "callout", "data-tone": str(node.attrs?.tone) || "info", role: "note" },
|
|
2101
|
+
children
|
|
2102
|
+
)
|
|
2103
|
+
};
|
|
2104
|
+
var mentionInline = {
|
|
2105
|
+
type: "mention",
|
|
2106
|
+
group: "inline",
|
|
2107
|
+
render: ({ node }) => createElement(
|
|
2108
|
+
"span",
|
|
2109
|
+
{ className: "mention", "data-mention-id": str(node.attrs?.id) },
|
|
2110
|
+
`@${str(node.attrs?.label)}`
|
|
2111
|
+
)
|
|
2112
|
+
};
|
|
2113
|
+
var sampleRichTextRegistry = createRichTextRegistry([
|
|
2114
|
+
calloutBlock,
|
|
2115
|
+
mentionInline
|
|
2116
|
+
]);
|
|
2117
|
+
var richTextPrimitiveRegistry = sampleRichTextRegistry;
|
|
2118
|
+
function asJson(value) {
|
|
2119
|
+
return value;
|
|
2120
|
+
}
|
|
2121
|
+
var RICH_TEXT_DEFAULT_VALUE = {
|
|
2122
|
+
version: 1,
|
|
2123
|
+
doc: {
|
|
2124
|
+
type: "doc",
|
|
2125
|
+
content: [
|
|
2126
|
+
{
|
|
2127
|
+
type: "paragraph",
|
|
2128
|
+
content: [{ type: "text", text: "Rich text. Select to format, double-click to edit." }]
|
|
2129
|
+
}
|
|
2130
|
+
]
|
|
2131
|
+
}
|
|
2132
|
+
};
|
|
2133
|
+
var RICH_TEXT_FIXTURE_VALUE = {
|
|
2134
|
+
version: 1,
|
|
2135
|
+
doc: {
|
|
2136
|
+
type: "doc",
|
|
2137
|
+
content: [
|
|
2138
|
+
{ type: "heading", attrs: { level: 3 }, content: [{ type: "text", text: "Rich text block" }] },
|
|
2139
|
+
{
|
|
2140
|
+
type: "paragraph",
|
|
2141
|
+
content: [
|
|
2142
|
+
{ type: "text", text: "Formatted " },
|
|
2143
|
+
{ type: "text", text: "bold", marks: [{ type: "bold" }] },
|
|
2144
|
+
{ type: "text", text: " and " },
|
|
2145
|
+
{ type: "text", text: "italic", marks: [{ type: "italic" }] },
|
|
2146
|
+
{ type: "text", text: " copy with a " },
|
|
2147
|
+
{ type: "text", text: "link", marks: [{ type: "link", attrs: { href: "https://example.com" } }] },
|
|
2148
|
+
{ type: "text", text: "." }
|
|
2149
|
+
]
|
|
2150
|
+
},
|
|
2151
|
+
{
|
|
2152
|
+
type: "callout",
|
|
2153
|
+
attrs: { tone: "info" },
|
|
2154
|
+
content: [{ type: "text", text: "A custom block, rendered by the project registry." }]
|
|
2155
|
+
}
|
|
2156
|
+
]
|
|
2157
|
+
}
|
|
2158
|
+
};
|
|
2159
|
+
function RichText(props) {
|
|
2160
|
+
const renderEmbed = typeof props.renderEmbed === "function" ? props.renderEmbed : void 0;
|
|
2161
|
+
const parsed = parseRichTextValue(props.content ?? RICH_TEXT_DEFAULT_VALUE);
|
|
2162
|
+
if (!parsed.ok) {
|
|
2163
|
+
return /* @__PURE__ */ jsx("div", { "data-ec-primitive": "RichText", className: "rich-text-invalid", role: "note", children: "Invalid rich-text content." });
|
|
2164
|
+
}
|
|
2165
|
+
return /* @__PURE__ */ jsx("div", { "data-ec-primitive": "RichText", children: renderRichTextValue(parsed.value, { registry: richTextPrimitiveRegistry, renderEmbed }) });
|
|
2166
|
+
}
|
|
2167
|
+
var RichTextManifest = defineComponent({
|
|
2168
|
+
id: "base.primitives.RichText",
|
|
2169
|
+
namespace: "base.primitives",
|
|
2170
|
+
title: "Rich Text",
|
|
2171
|
+
category: "content",
|
|
2172
|
+
description: "Formatted long-form content stored in the rich-text format.",
|
|
2173
|
+
props: {
|
|
2174
|
+
content: {
|
|
2175
|
+
// The structured @elytracms/rich-text storage value (EC-159); validated by
|
|
2176
|
+
// the renderer's parse guard rather than a scalar string check.
|
|
2177
|
+
type: "richText",
|
|
2178
|
+
context: "prop",
|
|
2179
|
+
default: asJson(RICH_TEXT_DEFAULT_VALUE),
|
|
2180
|
+
// Rich-text content prop, editable in place on the canvas (EC-159).
|
|
2181
|
+
form: { label: "Content", inlineEditable: true }
|
|
2182
|
+
}
|
|
2183
|
+
},
|
|
2184
|
+
slots: [],
|
|
2185
|
+
fixtures: [
|
|
2186
|
+
{ name: "Default" },
|
|
2187
|
+
{ name: "Formatted", props: { content: asJson(RICH_TEXT_FIXTURE_VALUE) } }
|
|
2188
|
+
]
|
|
2189
|
+
});
|
|
2190
|
+
function asString(value, fallback = "") {
|
|
2191
|
+
return typeof value === "string" ? value : fallback;
|
|
2192
|
+
}
|
|
2193
|
+
function gapCss(value) {
|
|
2194
|
+
if (typeof value === "string") return spacingTokenCss(value);
|
|
2195
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) return `${value * 4}px`;
|
|
2196
|
+
return void 0;
|
|
2197
|
+
}
|
|
2198
|
+
var ALIGN_ITEMS = {
|
|
2199
|
+
start: "flex-start",
|
|
2200
|
+
center: "center",
|
|
2201
|
+
end: "flex-end",
|
|
2202
|
+
stretch: "stretch"
|
|
2203
|
+
};
|
|
2204
|
+
function alignItemsCss(value) {
|
|
2205
|
+
if (value === "start" || value === "center" || value === "end") return ALIGN_ITEMS[value];
|
|
2206
|
+
return void 0;
|
|
2207
|
+
}
|
|
2208
|
+
var SPACING_OPTIONS = SPACING_TOKEN_NAMES.map((name) => ({ label: name, value: name }));
|
|
2209
|
+
var ALIGN_OPTIONS = CONTAINER_ALIGNMENTS.map((name) => ({ label: name, value: name }));
|
|
2210
|
+
function Container(props) {
|
|
2211
|
+
return /* @__PURE__ */ jsxs("div", { "data-ec-primitive": "Container", children: [
|
|
2212
|
+
props.children,
|
|
2213
|
+
props.outlet
|
|
2214
|
+
] });
|
|
2215
|
+
}
|
|
2216
|
+
var ContainerManifest = defineComponent({
|
|
2217
|
+
id: "base.primitives.Container",
|
|
2218
|
+
namespace: "base.primitives",
|
|
2219
|
+
title: "Container",
|
|
2220
|
+
category: "layout",
|
|
2221
|
+
props: {},
|
|
2222
|
+
// The outlet is filled by page injection, so it is not authored-required.
|
|
2223
|
+
slots: [{ name: "children" }, { name: "outlet" }]
|
|
2224
|
+
});
|
|
2225
|
+
function Stack(props) {
|
|
2226
|
+
const style = {
|
|
2227
|
+
display: "flex",
|
|
2228
|
+
flexDirection: props.direction === "horizontal" ? "row" : "column"
|
|
2229
|
+
};
|
|
2230
|
+
const gap = gapCss(props.gap);
|
|
2231
|
+
if (gap !== void 0) style.gap = gap;
|
|
2232
|
+
const alignItems = alignItemsCss(props.align);
|
|
2233
|
+
if (alignItems !== void 0) style.alignItems = alignItems;
|
|
2234
|
+
return /* @__PURE__ */ jsx("div", { "data-ec-primitive": "Stack", style, children: props.children });
|
|
2235
|
+
}
|
|
2236
|
+
var StackManifest = defineComponent({
|
|
2237
|
+
id: "base.primitives.Stack",
|
|
2238
|
+
namespace: "base.primitives",
|
|
2239
|
+
title: "Stack",
|
|
2240
|
+
category: "layout",
|
|
2241
|
+
props: {
|
|
2242
|
+
direction: {
|
|
2243
|
+
type: "select",
|
|
2244
|
+
context: "prop",
|
|
2245
|
+
default: "vertical",
|
|
2246
|
+
options: [
|
|
2247
|
+
{ value: "vertical", label: "Vertical" },
|
|
2248
|
+
{ value: "horizontal", label: "Horizontal" }
|
|
2249
|
+
],
|
|
2250
|
+
form: { label: "Direction" }
|
|
2251
|
+
},
|
|
2252
|
+
gap: {
|
|
2253
|
+
// A design-token spacing step name (EC-161); legacy numeric gaps still render.
|
|
2254
|
+
type: "select",
|
|
2255
|
+
context: "prop",
|
|
2256
|
+
default: "4",
|
|
2257
|
+
options: SPACING_OPTIONS,
|
|
2258
|
+
form: { label: "Gap" }
|
|
2259
|
+
},
|
|
2260
|
+
align: {
|
|
2261
|
+
type: "select",
|
|
2262
|
+
context: "prop",
|
|
2263
|
+
default: "stretch",
|
|
2264
|
+
options: ALIGN_OPTIONS,
|
|
2265
|
+
form: { label: "Align" }
|
|
2266
|
+
}
|
|
2267
|
+
},
|
|
2268
|
+
slots: [{ name: "children" }],
|
|
2269
|
+
designTokens: ["space.gap"]
|
|
2270
|
+
});
|
|
2271
|
+
function asColumns(value) {
|
|
2272
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return GRID_MIN_COLUMNS;
|
|
2273
|
+
return Math.min(Math.max(GRID_MIN_COLUMNS, Math.floor(value)), GRID_MAX_COLUMNS);
|
|
2274
|
+
}
|
|
2275
|
+
var GRID_MOBILE_BREAKPOINT_PX = 640;
|
|
2276
|
+
var GRID_STACK_CSS = `@media (max-width: ${GRID_MOBILE_BREAKPOINT_PX}px){[data-ec-grid-stack]{grid-template-columns:minmax(0,1fr) !important}}`;
|
|
2277
|
+
function Grid(props) {
|
|
2278
|
+
const style = {
|
|
2279
|
+
display: "grid",
|
|
2280
|
+
gridTemplateColumns: `repeat(${asColumns(props.columns)}, minmax(0, 1fr))`
|
|
2281
|
+
};
|
|
2282
|
+
const gap = gapCss(props.gap);
|
|
2283
|
+
if (gap !== void 0) style.gap = gap;
|
|
2284
|
+
const alignItems = alignItemsCss(props.align);
|
|
2285
|
+
if (alignItems !== void 0) style.alignItems = alignItems;
|
|
2286
|
+
const stacksOnMobile = props.stackOnMobile !== false;
|
|
2287
|
+
return /* @__PURE__ */ jsxs(
|
|
2288
|
+
"div",
|
|
2289
|
+
{
|
|
2290
|
+
"data-ec-primitive": "Grid",
|
|
2291
|
+
...stacksOnMobile ? { "data-ec-grid-stack": "" } : {},
|
|
2292
|
+
style,
|
|
2293
|
+
children: [
|
|
2294
|
+
stacksOnMobile ? /* @__PURE__ */ jsx("style", { children: GRID_STACK_CSS }) : null,
|
|
2295
|
+
props.children
|
|
2296
|
+
]
|
|
2297
|
+
}
|
|
2298
|
+
);
|
|
2299
|
+
}
|
|
2300
|
+
var GridManifest = defineComponent({
|
|
2301
|
+
id: "base.primitives.Grid",
|
|
2302
|
+
namespace: "base.primitives",
|
|
2303
|
+
title: "Grid",
|
|
2304
|
+
category: "layout",
|
|
2305
|
+
props: {
|
|
2306
|
+
columns: {
|
|
2307
|
+
type: "number",
|
|
2308
|
+
context: "prop",
|
|
2309
|
+
default: 2,
|
|
2310
|
+
validation: { min: GRID_MIN_COLUMNS, max: GRID_MAX_COLUMNS },
|
|
2311
|
+
form: { label: "Columns" }
|
|
2312
|
+
},
|
|
2313
|
+
gap: {
|
|
2314
|
+
type: "select",
|
|
2315
|
+
context: "prop",
|
|
2316
|
+
default: "4",
|
|
2317
|
+
options: SPACING_OPTIONS,
|
|
2318
|
+
form: { label: "Gap" }
|
|
2319
|
+
},
|
|
2320
|
+
align: {
|
|
2321
|
+
type: "select",
|
|
2322
|
+
context: "prop",
|
|
2323
|
+
default: "stretch",
|
|
2324
|
+
options: ALIGN_OPTIONS,
|
|
2325
|
+
form: { label: "Align" }
|
|
2326
|
+
},
|
|
2327
|
+
stackOnMobile: {
|
|
2328
|
+
// Responsive default (EC-163): stacking is on unless explicitly opted out.
|
|
2329
|
+
type: "boolean",
|
|
2330
|
+
context: "prop",
|
|
2331
|
+
default: true,
|
|
2332
|
+
form: { label: "Stack on phones" }
|
|
2333
|
+
}
|
|
2334
|
+
},
|
|
2335
|
+
slots: [{ name: "children" }],
|
|
2336
|
+
designTokens: ["space.gap"]
|
|
2337
|
+
});
|
|
2338
|
+
function Text(props) {
|
|
2339
|
+
return /* @__PURE__ */ jsx("p", { "data-ec-primitive": "Text", children: asString(props.value) });
|
|
2340
|
+
}
|
|
2341
|
+
var TextManifest = defineComponent({
|
|
2342
|
+
id: "base.primitives.Text",
|
|
2343
|
+
namespace: "base.primitives",
|
|
2344
|
+
title: "Text",
|
|
2345
|
+
category: "content",
|
|
2346
|
+
props: {
|
|
2347
|
+
value: {
|
|
2348
|
+
type: "text",
|
|
2349
|
+
context: "prop",
|
|
2350
|
+
default: "",
|
|
2351
|
+
// Plain-text content prop, editable in place on the canvas (EC-158).
|
|
2352
|
+
form: { label: "Text", control: "textarea", inlineEditable: true }
|
|
2353
|
+
}
|
|
2354
|
+
},
|
|
2355
|
+
slots: []
|
|
2356
|
+
});
|
|
2357
|
+
function Heading(props) {
|
|
2358
|
+
const level = typeof props.level === "number" && props.level >= 1 && props.level <= 6 ? props.level : 2;
|
|
2359
|
+
const tag = `h${level}`;
|
|
2360
|
+
return createElement(tag, { "data-ec-primitive": "Heading" }, asString(props.text));
|
|
2361
|
+
}
|
|
2362
|
+
var HeadingManifest = defineComponent({
|
|
2363
|
+
id: "base.primitives.Heading",
|
|
2364
|
+
namespace: "base.primitives",
|
|
2365
|
+
title: "Heading",
|
|
2366
|
+
category: "content",
|
|
2367
|
+
props: {
|
|
2368
|
+
text: {
|
|
2369
|
+
type: "text",
|
|
2370
|
+
context: "prop",
|
|
2371
|
+
default: "",
|
|
2372
|
+
// Plain-text content prop, editable in place on the canvas (EC-158).
|
|
2373
|
+
form: { label: "Text", inlineEditable: true }
|
|
2374
|
+
},
|
|
2375
|
+
level: {
|
|
2376
|
+
// A numeric heading level 1–6 (the impl clamps out-of-range to 2).
|
|
2377
|
+
type: "number",
|
|
2378
|
+
context: "prop",
|
|
2379
|
+
default: 2,
|
|
2380
|
+
validation: { min: 1, max: 6 },
|
|
2381
|
+
form: { label: "Level" }
|
|
2382
|
+
}
|
|
2383
|
+
},
|
|
2384
|
+
slots: []
|
|
2385
|
+
});
|
|
2386
|
+
function asPositiveInt(value) {
|
|
2387
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
|
|
2388
|
+
}
|
|
2389
|
+
function asFocalPoint(value) {
|
|
2390
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
2391
|
+
const point = value;
|
|
2392
|
+
if (typeof point.x === "number" && typeof point.y === "number") {
|
|
2393
|
+
return { x: point.x, y: point.y };
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
return null;
|
|
2397
|
+
}
|
|
2398
|
+
function normalizeImageSource(props) {
|
|
2399
|
+
const src = props.src;
|
|
2400
|
+
const asset = src && typeof src === "object" && !Array.isArray(src) ? src : null;
|
|
2401
|
+
const url = asset ? asString(asset.url) : asString(src);
|
|
2402
|
+
const width = asPositiveInt(props.width) ?? (asset ? asPositiveInt(asset.width) : void 0);
|
|
2403
|
+
const height = asPositiveInt(props.height) ?? (asset ? asPositiveInt(asset.height) : void 0);
|
|
2404
|
+
const alt = asString(props.alt) || (asset ? asString(asset.alt) : "");
|
|
2405
|
+
const focalPoint = asset ? asFocalPoint(asset.focalPoint) : null;
|
|
2406
|
+
return { url, width, height, alt, focalPoint };
|
|
2407
|
+
}
|
|
2408
|
+
function focalPointObjectPosition(focalPoint) {
|
|
2409
|
+
return focalPoint ? `${focalPoint.x * 100}% ${focalPoint.y * 100}%` : void 0;
|
|
2410
|
+
}
|
|
2411
|
+
function Image(props) {
|
|
2412
|
+
const { url, width, height, alt, focalPoint } = normalizeImageSource(props);
|
|
2413
|
+
const objectPosition = focalPointObjectPosition(focalPoint);
|
|
2414
|
+
if (url === "") {
|
|
2415
|
+
return /* @__PURE__ */ jsx(
|
|
2416
|
+
"span",
|
|
2417
|
+
{
|
|
2418
|
+
"data-ec-primitive": "Image",
|
|
2419
|
+
"data-ec-image": "empty",
|
|
2420
|
+
role: "img",
|
|
2421
|
+
"aria-label": alt || "No image selected",
|
|
2422
|
+
children: "No image selected"
|
|
2423
|
+
}
|
|
2424
|
+
);
|
|
2425
|
+
}
|
|
2426
|
+
return /* @__PURE__ */ jsx(
|
|
2427
|
+
"img",
|
|
2428
|
+
{
|
|
2429
|
+
"data-ec-primitive": "Image",
|
|
2430
|
+
src: url,
|
|
2431
|
+
alt,
|
|
2432
|
+
...width !== void 0 ? { width } : {},
|
|
2433
|
+
...height !== void 0 ? { height } : {},
|
|
2434
|
+
...objectPosition ? { style: { objectPosition } } : {}
|
|
2435
|
+
}
|
|
2436
|
+
);
|
|
2437
|
+
}
|
|
2438
|
+
var ImageManifest = defineComponent({
|
|
2439
|
+
id: "base.primitives.Image",
|
|
2440
|
+
namespace: "base.primitives",
|
|
2441
|
+
title: "Image",
|
|
2442
|
+
category: "media",
|
|
2443
|
+
props: {
|
|
2444
|
+
// EC-195: `asset`-typed, so the editor picks an uploaded asset (the renderer
|
|
2445
|
+
// resolves the id to a url + intrinsic dimensions at delivery) — a plain URL
|
|
2446
|
+
// string still works (the resolver passes URL-like values through).
|
|
2447
|
+
src: {
|
|
2448
|
+
type: "asset",
|
|
2449
|
+
cardinality: "one",
|
|
2450
|
+
context: "prop",
|
|
2451
|
+
default: "",
|
|
2452
|
+
form: { label: "Image" }
|
|
2453
|
+
},
|
|
2454
|
+
alt: {
|
|
2455
|
+
type: "text",
|
|
2456
|
+
context: "prop",
|
|
2457
|
+
default: "",
|
|
2458
|
+
form: { label: "Alt text" }
|
|
2459
|
+
},
|
|
2460
|
+
width: {
|
|
2461
|
+
type: "number",
|
|
2462
|
+
context: "prop",
|
|
2463
|
+
validation: { min: 1 },
|
|
2464
|
+
form: { label: "Width" }
|
|
2465
|
+
},
|
|
2466
|
+
height: {
|
|
2467
|
+
type: "number",
|
|
2468
|
+
context: "prop",
|
|
2469
|
+
validation: { min: 1 },
|
|
2470
|
+
form: { label: "Height" }
|
|
2471
|
+
},
|
|
2472
|
+
// EC-195: above-the-fold images load eagerly (`next/image priority`) so the
|
|
2473
|
+
// host can opt the hero image out of lazy-loading.
|
|
2474
|
+
priority: {
|
|
2475
|
+
type: "boolean",
|
|
2476
|
+
context: "prop",
|
|
2477
|
+
default: false,
|
|
2478
|
+
form: { label: "Load eagerly (above the fold)" }
|
|
2479
|
+
}
|
|
2480
|
+
},
|
|
2481
|
+
slots: []
|
|
2482
|
+
});
|
|
2483
|
+
function Button(props) {
|
|
2484
|
+
const label = asString(props.label, "Button");
|
|
2485
|
+
if (typeof props.href === "string" && props.href.length > 0) {
|
|
2486
|
+
return /* @__PURE__ */ jsx("a", { "data-ec-primitive": "Button", href: props.href, role: "button", children: label });
|
|
2487
|
+
}
|
|
2488
|
+
return /* @__PURE__ */ jsx("button", { "data-ec-primitive": "Button", type: "button", children: label });
|
|
2489
|
+
}
|
|
2490
|
+
var ButtonManifest = defineComponent({
|
|
2491
|
+
id: "base.primitives.Button",
|
|
2492
|
+
namespace: "base.primitives",
|
|
2493
|
+
title: "Button",
|
|
2494
|
+
category: "content",
|
|
2495
|
+
props: {
|
|
2496
|
+
label: {
|
|
2497
|
+
type: "text",
|
|
2498
|
+
context: "prop",
|
|
2499
|
+
default: "Button",
|
|
2500
|
+
form: { label: "Label" }
|
|
2501
|
+
},
|
|
2502
|
+
href: {
|
|
2503
|
+
type: "text",
|
|
2504
|
+
context: "prop",
|
|
2505
|
+
form: { label: "Link" }
|
|
2506
|
+
}
|
|
2507
|
+
},
|
|
2508
|
+
slots: []
|
|
2509
|
+
});
|
|
2510
|
+
function Repeater(props) {
|
|
2511
|
+
return /* @__PURE__ */ jsx("div", { "data-ec-primitive": "Repeater", children: props.children });
|
|
2512
|
+
}
|
|
2513
|
+
var RepeaterManifest = defineComponent({
|
|
2514
|
+
id: "base.primitives.Repeater",
|
|
2515
|
+
namespace: "base.primitives",
|
|
2516
|
+
title: "Repeater",
|
|
2517
|
+
category: "data",
|
|
2518
|
+
// EC-190 DD7: iterate-over-a-source is a binding construct designed out of v1.
|
|
2519
|
+
// The `items` value (an array) has no static field-def type; it rides node.props
|
|
2520
|
+
// undeclared (passing through the renderer) until the binding/iteration removal in
|
|
2521
|
+
// slice 5 decides Repeater's fate. No editable static prop is exposed meanwhile.
|
|
2522
|
+
props: {},
|
|
2523
|
+
slots: [{ name: "item", label: "Item template", required: true }],
|
|
2524
|
+
iterate: { itemsProp: "items", itemSlot: "item" }
|
|
2525
|
+
});
|
|
2526
|
+
function Switch(props) {
|
|
2527
|
+
const key = asString(props.case);
|
|
2528
|
+
const selected = props[`case:${key}`] ?? props.fallback;
|
|
2529
|
+
return /* @__PURE__ */ jsx("div", { "data-ec-primitive": "Switch", children: selected });
|
|
2530
|
+
}
|
|
2531
|
+
var SwitchManifest = defineComponent({
|
|
2532
|
+
id: "base.primitives.Switch",
|
|
2533
|
+
namespace: "base.primitives",
|
|
2534
|
+
title: "Switch",
|
|
2535
|
+
category: "data",
|
|
2536
|
+
props: {
|
|
2537
|
+
case: {
|
|
2538
|
+
type: "text",
|
|
2539
|
+
context: "prop",
|
|
2540
|
+
default: "",
|
|
2541
|
+
form: { label: "Case" }
|
|
2542
|
+
}
|
|
2543
|
+
},
|
|
2544
|
+
// Named case slots are dynamic; `fallback` renders when no case matches.
|
|
2545
|
+
slots: [{ name: "fallback", label: "Fallback" }]
|
|
2546
|
+
});
|
|
2547
|
+
var PRIMITIVES = [
|
|
2548
|
+
{ manifest: ContainerManifest, implementation: Container },
|
|
2549
|
+
{ manifest: StackManifest, implementation: Stack },
|
|
2550
|
+
{ manifest: GridManifest, implementation: Grid },
|
|
2551
|
+
{ manifest: TextManifest, implementation: Text },
|
|
2552
|
+
{ manifest: HeadingManifest, implementation: Heading },
|
|
2553
|
+
// Rich-text content primitive (EC-159) — content prop is the stored
|
|
2554
|
+
// `@elytracms/rich-text` value, flagged inline-editable.
|
|
2555
|
+
{ manifest: RichTextManifest, implementation: RichText },
|
|
2556
|
+
{ manifest: ImageManifest, implementation: Image },
|
|
2557
|
+
{ manifest: ButtonManifest, implementation: Button },
|
|
2558
|
+
{ manifest: RepeaterManifest, implementation: Repeater },
|
|
2559
|
+
{ manifest: SwitchManifest, implementation: Switch }
|
|
2560
|
+
];
|
|
2561
|
+
function basePrimitives() {
|
|
2562
|
+
const implementations = {};
|
|
2563
|
+
for (const { manifest, implementation } of PRIMITIVES) {
|
|
2564
|
+
implementations[manifest.id] = implementation;
|
|
2565
|
+
}
|
|
2566
|
+
return {
|
|
2567
|
+
manifests: PRIMITIVES.map((p) => p.manifest),
|
|
2568
|
+
implementations
|
|
2569
|
+
};
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
// src/components.ts
|
|
2573
|
+
function defineHostComponents(components = [], options = {}) {
|
|
2574
|
+
const includeBase = options.includeBasePrimitives !== false;
|
|
2575
|
+
const base = includeBase ? basePrimitives() : { manifests: [], implementations: {} };
|
|
2576
|
+
const registry = new ComponentRegistry([...base.manifests, ...components.map((c) => c.manifest)]);
|
|
2577
|
+
const implementations = {};
|
|
2578
|
+
for (const manifest of base.manifests) {
|
|
2579
|
+
const implementation = getImplementation(base.implementations, manifest.id);
|
|
2580
|
+
if (implementation) implementations[manifest.id] = implementation;
|
|
2581
|
+
}
|
|
2582
|
+
for (const component of components) {
|
|
2583
|
+
implementations[component.manifest.id] = component.implementation;
|
|
2584
|
+
}
|
|
2585
|
+
return { registry, implementations, issues: registry.issues };
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
// src/payloads.ts
|
|
2589
|
+
var SOURCE_PAYLOAD_ERROR_KEY = "__elytraSourcePayloadError";
|
|
2590
|
+
function sourcePayloadError(sourceId, messages) {
|
|
2591
|
+
return { [SOURCE_PAYLOAD_ERROR_KEY]: true, sourceId, messages: [...messages] };
|
|
2592
|
+
}
|
|
2593
|
+
function isSourcePayloadError(value) {
|
|
2594
|
+
return typeof value === "object" && value !== null && value[SOURCE_PAYLOAD_ERROR_KEY] === true && typeof value.sourceId === "string" && Array.isArray(value.messages);
|
|
2595
|
+
}
|
|
2596
|
+
function unescapeRefToken(token) {
|
|
2597
|
+
return token.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
2598
|
+
}
|
|
2599
|
+
function pathFromToken(token) {
|
|
2600
|
+
if (token === "" || token === "/") return [];
|
|
2601
|
+
if (!token.startsWith("/")) return void 0;
|
|
2602
|
+
return token.slice(1).split("/").map(unescapeRefToken);
|
|
2603
|
+
}
|
|
2604
|
+
var isPlainObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2605
|
+
function resolvePayloadToken(payload, token) {
|
|
2606
|
+
const path = pathFromToken(token);
|
|
2607
|
+
if (path === void 0) return void 0;
|
|
2608
|
+
let current = payload;
|
|
2609
|
+
for (const segment of path) {
|
|
2610
|
+
if (Array.isArray(current)) {
|
|
2611
|
+
if (!/^(0|[1-9][0-9]*)$/.test(segment)) return void 0;
|
|
2612
|
+
const index = Number(segment);
|
|
2613
|
+
if (index >= current.length) return void 0;
|
|
2614
|
+
current = current[index];
|
|
2615
|
+
continue;
|
|
2616
|
+
}
|
|
2617
|
+
if (isPlainObject2(current)) {
|
|
2618
|
+
if (!Object.prototype.hasOwnProperty.call(current, segment)) return void 0;
|
|
2619
|
+
current = current[segment];
|
|
2620
|
+
continue;
|
|
2621
|
+
}
|
|
2622
|
+
return void 0;
|
|
2623
|
+
}
|
|
2624
|
+
return current;
|
|
2625
|
+
}
|
|
2626
|
+
function createBindingResolver(payloads) {
|
|
2627
|
+
return (ref, item) => {
|
|
2628
|
+
if (ref.mode === "repeaterItem") {
|
|
2629
|
+
return item === void 0 ? void 0 : resolvePayloadToken(item.item, ref.token);
|
|
2630
|
+
}
|
|
2631
|
+
if (!Object.prototype.hasOwnProperty.call(payloads, ref.sourceId)) return void 0;
|
|
2632
|
+
const payload = payloads[ref.sourceId];
|
|
2633
|
+
if (isSourcePayloadError(payload)) {
|
|
2634
|
+
throw new Error(
|
|
2635
|
+
`Source "${payload.sourceId}" query was rejected: ${payload.messages.join("; ")}`
|
|
2636
|
+
);
|
|
2637
|
+
}
|
|
2638
|
+
return resolvePayloadToken(payload, ref.token);
|
|
2639
|
+
};
|
|
2640
|
+
}
|
|
2641
|
+
function relationResolver(relations) {
|
|
2642
|
+
return (ref) => relations[`${ref.collection}:${ref.id}`] ?? null;
|
|
2643
|
+
}
|
|
2644
|
+
function listingResolver(listings) {
|
|
2645
|
+
return (query) => listings[serializeListingQuery(query)] ?? null;
|
|
2646
|
+
}
|
|
2647
|
+
function assetResolver(assets) {
|
|
2648
|
+
const byId = new Map(assets.map((asset) => [asset.id, asset]));
|
|
2649
|
+
return (id) => {
|
|
2650
|
+
const asset = byId.get(id);
|
|
2651
|
+
return asset ? {
|
|
2652
|
+
url: asset.url,
|
|
2653
|
+
width: asset.width,
|
|
2654
|
+
height: asset.height,
|
|
2655
|
+
alt: asset.alt,
|
|
2656
|
+
focalPoint: asset.focalPoint
|
|
2657
|
+
} : null;
|
|
2658
|
+
};
|
|
2659
|
+
}
|
|
2660
|
+
function UnresolvedPageFallback(props) {
|
|
2661
|
+
return /* @__PURE__ */ jsx("div", { "data-ec-fallback": "unresolved-page", "data-ec-status": props.status, children: `This URL did not resolve to a page (status: ${props.status}).` });
|
|
2662
|
+
}
|
|
2663
|
+
function MissingDocumentFallback() {
|
|
2664
|
+
return /* @__PURE__ */ jsx("div", { "data-ec-fallback": "missing-document", children: `Route resolved but carried no page document to render.` });
|
|
2665
|
+
}
|
|
2666
|
+
function stripServerErrorBoundary(_node, rendered) {
|
|
2667
|
+
if (isValidElement(rendered) && rendered.type === RenderErrorBoundary) {
|
|
2668
|
+
return rendered.props.children ?? null;
|
|
2669
|
+
}
|
|
2670
|
+
return rendered;
|
|
2671
|
+
}
|
|
2672
|
+
function defaultBindingPayloads(result) {
|
|
2673
|
+
return { document: result.document, params: result.dynamicParams };
|
|
2674
|
+
}
|
|
2675
|
+
async function CanvasRenderer(props) {
|
|
2676
|
+
const { result, components } = props;
|
|
2677
|
+
if (result.status !== "ok") {
|
|
2678
|
+
return /* @__PURE__ */ jsx(UnresolvedPageFallback, { status: result.status });
|
|
2679
|
+
}
|
|
2680
|
+
const resolveBinding = props.resolveBinding ?? createBindingResolver(props.payloads ?? defaultBindingPayloads(result));
|
|
2681
|
+
const ctx = {
|
|
2682
|
+
registry: components.registry,
|
|
2683
|
+
implementations: components.implementations,
|
|
2684
|
+
resolveBinding,
|
|
2685
|
+
resolveAsset: assetResolver(props.assets ?? []),
|
|
2686
|
+
// EC-254: wire relation population only when the host baked the populated
|
|
2687
|
+
// targets, so a host that did not opt in keeps the raw-ref passthrough.
|
|
2688
|
+
...props.relations ? { resolveRelation: relationResolver(props.relations) } : {},
|
|
2689
|
+
// EC-255: wire the listing resolver only when the host baked the query results,
|
|
2690
|
+
// so a host that did not opt in keeps listing props empty (the block's own state).
|
|
2691
|
+
...props.listings ? { resolveListing: listingResolver(props.listings) } : {},
|
|
2692
|
+
decorateNode: stripServerErrorBoundary
|
|
2693
|
+
};
|
|
2694
|
+
const graph = props.graph ?? null;
|
|
2695
|
+
const document = result.document;
|
|
2696
|
+
if (document) {
|
|
2697
|
+
const layoutId = typeof document["layoutId"] === "string" ? document["layoutId"] : void 0;
|
|
2698
|
+
const composition = compositionOf(document);
|
|
2699
|
+
if (graph) return /* @__PURE__ */ jsx(Fragment, { children: renderCompositionInLayout(graph, layoutId, composition, ctx) });
|
|
2700
|
+
return /* @__PURE__ */ jsx(Fragment, { children: renderComposition(composition, ctx) });
|
|
2701
|
+
}
|
|
2702
|
+
return /* @__PURE__ */ jsx(MissingDocumentFallback, {});
|
|
2703
|
+
}
|
|
2704
|
+
function compositionOf(document) {
|
|
2705
|
+
const body = document["body"];
|
|
2706
|
+
if (Array.isArray(body) || isComponentNodeValue(body)) return body;
|
|
2707
|
+
return document["content"];
|
|
2708
|
+
}
|
|
2709
|
+
function compositionNodesOf(document) {
|
|
2710
|
+
if (!document) return [];
|
|
2711
|
+
const composition = compositionOf(document);
|
|
2712
|
+
const candidates = Array.isArray(composition) ? composition : [composition];
|
|
2713
|
+
return candidates.filter((node) => isComponentNodeValue(node));
|
|
2714
|
+
}
|
|
2715
|
+
function isComponentNodeValue(value) {
|
|
2716
|
+
return typeof value === "object" && value !== null && typeof value.id === "string" && typeof value.componentId === "string";
|
|
2717
|
+
}
|
|
2718
|
+
z.enum(["draft", "published"]);
|
|
2719
|
+
function createContentContext(init) {
|
|
2720
|
+
return {
|
|
2721
|
+
locale: init.locale,
|
|
2722
|
+
perspective: init.perspective,
|
|
2723
|
+
locales: init.locales
|
|
2724
|
+
};
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
// ../content/src/perspective.ts
|
|
2728
|
+
function isDocumentHistory(source) {
|
|
2729
|
+
return "draft" in source && "versions" in source;
|
|
2730
|
+
}
|
|
2731
|
+
function selectPerspective(context, source) {
|
|
2732
|
+
if (isDocumentHistory(source)) {
|
|
2733
|
+
if (context.perspective === "draft") return source.draft;
|
|
2734
|
+
return source.published ?? null;
|
|
2735
|
+
}
|
|
2736
|
+
if (context.perspective === "draft") return source;
|
|
2737
|
+
return null;
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
// ../content/src/locale.ts
|
|
2741
|
+
function resolveLocaleValues(context, collection, doc, basePath = []) {
|
|
2742
|
+
const values = {};
|
|
2743
|
+
const fallbacks = [];
|
|
2744
|
+
for (const field of collection.fields) {
|
|
2745
|
+
const resolved = resolveField(collection, doc, field.name, context.locale, context.locales);
|
|
2746
|
+
if (resolved.value === void 0) continue;
|
|
2747
|
+
values[field.name] = resolved.value;
|
|
2748
|
+
if (resolved.fellBack) {
|
|
2749
|
+
fallbacks.push({
|
|
2750
|
+
path: [...basePath, field.name],
|
|
2751
|
+
field: field.name,
|
|
2752
|
+
requestedLocale: context.locale,
|
|
2753
|
+
sourceLocale: resolved.sourceLocale
|
|
2754
|
+
});
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
return { values, fallbacks };
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
// ../content/src/populate.ts
|
|
2761
|
+
function isPopulatedRelation(field) {
|
|
2762
|
+
return field.populate !== false;
|
|
2763
|
+
}
|
|
2764
|
+
function relationStub(ref) {
|
|
2765
|
+
return { id: ref.id, collection: ref.collection };
|
|
2766
|
+
}
|
|
2767
|
+
function parseRelationRef(value) {
|
|
2768
|
+
const parsed = documentRefSchema.safeParse(value);
|
|
2769
|
+
return parsed.success ? parsed.data : void 0;
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
// ../content/src/assets.ts
|
|
2773
|
+
function resolvedAsset(record, url) {
|
|
2774
|
+
return {
|
|
2775
|
+
id: record.id,
|
|
2776
|
+
url,
|
|
2777
|
+
width: record.width ?? null,
|
|
2778
|
+
height: record.height ?? null,
|
|
2779
|
+
alt: record.alt ?? null,
|
|
2780
|
+
mimeType: record.contentType,
|
|
2781
|
+
focalPoint: record.focalPoint ?? null
|
|
2782
|
+
};
|
|
2783
|
+
}
|
|
2784
|
+
function resolveAssetValue(lookup, assetId, location) {
|
|
2785
|
+
const record = lookup.asset(assetId);
|
|
2786
|
+
if (!record) {
|
|
2787
|
+
return {
|
|
2788
|
+
asset: null,
|
|
2789
|
+
issues: [
|
|
2790
|
+
cmsIssue({
|
|
2791
|
+
code: "unknown-asset",
|
|
2792
|
+
message: `Asset "${assetId}" could not be resolved.`,
|
|
2793
|
+
path: location.path,
|
|
2794
|
+
...location.collectionId ? { collectionId: location.collectionId } : {},
|
|
2795
|
+
...location.documentId ? { documentId: location.documentId } : {},
|
|
2796
|
+
meta: { assetId }
|
|
2797
|
+
})
|
|
2798
|
+
]
|
|
2799
|
+
};
|
|
2800
|
+
}
|
|
2801
|
+
return { asset: resolvedAsset(record, lookup.assetUrl(record)), issues: [] };
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
// ../content/src/resolve.ts
|
|
2805
|
+
function invalidValueIssue(field, collection, documentId, path, expected) {
|
|
2806
|
+
return cmsIssue({
|
|
2807
|
+
code: "invalid-field-value",
|
|
2808
|
+
message: `Field "${field.name}" on "${collection.id}" holds a value that is not ${expected}.`,
|
|
2809
|
+
path,
|
|
2810
|
+
collectionId: collection.id,
|
|
2811
|
+
documentId,
|
|
2812
|
+
meta: { field: field.name, expected }
|
|
2813
|
+
});
|
|
2814
|
+
}
|
|
2815
|
+
function resolveRelationTarget(context, lookup, ref, populate, path, ownerCollectionId, ownerDocumentId, acc) {
|
|
2816
|
+
if (!populate) return { ...relationStub(ref) };
|
|
2817
|
+
const missing = (reason) => {
|
|
2818
|
+
acc.issues.push(
|
|
2819
|
+
cmsIssue({
|
|
2820
|
+
code: "unknown-relation-target",
|
|
2821
|
+
message: `Relation target "${ref.collection}:${ref.id}" could not be populated (${reason}).`,
|
|
2822
|
+
path,
|
|
2823
|
+
collectionId: ownerCollectionId,
|
|
2824
|
+
documentId: ownerDocumentId,
|
|
2825
|
+
meta: { targetCollection: ref.collection, targetId: ref.id, reason }
|
|
2826
|
+
})
|
|
2827
|
+
);
|
|
2828
|
+
return { ...relationStub(ref) };
|
|
2829
|
+
};
|
|
2830
|
+
const targetCollection = lookup.collection(ref.collection);
|
|
2831
|
+
if (!targetCollection) return missing("unknown collection");
|
|
2832
|
+
const source = lookup.document(ref);
|
|
2833
|
+
if (!source) return missing("document not found");
|
|
2834
|
+
const visible = selectPerspective(context, source);
|
|
2835
|
+
if (!visible) return missing(`not visible in ${context.perspective} perspective`);
|
|
2836
|
+
const fields = resolveFields(context, lookup, targetCollection, visible, 1, path, acc);
|
|
2837
|
+
return { id: ref.id, collection: ref.collection, ...fields };
|
|
2838
|
+
}
|
|
2839
|
+
function resolveRelationField(context, lookup, collection, doc, field, value, depth, path, acc) {
|
|
2840
|
+
const populate = depth === 0 && isPopulatedRelation(field);
|
|
2841
|
+
if (field.cardinality === "many") {
|
|
2842
|
+
if (!Array.isArray(value)) {
|
|
2843
|
+
acc.issues.push(invalidValueIssue(field, collection, doc.id, path, "an array of references"));
|
|
2844
|
+
return void 0;
|
|
2845
|
+
}
|
|
2846
|
+
const out = [];
|
|
2847
|
+
value.forEach((entry, index) => {
|
|
2848
|
+
const ref2 = parseRelationRef(entry);
|
|
2849
|
+
if (!ref2) {
|
|
2850
|
+
acc.issues.push(
|
|
2851
|
+
invalidValueIssue(
|
|
2852
|
+
field,
|
|
2853
|
+
collection,
|
|
2854
|
+
doc.id,
|
|
2855
|
+
[...path, index],
|
|
2856
|
+
"a { collection, id } reference"
|
|
2857
|
+
)
|
|
2858
|
+
);
|
|
2859
|
+
return;
|
|
2860
|
+
}
|
|
2861
|
+
out.push(
|
|
2862
|
+
resolveRelationTarget(
|
|
2863
|
+
context,
|
|
2864
|
+
lookup,
|
|
2865
|
+
ref2,
|
|
2866
|
+
populate,
|
|
2867
|
+
[...path, index],
|
|
2868
|
+
collection.id,
|
|
2869
|
+
doc.id,
|
|
2870
|
+
acc
|
|
2871
|
+
)
|
|
2872
|
+
);
|
|
2873
|
+
});
|
|
2874
|
+
return out;
|
|
2875
|
+
}
|
|
2876
|
+
const ref = parseRelationRef(value);
|
|
2877
|
+
if (!ref) {
|
|
2878
|
+
acc.issues.push(
|
|
2879
|
+
invalidValueIssue(field, collection, doc.id, path, "a { collection, id } reference")
|
|
2880
|
+
);
|
|
2881
|
+
return void 0;
|
|
2882
|
+
}
|
|
2883
|
+
return resolveRelationTarget(context, lookup, ref, populate, path, collection.id, doc.id, acc);
|
|
2884
|
+
}
|
|
2885
|
+
function resolveAssetField(lookup, collection, doc, field, value, path, acc) {
|
|
2886
|
+
const resolveOne = (assetId, entryPath) => {
|
|
2887
|
+
if (typeof assetId !== "string" || assetId.length === 0) {
|
|
2888
|
+
acc.issues.push(invalidValueIssue(field, collection, doc.id, entryPath, "an asset id string"));
|
|
2889
|
+
return null;
|
|
2890
|
+
}
|
|
2891
|
+
const resolution = resolveAssetValue(lookup, assetId, {
|
|
2892
|
+
path: entryPath,
|
|
2893
|
+
collectionId: collection.id,
|
|
2894
|
+
documentId: doc.id
|
|
2895
|
+
});
|
|
2896
|
+
acc.issues.push(...resolution.issues);
|
|
2897
|
+
return resolution.asset;
|
|
2898
|
+
};
|
|
2899
|
+
if (field.cardinality === "many") {
|
|
2900
|
+
if (!Array.isArray(value)) {
|
|
2901
|
+
acc.issues.push(invalidValueIssue(field, collection, doc.id, path, "an array of asset ids"));
|
|
2902
|
+
return void 0;
|
|
2903
|
+
}
|
|
2904
|
+
return value.map((entry, index) => resolveOne(entry, [...path, index])).filter((asset) => asset !== null);
|
|
2905
|
+
}
|
|
2906
|
+
return resolveOne(value, path);
|
|
2907
|
+
}
|
|
2908
|
+
function resolveObjectField(context, lookup, collection, doc, field, value, depth, path, acc) {
|
|
2909
|
+
const resolveItem = (item, itemPath) => {
|
|
2910
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) {
|
|
2911
|
+
acc.issues.push(invalidValueIssue(field, collection, doc.id, itemPath, "an object"));
|
|
2912
|
+
return null;
|
|
2913
|
+
}
|
|
2914
|
+
const record = item;
|
|
2915
|
+
const out = {};
|
|
2916
|
+
for (const sub of field.fields) {
|
|
2917
|
+
const subValue = record[sub.name];
|
|
2918
|
+
if (subValue === void 0) continue;
|
|
2919
|
+
const resolved = resolveFieldValue(
|
|
2920
|
+
context,
|
|
2921
|
+
lookup,
|
|
2922
|
+
collection,
|
|
2923
|
+
doc,
|
|
2924
|
+
sub,
|
|
2925
|
+
subValue,
|
|
2926
|
+
depth,
|
|
2927
|
+
[...itemPath, sub.name],
|
|
2928
|
+
acc
|
|
2929
|
+
);
|
|
2930
|
+
if (resolved !== void 0) out[sub.name] = resolved;
|
|
2931
|
+
}
|
|
2932
|
+
return out;
|
|
2933
|
+
};
|
|
2934
|
+
if (field.cardinality === "many") {
|
|
2935
|
+
if (!Array.isArray(value)) {
|
|
2936
|
+
acc.issues.push(invalidValueIssue(field, collection, doc.id, path, "an array of objects"));
|
|
2937
|
+
return void 0;
|
|
2938
|
+
}
|
|
2939
|
+
return value.map((item, index) => resolveItem(item, [...path, index])).filter((item) => item !== null);
|
|
2940
|
+
}
|
|
2941
|
+
return resolveItem(value, path) ?? void 0;
|
|
2942
|
+
}
|
|
2943
|
+
function resolveFieldValue(context, lookup, collection, doc, field, value, depth, path, acc) {
|
|
2944
|
+
switch (field.type) {
|
|
2945
|
+
case "relation":
|
|
2946
|
+
return resolveRelationField(context, lookup, collection, doc, field, value, depth, path, acc);
|
|
2947
|
+
case "asset":
|
|
2948
|
+
return resolveAssetField(lookup, collection, doc, field, value, path, acc);
|
|
2949
|
+
case "object":
|
|
2950
|
+
return resolveObjectField(context, lookup, collection, doc, field, value, depth, path, acc);
|
|
2951
|
+
default:
|
|
2952
|
+
return value;
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
function resolveFields(context, lookup, collection, doc, depth, basePath, acc) {
|
|
2956
|
+
const localeResolution = resolveLocaleValues(context, collection, doc, basePath);
|
|
2957
|
+
acc.fallbacks.push(...localeResolution.fallbacks);
|
|
2958
|
+
const out = {};
|
|
2959
|
+
for (const field of collection.fields) {
|
|
2960
|
+
const value = localeResolution.values[field.name];
|
|
2961
|
+
if (value === void 0) continue;
|
|
2962
|
+
const resolved = resolveFieldValue(
|
|
2963
|
+
context,
|
|
2964
|
+
lookup,
|
|
2965
|
+
collection,
|
|
2966
|
+
doc,
|
|
2967
|
+
field,
|
|
2968
|
+
value,
|
|
2969
|
+
depth,
|
|
2970
|
+
[...basePath, field.name],
|
|
2971
|
+
acc
|
|
2972
|
+
);
|
|
2973
|
+
if (resolved !== void 0) out[field.name] = resolved;
|
|
2974
|
+
}
|
|
2975
|
+
return out;
|
|
2976
|
+
}
|
|
2977
|
+
function resolveDocument(context, lookup, source) {
|
|
2978
|
+
const acc = {
|
|
2979
|
+
fallbacks: [],
|
|
2980
|
+
issues: [...validateLocale(context.locales, context.locale)]
|
|
2981
|
+
};
|
|
2982
|
+
const info = () => ({
|
|
2983
|
+
locale: context.locale,
|
|
2984
|
+
perspective: context.perspective,
|
|
2985
|
+
localeFallbacks: acc.fallbacks
|
|
2986
|
+
});
|
|
2987
|
+
const doc = selectPerspective(context, source);
|
|
2988
|
+
if (!doc) return { document: null, info: info(), issues: acc.issues };
|
|
2989
|
+
const collection = lookup.collection(doc.collection);
|
|
2990
|
+
if (!collection) {
|
|
2991
|
+
acc.issues.push(
|
|
2992
|
+
cmsIssue({
|
|
2993
|
+
code: "unknown-collection",
|
|
2994
|
+
message: `Document "${doc.id}" belongs to unknown collection "${doc.collection}".`,
|
|
2995
|
+
path: ["collection"],
|
|
2996
|
+
collectionId: doc.collection,
|
|
2997
|
+
documentId: doc.id
|
|
2998
|
+
})
|
|
2999
|
+
);
|
|
3000
|
+
return { document: null, info: info(), issues: acc.issues };
|
|
3001
|
+
}
|
|
3002
|
+
const fields = resolveFields(context, lookup, collection, doc, 0, [], acc);
|
|
3003
|
+
return {
|
|
3004
|
+
document: { id: doc.id, collection: doc.collection, ...fields },
|
|
3005
|
+
info: info(),
|
|
3006
|
+
issues: acc.issues
|
|
3007
|
+
};
|
|
3008
|
+
}
|
|
3009
|
+
function populateRelationRef(context, lookup, ref) {
|
|
3010
|
+
const acc = { fallbacks: [], issues: [] };
|
|
3011
|
+
const miss = (reason) => {
|
|
3012
|
+
acc.issues.push(
|
|
3013
|
+
cmsIssue({
|
|
3014
|
+
code: "unknown-relation-target",
|
|
3015
|
+
message: `Relation target "${ref.collection}:${ref.id}" could not be populated (${reason}).`,
|
|
3016
|
+
path: [ref.collection, ref.id],
|
|
3017
|
+
collectionId: ref.collection,
|
|
3018
|
+
documentId: ref.id,
|
|
3019
|
+
meta: { targetCollection: ref.collection, targetId: ref.id, reason }
|
|
3020
|
+
})
|
|
3021
|
+
);
|
|
3022
|
+
return { document: null, issues: acc.issues };
|
|
3023
|
+
};
|
|
3024
|
+
const targetCollection = lookup.collection(ref.collection);
|
|
3025
|
+
if (!targetCollection) return miss("unknown collection");
|
|
3026
|
+
const source = lookup.document(ref);
|
|
3027
|
+
if (!source) return miss("document not found");
|
|
3028
|
+
const visible = selectPerspective(context, source);
|
|
3029
|
+
if (!visible) return miss(`not visible in ${context.perspective} perspective`);
|
|
3030
|
+
const fields = resolveFields(context, lookup, targetCollection, visible, 1, [], acc);
|
|
3031
|
+
return {
|
|
3032
|
+
document: { id: ref.id, collection: ref.collection, ...fields },
|
|
3033
|
+
issues: acc.issues
|
|
3034
|
+
};
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
// ../content/src/query.ts
|
|
3038
|
+
var FILTER_OPERATORS = ["eq", "gt", "gte", "in", "lt", "lte"];
|
|
3039
|
+
function queryError(code, message, path, meta) {
|
|
3040
|
+
return { code, message, path, ...meta ? { meta } : {} };
|
|
3041
|
+
}
|
|
3042
|
+
function isFilterScalar(value) {
|
|
3043
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
3044
|
+
}
|
|
3045
|
+
function isOperator(key) {
|
|
3046
|
+
return FILTER_OPERATORS.includes(key);
|
|
3047
|
+
}
|
|
3048
|
+
function normalizeWhere(where) {
|
|
3049
|
+
const entries = [];
|
|
3050
|
+
for (const field of Object.keys(where).sort()) {
|
|
3051
|
+
const raw = where[field];
|
|
3052
|
+
if (raw === void 0) continue;
|
|
3053
|
+
if (isFilterScalar(raw)) {
|
|
3054
|
+
entries.push({ field, condition: { eq: raw } });
|
|
3055
|
+
continue;
|
|
3056
|
+
}
|
|
3057
|
+
const condition = {};
|
|
3058
|
+
for (const op of FILTER_OPERATORS) {
|
|
3059
|
+
const value = raw[op];
|
|
3060
|
+
if (value === void 0) continue;
|
|
3061
|
+
condition[op] = value;
|
|
3062
|
+
}
|
|
3063
|
+
entries.push({ field, condition });
|
|
3064
|
+
}
|
|
3065
|
+
return entries;
|
|
3066
|
+
}
|
|
3067
|
+
function fieldByName(collection, name) {
|
|
3068
|
+
return collection.fields.find((f) => f.name === name);
|
|
3069
|
+
}
|
|
3070
|
+
function validateWhere(collection, where) {
|
|
3071
|
+
const errors = [];
|
|
3072
|
+
for (const field of Object.keys(where).sort()) {
|
|
3073
|
+
const raw = where[field];
|
|
3074
|
+
if (raw === void 0) continue;
|
|
3075
|
+
const path = ["where", field];
|
|
3076
|
+
const def = fieldByName(collection, field);
|
|
3077
|
+
if (!def || def.filterable !== true) {
|
|
3078
|
+
errors.push(
|
|
3079
|
+
queryError(
|
|
3080
|
+
"non-filterable-field",
|
|
3081
|
+
def ? `Field "${field}" on "${collection.id}" is not declared filterable.` : `Field "${field}" does not exist on "${collection.id}".`,
|
|
3082
|
+
path,
|
|
3083
|
+
{ collection: collection.id, field }
|
|
3084
|
+
)
|
|
3085
|
+
);
|
|
3086
|
+
continue;
|
|
3087
|
+
}
|
|
3088
|
+
if (isFilterScalar(raw)) continue;
|
|
3089
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
3090
|
+
errors.push(
|
|
3091
|
+
queryError(
|
|
3092
|
+
"invalid-filter",
|
|
3093
|
+
`Filter for "${field}" must be a scalar or an operator object.`,
|
|
3094
|
+
path
|
|
3095
|
+
)
|
|
3096
|
+
);
|
|
3097
|
+
continue;
|
|
3098
|
+
}
|
|
3099
|
+
const keys = Object.keys(raw).sort();
|
|
3100
|
+
if (keys.length === 0) {
|
|
3101
|
+
errors.push(queryError("invalid-filter", `Filter for "${field}" has no operators.`, path));
|
|
3102
|
+
continue;
|
|
3103
|
+
}
|
|
3104
|
+
for (const key of keys) {
|
|
3105
|
+
const value = raw[key];
|
|
3106
|
+
if (value === void 0) continue;
|
|
3107
|
+
const opPath = ["where", field, key];
|
|
3108
|
+
if (!isOperator(key)) {
|
|
3109
|
+
errors.push(
|
|
3110
|
+
queryError(
|
|
3111
|
+
"invalid-filter",
|
|
3112
|
+
`Unknown filter operator "${key}" on "${field}" \u2014 the vocabulary is exactly ${FILTER_OPERATORS.join(", ")}.`,
|
|
3113
|
+
opPath,
|
|
3114
|
+
{ operator: key }
|
|
3115
|
+
)
|
|
3116
|
+
);
|
|
3117
|
+
continue;
|
|
3118
|
+
}
|
|
3119
|
+
if (key === "in") {
|
|
3120
|
+
if (!Array.isArray(value) || !value.every(isFilterScalar)) {
|
|
3121
|
+
errors.push(
|
|
3122
|
+
queryError(
|
|
3123
|
+
"invalid-filter",
|
|
3124
|
+
`Operator "in" on "${field}" requires an array of scalars.`,
|
|
3125
|
+
opPath
|
|
3126
|
+
)
|
|
3127
|
+
);
|
|
3128
|
+
}
|
|
3129
|
+
continue;
|
|
3130
|
+
}
|
|
3131
|
+
if (!isFilterScalar(value)) {
|
|
3132
|
+
errors.push(
|
|
3133
|
+
queryError(
|
|
3134
|
+
"invalid-filter",
|
|
3135
|
+
`Operator "${key}" on "${field}" requires a scalar value.`,
|
|
3136
|
+
opPath
|
|
3137
|
+
)
|
|
3138
|
+
);
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
return errors;
|
|
3143
|
+
}
|
|
3144
|
+
function validateSort(collection, sort) {
|
|
3145
|
+
const def = fieldByName(collection, sort.field);
|
|
3146
|
+
if (!def || def.filterable !== true) {
|
|
3147
|
+
return [
|
|
3148
|
+
queryError(
|
|
3149
|
+
"invalid-sort",
|
|
3150
|
+
def ? `Sort field "${sort.field}" on "${collection.id}" is not declared filterable.` : `Sort field "${sort.field}" does not exist on "${collection.id}".`,
|
|
3151
|
+
["sort", "field"],
|
|
3152
|
+
{ collection: collection.id, field: sort.field }
|
|
3153
|
+
)
|
|
3154
|
+
];
|
|
3155
|
+
}
|
|
3156
|
+
if (sort.direction !== void 0 && sort.direction !== "asc" && sort.direction !== "desc") {
|
|
3157
|
+
return [
|
|
3158
|
+
queryError("invalid-sort", `Sort direction must be "asc" or "desc".`, ["sort", "direction"])
|
|
3159
|
+
];
|
|
3160
|
+
}
|
|
3161
|
+
return [];
|
|
3162
|
+
}
|
|
3163
|
+
function validateLimit(limit) {
|
|
3164
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
3165
|
+
return [queryError("invalid-limit", "Limit must be a positive integer.", ["limit"])];
|
|
3166
|
+
}
|
|
3167
|
+
return [];
|
|
3168
|
+
}
|
|
3169
|
+
function filterScalarOf(value) {
|
|
3170
|
+
if (isFilterScalar(value)) return value;
|
|
3171
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value) && typeof value.id === "string") {
|
|
3172
|
+
return value.id;
|
|
3173
|
+
}
|
|
3174
|
+
return null;
|
|
3175
|
+
}
|
|
3176
|
+
function compares(value, bound) {
|
|
3177
|
+
if (value === null) return null;
|
|
3178
|
+
if (typeof value === "number" && typeof bound === "number") {
|
|
3179
|
+
return value < bound ? -1 : value > bound ? 1 : 0;
|
|
3180
|
+
}
|
|
3181
|
+
if (typeof value === "string" && typeof bound === "string") {
|
|
3182
|
+
return value < bound ? -1 : value > bound ? 1 : 0;
|
|
3183
|
+
}
|
|
3184
|
+
return null;
|
|
3185
|
+
}
|
|
3186
|
+
function matchesFieldValue(value, condition) {
|
|
3187
|
+
if (Array.isArray(value)) {
|
|
3188
|
+
return value.some((element) => matchesCondition(filterScalarOf(element), condition));
|
|
3189
|
+
}
|
|
3190
|
+
return matchesCondition(filterScalarOf(value), condition);
|
|
3191
|
+
}
|
|
3192
|
+
function matchesCondition(value, condition) {
|
|
3193
|
+
for (const op of FILTER_OPERATORS) {
|
|
3194
|
+
const bound = condition[op];
|
|
3195
|
+
if (bound === void 0) continue;
|
|
3196
|
+
switch (op) {
|
|
3197
|
+
case "eq":
|
|
3198
|
+
if (value !== bound) return false;
|
|
3199
|
+
break;
|
|
3200
|
+
case "in":
|
|
3201
|
+
if (value === null || !bound.includes(value)) return false;
|
|
3202
|
+
break;
|
|
3203
|
+
case "lt":
|
|
3204
|
+
case "lte":
|
|
3205
|
+
case "gt":
|
|
3206
|
+
case "gte": {
|
|
3207
|
+
const cmp = compares(value, bound);
|
|
3208
|
+
if (cmp === null) return false;
|
|
3209
|
+
if (op === "lt" && !(cmp < 0)) return false;
|
|
3210
|
+
if (op === "lte" && !(cmp <= 0)) return false;
|
|
3211
|
+
if (op === "gt" && !(cmp > 0)) return false;
|
|
3212
|
+
if (op === "gte" && !(cmp >= 0)) return false;
|
|
3213
|
+
break;
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
return true;
|
|
3218
|
+
}
|
|
3219
|
+
function compareSortValues(a, b) {
|
|
3220
|
+
const rank = (v) => typeof v === "number" ? 0 : typeof v === "string" ? 1 : typeof v === "boolean" ? 2 : 3;
|
|
3221
|
+
const ra = rank(a);
|
|
3222
|
+
const rb = rank(b);
|
|
3223
|
+
if (ra !== rb) return ra - rb;
|
|
3224
|
+
if (typeof a === "number" && typeof b === "number") return a < b ? -1 : a > b ? 1 : 0;
|
|
3225
|
+
if (typeof a === "string" && typeof b === "string") return a < b ? -1 : a > b ? 1 : 0;
|
|
3226
|
+
if (typeof a === "boolean" && typeof b === "boolean") return a === b ? 0 : a ? 1 : -1;
|
|
3227
|
+
return 0;
|
|
3228
|
+
}
|
|
3229
|
+
function compareListEntries(a, b, direction) {
|
|
3230
|
+
const byValue = compareSortValues(a.sortValue, b.sortValue);
|
|
3231
|
+
const directed = direction === "desc" ? -byValue : byValue;
|
|
3232
|
+
if (directed !== 0) return directed;
|
|
3233
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
3234
|
+
}
|
|
3235
|
+
var LIST_CURSOR_PREFIX = "ec1.";
|
|
3236
|
+
var cursorPayloadSchema = z.object({
|
|
3237
|
+
v: z.literal(1),
|
|
3238
|
+
f: z.string().nullable(),
|
|
3239
|
+
d: z.enum(["asc", "desc"]),
|
|
3240
|
+
s: z.union([z.string(), z.number(), z.boolean(), z.null()]),
|
|
3241
|
+
i: z.string().min(1)
|
|
3242
|
+
});
|
|
3243
|
+
var BASE64URL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
3244
|
+
function toBase64Url(bytes) {
|
|
3245
|
+
let out = "";
|
|
3246
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
3247
|
+
const a = bytes[i] ?? 0;
|
|
3248
|
+
const b = bytes[i + 1];
|
|
3249
|
+
const c = bytes[i + 2];
|
|
3250
|
+
out += BASE64URL[a >> 2];
|
|
3251
|
+
out += BASE64URL[(a & 3) << 4 | (b ?? 0) >> 4];
|
|
3252
|
+
if (b !== void 0) out += BASE64URL[(b & 15) << 2 | (c ?? 0) >> 6];
|
|
3253
|
+
if (c !== void 0) out += BASE64URL[c & 63];
|
|
3254
|
+
}
|
|
3255
|
+
return out;
|
|
3256
|
+
}
|
|
3257
|
+
function fromBase64Url(text) {
|
|
3258
|
+
const values = [];
|
|
3259
|
+
for (const char of text) {
|
|
3260
|
+
const value = BASE64URL.indexOf(char);
|
|
3261
|
+
if (value === -1) return null;
|
|
3262
|
+
values.push(value);
|
|
3263
|
+
}
|
|
3264
|
+
if (values.length % 4 === 1) return null;
|
|
3265
|
+
const bytes = [];
|
|
3266
|
+
for (let i = 0; i < values.length; i += 4) {
|
|
3267
|
+
const [a, b, c, d] = [values[i], values[i + 1], values[i + 2], values[i + 3]];
|
|
3268
|
+
if (a === void 0 || b === void 0) break;
|
|
3269
|
+
bytes.push(a << 2 | b >> 4);
|
|
3270
|
+
if (c !== void 0) bytes.push((b & 15) << 4 | c >> 2);
|
|
3271
|
+
if (d !== void 0) bytes.push(((c ?? 0) & 3) << 6 | d);
|
|
3272
|
+
}
|
|
3273
|
+
return new Uint8Array(bytes);
|
|
3274
|
+
}
|
|
3275
|
+
function encodeListCursor(cursor) {
|
|
3276
|
+
const payload = JSON.stringify({
|
|
3277
|
+
v: 1,
|
|
3278
|
+
f: cursor.sortField,
|
|
3279
|
+
d: cursor.direction,
|
|
3280
|
+
s: cursor.sortValue,
|
|
3281
|
+
i: cursor.id
|
|
3282
|
+
});
|
|
3283
|
+
return LIST_CURSOR_PREFIX + toBase64Url(new TextEncoder().encode(payload));
|
|
3284
|
+
}
|
|
3285
|
+
function decodeListCursor(text) {
|
|
3286
|
+
if (!text.startsWith(LIST_CURSOR_PREFIX)) return null;
|
|
3287
|
+
const bytes = fromBase64Url(text.slice(LIST_CURSOR_PREFIX.length));
|
|
3288
|
+
if (!bytes) return null;
|
|
3289
|
+
let parsed;
|
|
3290
|
+
try {
|
|
3291
|
+
parsed = JSON.parse(new TextDecoder().decode(bytes));
|
|
3292
|
+
} catch {
|
|
3293
|
+
return null;
|
|
3294
|
+
}
|
|
3295
|
+
const payload = cursorPayloadSchema.safeParse(parsed);
|
|
3296
|
+
if (!payload.success) return null;
|
|
3297
|
+
return {
|
|
3298
|
+
sortField: payload.data.f,
|
|
3299
|
+
direction: payload.data.d,
|
|
3300
|
+
sortValue: payload.data.s,
|
|
3301
|
+
id: payload.data.i
|
|
3302
|
+
};
|
|
3303
|
+
}
|
|
3304
|
+
|
|
3305
|
+
// ../content/src/tags.ts
|
|
3306
|
+
function sortedUnique(values) {
|
|
3307
|
+
return [...new Set(values)].sort();
|
|
3308
|
+
}
|
|
3309
|
+
function cacheTags(init) {
|
|
3310
|
+
return {
|
|
3311
|
+
docs: sortedUnique(init.docs ?? []),
|
|
3312
|
+
collections: sortedUnique(init.collections ?? []),
|
|
3313
|
+
routes: sortedUnique(init.routes ?? []),
|
|
3314
|
+
assets: sortedUnique(init.assets ?? []),
|
|
3315
|
+
locale: init.locale
|
|
3316
|
+
};
|
|
3317
|
+
}
|
|
3318
|
+
function documentCacheKeys(collection, document) {
|
|
3319
|
+
const keys = [documentKey({ collection: document.collection, id: document.id })];
|
|
3320
|
+
for (const field of collection.fields) {
|
|
3321
|
+
if (field.type !== "relation") continue;
|
|
3322
|
+
const value = document[field.name];
|
|
3323
|
+
const entries = Array.isArray(value) ? value : [value];
|
|
3324
|
+
for (const entry of entries) {
|
|
3325
|
+
if (entry !== null && typeof entry === "object" && typeof entry.id === "string" && typeof entry.collection === "string") {
|
|
3326
|
+
const ref = entry;
|
|
3327
|
+
keys.push(documentKey({ collection: ref.collection, id: ref.id }));
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
return keys;
|
|
3332
|
+
}
|
|
3333
|
+
|
|
3334
|
+
// ../content/src/client.ts
|
|
3335
|
+
function withLocale(context, locale) {
|
|
3336
|
+
return locale === void 0 || locale === context.locale ? context : { ...context, locale };
|
|
3337
|
+
}
|
|
3338
|
+
function normalizePath2(url) {
|
|
3339
|
+
return "/" + splitPath(url).join("/");
|
|
3340
|
+
}
|
|
3341
|
+
function materializeRouteDocumentRef(ref, params) {
|
|
3342
|
+
if (!ref.id.startsWith(":")) return { ok: true, ref };
|
|
3343
|
+
const param = ref.id.slice(1);
|
|
3344
|
+
const value = params[param];
|
|
3345
|
+
if (value === void 0 || value.length === 0) return { ok: false, missingParam: param };
|
|
3346
|
+
return { ok: true, ref: { collection: ref.collection, id: value } };
|
|
3347
|
+
}
|
|
3348
|
+
function mountParamName(route) {
|
|
3349
|
+
const last = splitPath(route.pattern).at(-1) ?? "";
|
|
3350
|
+
return last.replace(/^:/, "").replace(/\*$/, "");
|
|
3351
|
+
}
|
|
3352
|
+
function createContentClient(init) {
|
|
3353
|
+
const { lookup, context } = init;
|
|
3354
|
+
const allRoutes = init.routes ?? [];
|
|
3355
|
+
const mounts = allRoutes.map((route) => {
|
|
3356
|
+
const mount = hierarchyMount(route);
|
|
3357
|
+
return mount ? { mount, route } : void 0;
|
|
3358
|
+
}).filter((m) => m !== void 0);
|
|
3359
|
+
const router = createRouter(
|
|
3360
|
+
allRoutes.filter((route) => !isHierarchyTemplate(route)),
|
|
3361
|
+
init.redirects ?? [],
|
|
3362
|
+
{ defaultLocale: context.locales.default }
|
|
3363
|
+
);
|
|
3364
|
+
const resolveBoundDocument = (ctx, ref, issues, docKeys, collections) => {
|
|
3365
|
+
collections.push(ref.collection);
|
|
3366
|
+
const collection = lookup.collection(ref.collection);
|
|
3367
|
+
const source = lookup.document(ref) ?? (collection ? findBySlug(ctx, collection, ref.id) : void 0);
|
|
3368
|
+
if (!source) {
|
|
3369
|
+
issues.push(
|
|
3370
|
+
cmsIssue({
|
|
3371
|
+
code: "unknown-route-target",
|
|
3372
|
+
message: `Route-bound document "${ref.collection}:${ref.id}" was not found.`,
|
|
3373
|
+
path: ["document"],
|
|
3374
|
+
collectionId: ref.collection,
|
|
3375
|
+
documentId: ref.id
|
|
3376
|
+
})
|
|
3377
|
+
);
|
|
3378
|
+
return null;
|
|
3379
|
+
}
|
|
3380
|
+
const result = resolveDocument(ctx, lookup, source);
|
|
3381
|
+
issues.push(...result.issues);
|
|
3382
|
+
if (result.document && collection) {
|
|
3383
|
+
docKeys.push(...documentCacheKeys(collection, result.document));
|
|
3384
|
+
}
|
|
3385
|
+
return result.document;
|
|
3386
|
+
};
|
|
3387
|
+
const resolvePage = (url, locale) => {
|
|
3388
|
+
const ctx = withLocale(context, locale);
|
|
3389
|
+
const path = normalizePath2(url);
|
|
3390
|
+
const resolution = router.resolveUrl(path, ctx.locale);
|
|
3391
|
+
const issues = [];
|
|
3392
|
+
const docKeys = [];
|
|
3393
|
+
const collections = [];
|
|
3394
|
+
const routePaths = [path];
|
|
3395
|
+
if (resolution.status === "redirect") {
|
|
3396
|
+
const chain = router.followRedirects(path);
|
|
3397
|
+
routePaths.push(...chain.chain);
|
|
3398
|
+
return {
|
|
3399
|
+
status: "redirect",
|
|
3400
|
+
route: null,
|
|
3401
|
+
dynamicParams: {},
|
|
3402
|
+
localeFallback: false,
|
|
3403
|
+
redirect: {
|
|
3404
|
+
target: resolution.redirectTarget ?? chain.target,
|
|
3405
|
+
permanent: resolution.permanent ?? chain.permanent
|
|
3406
|
+
},
|
|
3407
|
+
document: null,
|
|
3408
|
+
issues,
|
|
3409
|
+
tags: cacheTags({ routes: routePaths, locale: ctx.locale })
|
|
3410
|
+
};
|
|
3411
|
+
}
|
|
3412
|
+
if (resolution.status === "notFound") {
|
|
3413
|
+
const urlSegments = splitPath(path);
|
|
3414
|
+
for (const { mount, route: route2 } of mounts) {
|
|
3415
|
+
if (mount.locale !== void 0 && mount.locale !== ctx.locale) continue;
|
|
3416
|
+
if (urlSegments.length <= mount.prefix.length) continue;
|
|
3417
|
+
if (!mount.prefix.every((seg, i) => urlSegments[i] === seg)) continue;
|
|
3418
|
+
const collection = lookup.collection(mount.collection);
|
|
3419
|
+
if (!collection || !isHierarchical(collection)) continue;
|
|
3420
|
+
const rest = urlSegments.slice(mount.prefix.length);
|
|
3421
|
+
const source = findByComposedPath(ctx, mount.collection, rest);
|
|
3422
|
+
if (!source) continue;
|
|
3423
|
+
collections.push(mount.collection);
|
|
3424
|
+
const result = resolveDocument(ctx, lookup, source);
|
|
3425
|
+
issues.push(...result.issues);
|
|
3426
|
+
if (!result.document) continue;
|
|
3427
|
+
docKeys.push(...documentCacheKeys(collection, result.document));
|
|
3428
|
+
return {
|
|
3429
|
+
status: "ok",
|
|
3430
|
+
route: route2,
|
|
3431
|
+
dynamicParams: { [mountParamName(route2)]: rest.join("/") },
|
|
3432
|
+
localeFallback: false,
|
|
3433
|
+
redirect: null,
|
|
3434
|
+
document: result.document,
|
|
3435
|
+
issues,
|
|
3436
|
+
tags: cacheTags({ docs: docKeys, collections, routes: routePaths, locale: ctx.locale })
|
|
3437
|
+
};
|
|
3438
|
+
}
|
|
3439
|
+
return {
|
|
3440
|
+
status: "notFound",
|
|
3441
|
+
route: null,
|
|
3442
|
+
dynamicParams: {},
|
|
3443
|
+
localeFallback: resolution.fallback,
|
|
3444
|
+
redirect: null,
|
|
3445
|
+
document: null,
|
|
3446
|
+
issues,
|
|
3447
|
+
tags: cacheTags({ routes: routePaths, locale: ctx.locale })
|
|
3448
|
+
};
|
|
3449
|
+
}
|
|
3450
|
+
const route = resolution.route ?? null;
|
|
3451
|
+
let document = null;
|
|
3452
|
+
let documentMissing = false;
|
|
3453
|
+
if (resolution.documentRef) {
|
|
3454
|
+
const materialized = materializeRouteDocumentRef(
|
|
3455
|
+
resolution.documentRef,
|
|
3456
|
+
resolution.dynamicParams
|
|
3457
|
+
);
|
|
3458
|
+
if (materialized.ok) {
|
|
3459
|
+
document = resolveBoundDocument(ctx, materialized.ref, issues, docKeys, collections);
|
|
3460
|
+
documentMissing = document === null;
|
|
3461
|
+
} else {
|
|
3462
|
+
documentMissing = true;
|
|
3463
|
+
collections.push(resolution.documentRef.collection);
|
|
3464
|
+
issues.push(
|
|
3465
|
+
cmsIssue({
|
|
3466
|
+
code: "unknown-route-target",
|
|
3467
|
+
message: `Route "${route?.id ?? path}" binds its document to URL parameter ":${materialized.missingParam}", which pattern "${route?.pattern ?? path}" does not capture.`,
|
|
3468
|
+
path: ["routes", route?.id ?? path, "document"],
|
|
3469
|
+
collectionId: resolution.documentRef.collection,
|
|
3470
|
+
meta: { param: materialized.missingParam, pattern: route?.pattern }
|
|
3471
|
+
})
|
|
3472
|
+
);
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
if (documentMissing) {
|
|
3476
|
+
return {
|
|
3477
|
+
status: "notFound",
|
|
3478
|
+
route: null,
|
|
3479
|
+
dynamicParams: resolution.dynamicParams,
|
|
3480
|
+
localeFallback: resolution.fallback,
|
|
3481
|
+
redirect: null,
|
|
3482
|
+
document: null,
|
|
3483
|
+
issues,
|
|
3484
|
+
tags: cacheTags({ docs: docKeys, collections, routes: routePaths, locale: ctx.locale })
|
|
3485
|
+
};
|
|
3486
|
+
}
|
|
3487
|
+
return {
|
|
3488
|
+
status: "ok",
|
|
3489
|
+
route,
|
|
3490
|
+
dynamicParams: resolution.dynamicParams,
|
|
3491
|
+
localeFallback: resolution.fallback,
|
|
3492
|
+
redirect: null,
|
|
3493
|
+
document,
|
|
3494
|
+
issues,
|
|
3495
|
+
tags: cacheTags({ docs: docKeys, collections, routes: routePaths, locale: ctx.locale })
|
|
3496
|
+
};
|
|
3497
|
+
};
|
|
3498
|
+
const findBySlug = (ctx, collection, slug) => {
|
|
3499
|
+
if (!lookup.documents) return void 0;
|
|
3500
|
+
if (!collection.fields.some((f) => f.name === "slug")) return void 0;
|
|
3501
|
+
const matches = [];
|
|
3502
|
+
for (const source of lookup.documents(collection.id)) {
|
|
3503
|
+
const row = selectPerspective(ctx, source);
|
|
3504
|
+
if (!row) continue;
|
|
3505
|
+
const value = resolveField(collection, row, "slug", ctx.locale, ctx.locales).value;
|
|
3506
|
+
if (value === slug) matches.push({ id: row.id, source });
|
|
3507
|
+
}
|
|
3508
|
+
matches.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
3509
|
+
return matches[0]?.source;
|
|
3510
|
+
};
|
|
3511
|
+
const findByComposedPath = (ctx, collectionId, segments) => {
|
|
3512
|
+
if (!lookup.documents) return void 0;
|
|
3513
|
+
const rows = [];
|
|
3514
|
+
const sourceById = /* @__PURE__ */ new Map();
|
|
3515
|
+
for (const source of lookup.documents(collectionId)) {
|
|
3516
|
+
const row = selectPerspective(ctx, source);
|
|
3517
|
+
if (!row) continue;
|
|
3518
|
+
rows.push(row);
|
|
3519
|
+
sourceById.set(row.id, source);
|
|
3520
|
+
}
|
|
3521
|
+
const matched = resolveByPath(segments, rows);
|
|
3522
|
+
return matched ? sourceById.get(matched.id) : void 0;
|
|
3523
|
+
};
|
|
3524
|
+
const getDocument = (collectionId, idOrSlug, locale) => {
|
|
3525
|
+
const ctx = withLocale(context, locale);
|
|
3526
|
+
const baseTags = { collections: [collectionId], locale: ctx.locale };
|
|
3527
|
+
const collection = lookup.collection(collectionId);
|
|
3528
|
+
if (!collection) {
|
|
3529
|
+
return {
|
|
3530
|
+
document: null,
|
|
3531
|
+
info: null,
|
|
3532
|
+
issues: [
|
|
3533
|
+
cmsIssue({
|
|
3534
|
+
code: "unknown-collection",
|
|
3535
|
+
message: `Collection "${collectionId}" is not defined.`,
|
|
3536
|
+
path: ["collection"],
|
|
3537
|
+
collectionId
|
|
3538
|
+
})
|
|
3539
|
+
],
|
|
3540
|
+
tags: cacheTags(baseTags)
|
|
3541
|
+
};
|
|
3542
|
+
}
|
|
3543
|
+
const source = lookup.document({ collection: collectionId, id: idOrSlug }) ?? findBySlug(ctx, collection, idOrSlug);
|
|
3544
|
+
if (!source) {
|
|
3545
|
+
return { document: null, info: null, issues: [], tags: cacheTags(baseTags) };
|
|
3546
|
+
}
|
|
3547
|
+
const result = resolveDocument(ctx, lookup, source);
|
|
3548
|
+
const docKeys = result.document ? documentCacheKeys(collection, result.document) : [];
|
|
3549
|
+
return {
|
|
3550
|
+
// The generated per-collection delivery type (EC-142 codegen) is the
|
|
3551
|
+
// static face of what resolveDocument produces for this collection.
|
|
3552
|
+
document: result.document,
|
|
3553
|
+
info: result.info,
|
|
3554
|
+
issues: result.issues,
|
|
3555
|
+
tags: cacheTags({ ...baseTags, docs: docKeys })
|
|
3556
|
+
};
|
|
3557
|
+
};
|
|
3558
|
+
const listDocuments = (collectionId, query = {}) => {
|
|
3559
|
+
const ctx = context;
|
|
3560
|
+
const errorTags = cacheTags({ collections: [collectionId], locale: ctx.locale });
|
|
3561
|
+
const errors = [];
|
|
3562
|
+
const collection = lookup.collection(collectionId);
|
|
3563
|
+
if (!collection) {
|
|
3564
|
+
errors.push(
|
|
3565
|
+
queryError("unknown-collection", `Collection "${collectionId}" is not defined.`, [
|
|
3566
|
+
"collection"
|
|
3567
|
+
])
|
|
3568
|
+
);
|
|
3569
|
+
return { ok: false, errors, tags: errorTags };
|
|
3570
|
+
}
|
|
3571
|
+
if (!lookup.documents) {
|
|
3572
|
+
errors.push(
|
|
3573
|
+
queryError(
|
|
3574
|
+
"list-not-supported",
|
|
3575
|
+
"This content lookup does not provide a `documents` capability.",
|
|
3576
|
+
[]
|
|
3577
|
+
)
|
|
3578
|
+
);
|
|
3579
|
+
return { ok: false, errors, tags: errorTags };
|
|
3580
|
+
}
|
|
3581
|
+
if (query.where) errors.push(...validateWhere(collection, query.where));
|
|
3582
|
+
if (query.sort) errors.push(...validateSort(collection, query.sort));
|
|
3583
|
+
if (query.limit !== void 0) errors.push(...validateLimit(query.limit));
|
|
3584
|
+
const sortField = query.sort?.field ?? null;
|
|
3585
|
+
const direction = query.sort?.direction ?? "asc";
|
|
3586
|
+
let after = null;
|
|
3587
|
+
if (query.cursor !== void 0) {
|
|
3588
|
+
const decoded = decodeListCursor(query.cursor);
|
|
3589
|
+
if (!decoded || decoded.sortField !== sortField || decoded.direction !== direction) {
|
|
3590
|
+
errors.push(
|
|
3591
|
+
queryError(
|
|
3592
|
+
"invalid-cursor",
|
|
3593
|
+
decoded ? "Cursor was minted under a different sort and cannot be reused." : "Cursor is malformed.",
|
|
3594
|
+
["cursor"]
|
|
3595
|
+
)
|
|
3596
|
+
);
|
|
3597
|
+
} else {
|
|
3598
|
+
after = { sortValue: decoded.sortValue, id: decoded.id };
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
if (errors.length > 0) return { ok: false, errors, tags: errorTags };
|
|
3602
|
+
const where = normalizeWhere(query.where ?? {});
|
|
3603
|
+
const entries = [];
|
|
3604
|
+
for (const source of lookup.documents(collectionId)) {
|
|
3605
|
+
const row = selectPerspective(ctx, source);
|
|
3606
|
+
if (!row || row.collection !== collectionId) continue;
|
|
3607
|
+
const values = resolveLocaleValues(ctx, collection, row).values;
|
|
3608
|
+
const matches = where.every(
|
|
3609
|
+
(entry) => matchesFieldValue(values[entry.field], entry.condition)
|
|
3610
|
+
);
|
|
3611
|
+
if (!matches) continue;
|
|
3612
|
+
entries.push({
|
|
3613
|
+
source,
|
|
3614
|
+
id: row.id,
|
|
3615
|
+
sortValue: sortField === null ? row.id : filterScalarOf(values[sortField])
|
|
3616
|
+
});
|
|
3617
|
+
}
|
|
3618
|
+
entries.sort(
|
|
3619
|
+
(a, b) => compareListEntries(
|
|
3620
|
+
{ id: a.id, sortValue: a.sortValue },
|
|
3621
|
+
{ id: b.id, sortValue: b.sortValue },
|
|
3622
|
+
direction
|
|
3623
|
+
)
|
|
3624
|
+
);
|
|
3625
|
+
const afterPosition = after;
|
|
3626
|
+
const fromIndex = afterPosition ? entries.findIndex(
|
|
3627
|
+
(entry) => compareListEntries(
|
|
3628
|
+
{ id: entry.id, sortValue: entry.sortValue },
|
|
3629
|
+
afterPosition,
|
|
3630
|
+
direction
|
|
3631
|
+
) > 0
|
|
3632
|
+
) : 0;
|
|
3633
|
+
const start = fromIndex === -1 ? entries.length : fromIndex;
|
|
3634
|
+
const end = query.limit !== void 0 ? start + query.limit : entries.length;
|
|
3635
|
+
const pageEntries = entries.slice(start, end);
|
|
3636
|
+
const hasMore = end < entries.length;
|
|
3637
|
+
const documents = [];
|
|
3638
|
+
const issues = [];
|
|
3639
|
+
const docKeys = [];
|
|
3640
|
+
for (const entry of pageEntries) {
|
|
3641
|
+
const result = resolveDocument(ctx, lookup, entry.source);
|
|
3642
|
+
issues.push(...result.issues);
|
|
3643
|
+
if (result.document) {
|
|
3644
|
+
documents.push(result.document);
|
|
3645
|
+
docKeys.push(...documentCacheKeys(collection, result.document));
|
|
3646
|
+
}
|
|
3647
|
+
}
|
|
3648
|
+
const last = pageEntries[pageEntries.length - 1];
|
|
3649
|
+
const nextCursor = hasMore && last ? encodeListCursor({ sortField, direction, sortValue: last.sortValue, id: last.id }) : null;
|
|
3650
|
+
return {
|
|
3651
|
+
ok: true,
|
|
3652
|
+
// The generated per-collection delivery type (EC-142 codegen) is the
|
|
3653
|
+
// static face of what resolveDocument produces for this collection.
|
|
3654
|
+
documents,
|
|
3655
|
+
nextCursor,
|
|
3656
|
+
issues,
|
|
3657
|
+
tags: cacheTags({
|
|
3658
|
+
docs: docKeys,
|
|
3659
|
+
collections: [collectionId],
|
|
3660
|
+
locale: ctx.locale
|
|
3661
|
+
})
|
|
3662
|
+
};
|
|
3663
|
+
};
|
|
3664
|
+
const resolveRelation = (ref) => populateRelationRef(context, lookup, ref).document;
|
|
3665
|
+
return {
|
|
3666
|
+
context,
|
|
3667
|
+
routerIssues: router.issues,
|
|
3668
|
+
resolvePage,
|
|
3669
|
+
getDocument,
|
|
3670
|
+
listDocuments,
|
|
3671
|
+
resolveRelation
|
|
3672
|
+
};
|
|
3673
|
+
}
|
|
3674
|
+
|
|
3675
|
+
// ../content/src/binding-sources.ts
|
|
3676
|
+
var SECTION_SOURCE_PREFIX = "cms.section.";
|
|
3677
|
+
function isSectionSourceId(sourceId) {
|
|
3678
|
+
return sourceId.startsWith(SECTION_SOURCE_PREFIX);
|
|
3679
|
+
}
|
|
3680
|
+
var isPlainObject3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3681
|
+
function parseCmsSourceQueryConfig(value) {
|
|
3682
|
+
if (!isPlainObject3(value)) return void 0;
|
|
3683
|
+
const collectionId = value.collectionId;
|
|
3684
|
+
if (typeof collectionId !== "string" || collectionId.length === 0) return void 0;
|
|
3685
|
+
const config = { collectionId };
|
|
3686
|
+
if (typeof value.documentId === "string") config.documentId = value.documentId;
|
|
3687
|
+
if (isPlainObject3(value.filter)) config.filter = value.filter;
|
|
3688
|
+
if (typeof value.limit === "number") config.limit = value.limit;
|
|
3689
|
+
const sort = value.sort;
|
|
3690
|
+
if (isPlainObject3(sort) && typeof sort.field === "string") {
|
|
3691
|
+
const direction = sort.direction;
|
|
3692
|
+
config.sort = {
|
|
3693
|
+
field: sort.field,
|
|
3694
|
+
...direction === "asc" || direction === "desc" ? { direction } : {}
|
|
3695
|
+
};
|
|
3696
|
+
}
|
|
3697
|
+
return config;
|
|
3698
|
+
}
|
|
3699
|
+
function listQueryOfCmsConfig(config) {
|
|
3700
|
+
return {
|
|
3701
|
+
...config.filter !== void 0 && Object.keys(config.filter).length > 0 ? { where: config.filter } : {},
|
|
3702
|
+
...config.sort !== void 0 ? { sort: config.sort } : {},
|
|
3703
|
+
...config.limit !== void 0 ? { limit: config.limit } : {}
|
|
3704
|
+
};
|
|
3705
|
+
}
|
|
3706
|
+
|
|
3707
|
+
// src/source.ts
|
|
3708
|
+
function mergeCacheTags(first, ...rest) {
|
|
3709
|
+
const all = [first, ...rest];
|
|
3710
|
+
return cacheTags({
|
|
3711
|
+
docs: all.flatMap((t) => t.docs),
|
|
3712
|
+
collections: all.flatMap((t) => t.collections),
|
|
3713
|
+
routes: all.flatMap((t) => t.routes),
|
|
3714
|
+
assets: all.flatMap((t) => t.assets),
|
|
3715
|
+
locale: first.locale
|
|
3716
|
+
});
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3719
|
+
// src/source-payloads.ts
|
|
3720
|
+
function collectBindingSourceIds(node, into = /* @__PURE__ */ new Set()) {
|
|
3721
|
+
for (const value of Object.values(node.props ?? {})) {
|
|
3722
|
+
if (value.kind === "binding") into.add(value.binding.sourceId);
|
|
3723
|
+
}
|
|
3724
|
+
if (node.condition) into.add(node.condition.source.sourceId);
|
|
3725
|
+
for (const children of Object.values(node.slots ?? {})) {
|
|
3726
|
+
for (const child of children) collectBindingSourceIds(child, into);
|
|
3727
|
+
}
|
|
3728
|
+
return into;
|
|
3729
|
+
}
|
|
3730
|
+
function materializeSourcePayloads(input) {
|
|
3731
|
+
const { client, result, graph, sources } = input;
|
|
3732
|
+
const payloads = {};
|
|
3733
|
+
const tags = [];
|
|
3734
|
+
if (result.status !== "ok") return { payloads, tags };
|
|
3735
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
3736
|
+
const layoutId = result.document?.["layoutId"];
|
|
3737
|
+
if (graph && typeof layoutId === "string") {
|
|
3738
|
+
const layout = graph.layouts.find((candidate) => candidate.id === layoutId);
|
|
3739
|
+
if (layout) collectBindingSourceIds(layout.root, referenced);
|
|
3740
|
+
}
|
|
3741
|
+
const byId = new Map(sources.map((source) => [source.id, source]));
|
|
3742
|
+
for (const sourceId of [...referenced].sort()) {
|
|
3743
|
+
if (!isSectionSourceId(sourceId)) continue;
|
|
3744
|
+
const definition = byId.get(sourceId);
|
|
3745
|
+
if (!definition || definition.kind !== "cms") continue;
|
|
3746
|
+
const config = parseCmsSourceQueryConfig(definition.config);
|
|
3747
|
+
if (!config || config.documentId !== void 0) continue;
|
|
3748
|
+
const list = client.listDocuments(config.collectionId, listQueryOfCmsConfig(config));
|
|
3749
|
+
tags.push(list.tags);
|
|
3750
|
+
if (list.ok) {
|
|
3751
|
+
payloads[sourceId] = list.documents;
|
|
3752
|
+
} else {
|
|
3753
|
+
payloads[sourceId] = sourcePayloadError(
|
|
3754
|
+
sourceId,
|
|
3755
|
+
list.errors.map((error) => error.message)
|
|
3756
|
+
);
|
|
3757
|
+
}
|
|
3758
|
+
}
|
|
3759
|
+
return { payloads, tags };
|
|
3760
|
+
}
|
|
3761
|
+
|
|
3762
|
+
// src/snapshot.ts
|
|
3763
|
+
var DEFAULT_LOCALES = { default: "en", locales: ["en"] };
|
|
3764
|
+
function patternKey(pattern) {
|
|
3765
|
+
return "/" + splitPath(pattern).join("/");
|
|
3766
|
+
}
|
|
3767
|
+
function mergeRouteRecords(stored, fallback) {
|
|
3768
|
+
const agnostic = /* @__PURE__ */ new Set();
|
|
3769
|
+
const localized = /* @__PURE__ */ new Set();
|
|
3770
|
+
for (const route of stored) {
|
|
3771
|
+
if (route.locale === void 0) agnostic.add(patternKey(route.pattern));
|
|
3772
|
+
else localized.add(`${route.locale}|${patternKey(route.pattern)}`);
|
|
3773
|
+
}
|
|
3774
|
+
const covered = (route) => {
|
|
3775
|
+
const key = patternKey(route.pattern);
|
|
3776
|
+
if (agnostic.has(key)) return true;
|
|
3777
|
+
return route.locale !== void 0 && localized.has(`${route.locale}|${key}`);
|
|
3778
|
+
};
|
|
3779
|
+
return [...stored, ...fallback.filter((route) => !covered(route))];
|
|
3780
|
+
}
|
|
3781
|
+
function createStaticContentSource(data) {
|
|
3782
|
+
const locales = data.locales ?? DEFAULT_LOCALES;
|
|
3783
|
+
const routes = data.routes ?? [];
|
|
3784
|
+
const redirects = data.redirects ?? [];
|
|
3785
|
+
const draftGraph = data.graphs?.draft ?? null;
|
|
3786
|
+
const publishedGraph = data.graphs?.published ?? null;
|
|
3787
|
+
const assetUrl = data.assetUrl ?? ((asset) => `asset://unresolved/${asset.storageKey}`);
|
|
3788
|
+
const collections = /* @__PURE__ */ new Map();
|
|
3789
|
+
for (const collection of data.collections ?? []) collections.set(collection.id, collection);
|
|
3790
|
+
const documents = /* @__PURE__ */ new Map();
|
|
3791
|
+
const byCollection = /* @__PURE__ */ new Map();
|
|
3792
|
+
for (const source of data.documents ?? []) {
|
|
3793
|
+
const doc = isDocumentHistory(source) ? source.draft : source;
|
|
3794
|
+
documents.set(documentKey(doc), source);
|
|
3795
|
+
const list = byCollection.get(doc.collection) ?? [];
|
|
3796
|
+
list.push(source);
|
|
3797
|
+
byCollection.set(doc.collection, list);
|
|
3798
|
+
}
|
|
3799
|
+
const assets = /* @__PURE__ */ new Map();
|
|
3800
|
+
for (const record of data.assets ?? []) assets.set(record.id, record);
|
|
3801
|
+
const graphFor = (perspective) => perspective === "draft" ? draftGraph : publishedGraph;
|
|
3802
|
+
const lookupFor = (_perspective) => ({
|
|
3803
|
+
collection: (id) => collections.get(id),
|
|
3804
|
+
document: (ref) => documents.get(documentKey(ref)),
|
|
3805
|
+
documents: (collectionId) => byCollection.get(collectionId) ?? [],
|
|
3806
|
+
asset: (id) => assets.get(id),
|
|
3807
|
+
assetUrl: (asset) => asset.url ?? assetUrl(asset)
|
|
3808
|
+
});
|
|
3809
|
+
const client = (init) => createContentClient({
|
|
3810
|
+
lookup: lookupFor(init.perspective),
|
|
3811
|
+
context: createContentContext({
|
|
3812
|
+
locale: init.locale ?? locales.default,
|
|
3813
|
+
perspective: init.perspective,
|
|
3814
|
+
locales
|
|
3815
|
+
}),
|
|
3816
|
+
routes,
|
|
3817
|
+
redirects
|
|
3818
|
+
});
|
|
3819
|
+
return {
|
|
3820
|
+
projectId: data.projectId,
|
|
3821
|
+
locales,
|
|
3822
|
+
client,
|
|
3823
|
+
graph: graphFor,
|
|
3824
|
+
routes,
|
|
3825
|
+
// EC-195: the delivery-shaped assets, so the renderer can resolve an
|
|
3826
|
+
// `asset`-typed Image prop (an asset id) to a usable url + intrinsic
|
|
3827
|
+
// dimensions. Baked into the (serializable) render outcome by the route
|
|
3828
|
+
// helper, so a cached page keeps its images during a backend outage.
|
|
3829
|
+
assets: () => [...assets.values()].map((record) => resolvedAsset(record, record.url ?? assetUrl(record))),
|
|
3830
|
+
...data.sources ? { sources: data.sources } : {}
|
|
3831
|
+
};
|
|
3832
|
+
}
|
|
3833
|
+
async function createContentSnapshot(adapter, projectId, options = {}) {
|
|
3834
|
+
const schema = await adapter.cms.findSchema(projectId);
|
|
3835
|
+
const documentRecords = await adapter.cms.listDocuments(projectId);
|
|
3836
|
+
const versionRecords = await adapter.cms.listAllDocumentVersions(projectId);
|
|
3837
|
+
const assetRecords = await adapter.assets.listAssets(projectId);
|
|
3838
|
+
const versionsByKey = /* @__PURE__ */ new Map();
|
|
3839
|
+
for (const record of versionRecords) {
|
|
3840
|
+
const key = `${record.collection}:${record.docId}`;
|
|
3841
|
+
let byVersion = versionsByKey.get(key);
|
|
3842
|
+
if (!byVersion) {
|
|
3843
|
+
byVersion = /* @__PURE__ */ new Map();
|
|
3844
|
+
versionsByKey.set(key, byVersion);
|
|
3845
|
+
}
|
|
3846
|
+
byVersion.set(record.version, record.snapshot);
|
|
3847
|
+
}
|
|
3848
|
+
const documents = documentRecords.map((record) => {
|
|
3849
|
+
if (record.publishedVersion === void 0) return record.document;
|
|
3850
|
+
const published = versionsByKey.get(`${record.document.collection}:${record.document.id}`)?.get(record.publishedVersion);
|
|
3851
|
+
return { draft: record.document, published, versions: [] };
|
|
3852
|
+
});
|
|
3853
|
+
const draftGraph = await adapter.graphs.hasGraph(projectId) ? (await adapter.graphs.getLatest(projectId)).graph : null;
|
|
3854
|
+
const publishState = await adapter.publishing.findState(projectId);
|
|
3855
|
+
const publishedGraph = publishState?.status === "published" && publishState.publishedRevision !== void 0 ? (await adapter.graphs.getRevision(projectId, publishState.publishedRevision)).graph : null;
|
|
3856
|
+
return createStaticContentSource({
|
|
3857
|
+
projectId,
|
|
3858
|
+
...options.locales ? { locales: options.locales } : {},
|
|
3859
|
+
...options.routes ? { routes: options.routes } : {},
|
|
3860
|
+
...options.redirects ? { redirects: options.redirects } : {},
|
|
3861
|
+
...options.sources ? { sources: options.sources } : {},
|
|
3862
|
+
...schema ? { collections: schema.collections } : {},
|
|
3863
|
+
documents,
|
|
3864
|
+
assets: assetRecords,
|
|
3865
|
+
graphs: { draft: draftGraph, published: publishedGraph },
|
|
3866
|
+
assetUrl: (asset) => adapter.blobs.url(asset.storageKey)
|
|
3867
|
+
});
|
|
3868
|
+
}
|
|
3869
|
+
|
|
3870
|
+
// src/route-core.ts
|
|
3871
|
+
function pathFromSlug(slug) {
|
|
3872
|
+
return "/" + (slug ?? []).join("/");
|
|
3873
|
+
}
|
|
3874
|
+
function splitLocalePath(url, locales) {
|
|
3875
|
+
const segments = splitPath(url);
|
|
3876
|
+
const first = segments[0];
|
|
3877
|
+
if (first !== void 0 && first !== locales.default && locales.locales.includes(first)) {
|
|
3878
|
+
return { locale: first, path: "/" + segments.slice(1).join("/") };
|
|
3879
|
+
}
|
|
3880
|
+
return { locale: locales.default, path: "/" + segments.join("/") };
|
|
3881
|
+
}
|
|
3882
|
+
function scopeAssetsToPage(allAssets, registry, result, graph) {
|
|
3883
|
+
if (allAssets.length === 0) return allAssets;
|
|
3884
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3885
|
+
for (const node of compositionNodesOf(result.document)) {
|
|
3886
|
+
collectReferencedAssetIds(node, registry, ids);
|
|
3887
|
+
}
|
|
3888
|
+
const layoutId = result.document?.["layoutId"];
|
|
3889
|
+
if (graph && typeof layoutId === "string") {
|
|
3890
|
+
const layout = graph.layouts.find((candidate) => candidate.id === layoutId);
|
|
3891
|
+
if (layout) collectReferencedAssetIds(layout.root, registry, ids);
|
|
3892
|
+
}
|
|
3893
|
+
const ogImage = result.document?.["seoOgImage"];
|
|
3894
|
+
if (typeof ogImage === "string") ids.add(ogImage);
|
|
3895
|
+
return ids.size === 0 ? [] : allAssets.filter((asset) => ids.has(asset.id));
|
|
3896
|
+
}
|
|
3897
|
+
function pageCompositionNodes(result, graph) {
|
|
3898
|
+
const nodes = [...compositionNodesOf(result.document)];
|
|
3899
|
+
const layoutId = result.document?.["layoutId"];
|
|
3900
|
+
if (graph && typeof layoutId === "string") {
|
|
3901
|
+
const layout = graph.layouts.find((candidate) => candidate.id === layoutId);
|
|
3902
|
+
if (layout) nodes.push(layout.root);
|
|
3903
|
+
}
|
|
3904
|
+
return nodes;
|
|
3905
|
+
}
|
|
3906
|
+
function populatePageRelations(result, graph, client, registry) {
|
|
3907
|
+
if (!registry) return { relations: {}, docKeys: [] };
|
|
3908
|
+
const refKeys = /* @__PURE__ */ new Set();
|
|
3909
|
+
const relations = collectReferencedRelations(
|
|
3910
|
+
pageCompositionNodes(result, graph),
|
|
3911
|
+
registry,
|
|
3912
|
+
(ref) => client.resolveRelation(ref),
|
|
3913
|
+
{},
|
|
3914
|
+
refKeys
|
|
3915
|
+
);
|
|
3916
|
+
return { relations, docKeys: [...refKeys] };
|
|
3917
|
+
}
|
|
3918
|
+
function populatePageListings(result, graph, client, registry) {
|
|
3919
|
+
if (!registry) return { listings: {}, tags: [] };
|
|
3920
|
+
const tags = [];
|
|
3921
|
+
const listings = collectReferencedListings(
|
|
3922
|
+
pageCompositionNodes(result, graph),
|
|
3923
|
+
registry,
|
|
3924
|
+
(query) => {
|
|
3925
|
+
const listed = client.listDocuments(query.collection, {
|
|
3926
|
+
where: query.where,
|
|
3927
|
+
...query.sort ? { sort: query.sort } : {},
|
|
3928
|
+
...query.limit !== void 0 ? { limit: query.limit } : {}
|
|
3929
|
+
});
|
|
3930
|
+
tags.push(listed.tags);
|
|
3931
|
+
return listed.ok ? listed.documents : [];
|
|
3932
|
+
},
|
|
3933
|
+
// EC-255: reach listings nested inside transcluded reusable content (the same
|
|
3934
|
+
// depth-1 populate the relation bake uses), so a grid inside a reusable bakes —
|
|
3935
|
+
// and its membership tag is folded in — instead of rendering empty on the host.
|
|
3936
|
+
(ref) => client.resolveRelation(ref)
|
|
3937
|
+
);
|
|
3938
|
+
return { listings, tags };
|
|
3939
|
+
}
|
|
3940
|
+
async function resolveCanvasPage(source, url, perspective, options = {}) {
|
|
3941
|
+
const { locale, path } = splitLocalePath(url, source.locales);
|
|
3942
|
+
const client = source.client({ perspective, locale });
|
|
3943
|
+
const result = client.resolvePage(path);
|
|
3944
|
+
if (result.status === "redirect" && result.redirect) {
|
|
3945
|
+
return {
|
|
3946
|
+
kind: "redirect",
|
|
3947
|
+
target: result.redirect.target,
|
|
3948
|
+
permanent: result.redirect.permanent,
|
|
3949
|
+
tags: result.tags
|
|
3950
|
+
};
|
|
3951
|
+
}
|
|
3952
|
+
if (result.status !== "ok") {
|
|
3953
|
+
return { kind: "not-found", tags: result.tags };
|
|
3954
|
+
}
|
|
3955
|
+
const graph = source.graph(perspective);
|
|
3956
|
+
const extraTags = [];
|
|
3957
|
+
let payloads = defaultBindingPayloads(result);
|
|
3958
|
+
const materialized = materializeSourcePayloads({
|
|
3959
|
+
client,
|
|
3960
|
+
result,
|
|
3961
|
+
graph,
|
|
3962
|
+
sources: source.sources ?? []
|
|
3963
|
+
});
|
|
3964
|
+
payloads = { ...payloads, ...materialized.payloads };
|
|
3965
|
+
extraTags.push(...materialized.tags);
|
|
3966
|
+
if (options.payloads) {
|
|
3967
|
+
const custom = await options.payloads({
|
|
3968
|
+
client,
|
|
3969
|
+
result,
|
|
3970
|
+
perspective,
|
|
3971
|
+
path,
|
|
3972
|
+
params: result.dynamicParams,
|
|
3973
|
+
addTags: (tags) => extraTags.push(tags)
|
|
3974
|
+
});
|
|
3975
|
+
payloads = { ...payloads, ...custom };
|
|
3976
|
+
}
|
|
3977
|
+
const allAssets = source.assets?.() ?? [];
|
|
3978
|
+
const bakedAssets = options.registry ? scopeAssetsToPage(allAssets, options.registry, result, graph) : allAssets;
|
|
3979
|
+
const assetTags = cacheTags({
|
|
3980
|
+
assets: bakedAssets.map((asset) => asset.id),
|
|
3981
|
+
locale: result.tags.locale
|
|
3982
|
+
});
|
|
3983
|
+
const { relations, docKeys: relationDocKeys } = populatePageRelations(
|
|
3984
|
+
result,
|
|
3985
|
+
graph,
|
|
3986
|
+
client,
|
|
3987
|
+
options.registry
|
|
3988
|
+
);
|
|
3989
|
+
const relationTags = cacheTags({ docs: relationDocKeys, locale: result.tags.locale });
|
|
3990
|
+
const { listings, tags: listingTags } = populatePageListings(
|
|
3991
|
+
result,
|
|
3992
|
+
graph,
|
|
3993
|
+
client,
|
|
3994
|
+
options.registry
|
|
3995
|
+
);
|
|
3996
|
+
return {
|
|
3997
|
+
kind: "render",
|
|
3998
|
+
result,
|
|
3999
|
+
graph,
|
|
4000
|
+
payloads,
|
|
4001
|
+
assets: bakedAssets,
|
|
4002
|
+
relations,
|
|
4003
|
+
listings,
|
|
4004
|
+
tags: mergeCacheTags(result.tags, ...extraTags, ...listingTags, assetTags, relationTags)
|
|
4005
|
+
};
|
|
4006
|
+
}
|
|
4007
|
+
|
|
4008
|
+
// src/cache.ts
|
|
4009
|
+
function scopeCacheTag(tag, scope) {
|
|
4010
|
+
return scope === void 0 || scope.length === 0 ? tag : `p:${scope}:${tag}`;
|
|
4011
|
+
}
|
|
4012
|
+
function docCacheTag(docKey, scope) {
|
|
4013
|
+
return scopeCacheTag(`doc:${docKey}`, scope);
|
|
4014
|
+
}
|
|
4015
|
+
function collectionCacheTag(collection, locale, scope) {
|
|
4016
|
+
return scopeCacheTag(`collection:${collection}:${locale}`, scope);
|
|
4017
|
+
}
|
|
4018
|
+
function routeCacheTag(pattern, locale, scope) {
|
|
4019
|
+
return scopeCacheTag(`route:${pattern}:${locale}`, scope);
|
|
4020
|
+
}
|
|
4021
|
+
function assetCacheTag(assetId, scope) {
|
|
4022
|
+
return scopeCacheTag(`asset:${assetId}`, scope);
|
|
4023
|
+
}
|
|
4024
|
+
function projectSweepTag(scope) {
|
|
4025
|
+
return scopeCacheTag("project", scope);
|
|
4026
|
+
}
|
|
4027
|
+
function nextCacheTags(tags, scope) {
|
|
4028
|
+
const all = /* @__PURE__ */ new Set();
|
|
4029
|
+
all.add(projectSweepTag(scope));
|
|
4030
|
+
for (const docKey of tags.docs) all.add(docCacheTag(docKey, scope));
|
|
4031
|
+
for (const collection of tags.collections) {
|
|
4032
|
+
all.add(collectionCacheTag(collection, tags.locale, scope));
|
|
4033
|
+
}
|
|
4034
|
+
for (const route of tags.routes) all.add(routeCacheTag(route, tags.locale, scope));
|
|
4035
|
+
for (const assetId of tags.assets) all.add(assetCacheTag(assetId, scope));
|
|
4036
|
+
return [...all].sort();
|
|
4037
|
+
}
|
|
4038
|
+
var CacheDiscoveryBail = class extends Error {
|
|
4039
|
+
constructor() {
|
|
4040
|
+
super("elytra cache discovery bail (never user-visible)");
|
|
4041
|
+
this.name = "CacheDiscoveryBail";
|
|
4042
|
+
}
|
|
4043
|
+
};
|
|
4044
|
+
function passthrough(impl) {
|
|
4045
|
+
return async () => impl();
|
|
4046
|
+
}
|
|
4047
|
+
async function loadCachedEntry(cache, keyParts, baselineTags, load) {
|
|
4048
|
+
let fresh;
|
|
4049
|
+
const probe = cache(
|
|
4050
|
+
passthrough(async () => {
|
|
4051
|
+
fresh = await load();
|
|
4052
|
+
throw new CacheDiscoveryBail();
|
|
4053
|
+
}),
|
|
4054
|
+
keyParts,
|
|
4055
|
+
{ tags: [...baselineTags], revalidate: false }
|
|
4056
|
+
);
|
|
4057
|
+
try {
|
|
4058
|
+
return await probe();
|
|
4059
|
+
} catch (error) {
|
|
4060
|
+
if (!(error instanceof CacheDiscoveryBail)) throw error;
|
|
4061
|
+
}
|
|
4062
|
+
const entry = fresh;
|
|
4063
|
+
const write = cache(
|
|
4064
|
+
passthrough(async () => entry),
|
|
4065
|
+
keyParts,
|
|
4066
|
+
{ tags: entry.tags, revalidate: false }
|
|
4067
|
+
);
|
|
4068
|
+
return write();
|
|
4069
|
+
}
|
|
4070
|
+
|
|
4071
|
+
// src/metadata.ts
|
|
4072
|
+
function str2(value) {
|
|
4073
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
4074
|
+
}
|
|
4075
|
+
function looksLikeUrl(value) {
|
|
4076
|
+
return /^(https?:)?\/\//.test(value) || value.startsWith("/");
|
|
4077
|
+
}
|
|
4078
|
+
function resolveOgImage(value, assets) {
|
|
4079
|
+
if (value === null) return null;
|
|
4080
|
+
if (looksLikeUrl(value)) return value;
|
|
4081
|
+
return assets.find((asset) => asset.id === value)?.url ?? null;
|
|
4082
|
+
}
|
|
4083
|
+
function canvasPageMetadata(result, assets = []) {
|
|
4084
|
+
const doc = result.document;
|
|
4085
|
+
const seoTitle = str2(doc?.["seoTitle"]);
|
|
4086
|
+
const seoDescription = str2(doc?.["seoDescription"]);
|
|
4087
|
+
const seoOgTitle = str2(doc?.["seoOgTitle"]);
|
|
4088
|
+
const seoOgImage = resolveOgImage(str2(doc?.["seoOgImage"]), assets);
|
|
4089
|
+
const seoNoindex = doc?.["seoNoindex"] === true;
|
|
4090
|
+
const seoCanonical = str2(doc?.["seoCanonical"]);
|
|
4091
|
+
const docTitle = str2(doc?.["name"]) ?? str2(doc?.["title"]);
|
|
4092
|
+
const title = seoTitle ?? docTitle;
|
|
4093
|
+
const metadata = {};
|
|
4094
|
+
if (title !== null) metadata.title = title;
|
|
4095
|
+
if (seoDescription) metadata.description = seoDescription;
|
|
4096
|
+
const ogTitle = seoOgTitle ?? title;
|
|
4097
|
+
if (ogTitle !== null || seoDescription || seoOgImage) {
|
|
4098
|
+
metadata.openGraph = {
|
|
4099
|
+
...ogTitle !== null ? { title: ogTitle } : {},
|
|
4100
|
+
...seoDescription ? { description: seoDescription } : {},
|
|
4101
|
+
...seoOgImage ? { images: [{ url: seoOgImage }] } : {}
|
|
4102
|
+
};
|
|
4103
|
+
}
|
|
4104
|
+
if (seoNoindex) metadata.robots = { index: false };
|
|
4105
|
+
if (seoCanonical) metadata.alternates = { canonical: seoCanonical };
|
|
4106
|
+
return metadata;
|
|
4107
|
+
}
|
|
4108
|
+
function defaultMetadata(result, assets) {
|
|
4109
|
+
return canvasPageMetadata(result, assets);
|
|
4110
|
+
}
|
|
4111
|
+
async function requestPerspective() {
|
|
4112
|
+
const { isEnabled } = await draftMode();
|
|
4113
|
+
return isEnabled ? "draft" : "published";
|
|
4114
|
+
}
|
|
4115
|
+
var nextCacheWrapper = (cb, keyParts, options) => unstable_cache(cb, keyParts, { tags: options.tags, revalidate: options.revalidate });
|
|
4116
|
+
function createCanvasRoute(options) {
|
|
4117
|
+
const loadOutcome = async (url, perspective) => {
|
|
4118
|
+
const resolveFresh = async () => {
|
|
4119
|
+
const source = await options.loadContent({ perspective });
|
|
4120
|
+
return resolveCanvasPage(source, url, perspective, {
|
|
4121
|
+
...options.payloads ? { payloads: options.payloads } : {},
|
|
4122
|
+
// EC-205: page-scope the baked assets to what the page actually uses.
|
|
4123
|
+
registry: options.components.registry
|
|
4124
|
+
});
|
|
4125
|
+
};
|
|
4126
|
+
const devBypass = process.env.NODE_ENV === "development";
|
|
4127
|
+
if (!options.cache || perspective !== "published" || devBypass) return resolveFresh();
|
|
4128
|
+
const scope = options.cache.scope;
|
|
4129
|
+
const entry = await loadCachedEntry(
|
|
4130
|
+
nextCacheWrapper,
|
|
4131
|
+
["elytra-route", scope ?? "", url],
|
|
4132
|
+
[projectSweepTag(scope)],
|
|
4133
|
+
async () => {
|
|
4134
|
+
const outcome = await resolveFresh();
|
|
4135
|
+
return { value: outcome, tags: nextCacheTags(outcome.tags, scope) };
|
|
4136
|
+
}
|
|
4137
|
+
);
|
|
4138
|
+
return entry.value;
|
|
4139
|
+
};
|
|
4140
|
+
const Page = async (props) => {
|
|
4141
|
+
const { slug } = await props.params;
|
|
4142
|
+
const url = pathFromSlug(slug);
|
|
4143
|
+
const perspective = await requestPerspective();
|
|
4144
|
+
const outcome = await loadOutcome(url, perspective);
|
|
4145
|
+
options.onTags?.(outcome.tags, { url, perspective });
|
|
4146
|
+
switch (outcome.kind) {
|
|
4147
|
+
case "redirect":
|
|
4148
|
+
return outcome.permanent ? permanentRedirect(outcome.target) : redirect(outcome.target);
|
|
4149
|
+
case "not-found":
|
|
4150
|
+
return notFound();
|
|
4151
|
+
case "render":
|
|
4152
|
+
return /* @__PURE__ */ jsx(
|
|
4153
|
+
CanvasRenderer,
|
|
4154
|
+
{
|
|
4155
|
+
result: outcome.result,
|
|
4156
|
+
graph: outcome.graph,
|
|
4157
|
+
components: options.components,
|
|
4158
|
+
payloads: outcome.payloads,
|
|
4159
|
+
assets: outcome.assets,
|
|
4160
|
+
relations: outcome.relations,
|
|
4161
|
+
listings: outcome.listings
|
|
4162
|
+
}
|
|
4163
|
+
);
|
|
4164
|
+
}
|
|
4165
|
+
};
|
|
4166
|
+
const generateMetadata = async (props) => {
|
|
4167
|
+
const { slug } = await props.params;
|
|
4168
|
+
const url = pathFromSlug(slug);
|
|
4169
|
+
const perspective = await requestPerspective();
|
|
4170
|
+
if (options.cache && perspective === "published") {
|
|
4171
|
+
const outcome2 = await loadOutcome(url, perspective);
|
|
4172
|
+
if (outcome2.kind !== "render") return {};
|
|
4173
|
+
return options.metadata ? options.metadata(outcome2.result, { url, perspective }) : defaultMetadata(outcome2.result, outcome2.assets);
|
|
4174
|
+
}
|
|
4175
|
+
const source = await options.loadContent({ perspective });
|
|
4176
|
+
const outcome = await resolveCanvasPage(source, url, perspective);
|
|
4177
|
+
if (outcome.kind !== "render") return {};
|
|
4178
|
+
return options.metadata ? options.metadata(outcome.result, { url, perspective }) : defaultMetadata(outcome.result, outcome.assets);
|
|
4179
|
+
};
|
|
4180
|
+
return { Page, generateMetadata };
|
|
4181
|
+
}
|
|
4182
|
+
|
|
4183
|
+
// src/sitemap.ts
|
|
4184
|
+
function paramNamesOf(pattern) {
|
|
4185
|
+
return splitPath(pattern).filter((segment) => segment.startsWith(":")).map((segment) => segment.slice(1));
|
|
4186
|
+
}
|
|
4187
|
+
function fillPattern(pattern, params) {
|
|
4188
|
+
const segments = [];
|
|
4189
|
+
for (const segment of splitPath(pattern)) {
|
|
4190
|
+
if (!segment.startsWith(":")) {
|
|
4191
|
+
segments.push(segment);
|
|
4192
|
+
continue;
|
|
4193
|
+
}
|
|
4194
|
+
const value = params[segment.slice(1)];
|
|
4195
|
+
if (value === void 0 || value.length === 0) return null;
|
|
4196
|
+
segments.push(encodeURIComponent(value));
|
|
4197
|
+
}
|
|
4198
|
+
return "/" + segments.join("/");
|
|
4199
|
+
}
|
|
4200
|
+
function absoluteUrl(baseUrl, locale, defaultLocale, path) {
|
|
4201
|
+
const origin = baseUrl.replace(/\/+$/, "");
|
|
4202
|
+
if (locale === defaultLocale) return origin + path;
|
|
4203
|
+
return origin + (path === "/" ? `/${locale}` : `/${locale}${path}`);
|
|
4204
|
+
}
|
|
4205
|
+
function canvasSitemapEntries(source, options) {
|
|
4206
|
+
const routes = options.routes ?? source.routes ?? [];
|
|
4207
|
+
const locales = source.locales;
|
|
4208
|
+
const skipped = [];
|
|
4209
|
+
const skip = (route, reason) => {
|
|
4210
|
+
if (!skipped.some((s) => s.routeId === route.id && s.reason === reason)) {
|
|
4211
|
+
skipped.push({ routeId: route.id, pattern: route.pattern, reason });
|
|
4212
|
+
}
|
|
4213
|
+
};
|
|
4214
|
+
const clients = /* @__PURE__ */ new Map();
|
|
4215
|
+
const clientFor = (locale) => {
|
|
4216
|
+
let client = clients.get(locale);
|
|
4217
|
+
if (!client) {
|
|
4218
|
+
client = source.client({ perspective: "published", locale });
|
|
4219
|
+
clients.set(locale, client);
|
|
4220
|
+
}
|
|
4221
|
+
return client;
|
|
4222
|
+
};
|
|
4223
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
4224
|
+
for (const route of routes) {
|
|
4225
|
+
const paramNames = paramNamesOf(route.pattern);
|
|
4226
|
+
const mount = hierarchyMount(route);
|
|
4227
|
+
const routeLocales = route.locale ? [route.locale] : locales.locales;
|
|
4228
|
+
for (const locale of routeLocales) {
|
|
4229
|
+
const client = clientFor(locale);
|
|
4230
|
+
let paths;
|
|
4231
|
+
if (mount) {
|
|
4232
|
+
const listed = client.listDocuments(mount.collection);
|
|
4233
|
+
if (!listed.ok) {
|
|
4234
|
+
skip(route, `hierarchy collection "${mount.collection}" could not be listed`);
|
|
4235
|
+
continue;
|
|
4236
|
+
}
|
|
4237
|
+
const rows = listed.documents.map((doc) => ({
|
|
4238
|
+
collection: mount.collection,
|
|
4239
|
+
id: doc.id,
|
|
4240
|
+
values: { slug: doc["slug"], parent: doc["parent"] }
|
|
4241
|
+
}));
|
|
4242
|
+
const byId = new Map(rows.map((row) => [row.id, row]));
|
|
4243
|
+
paths = [];
|
|
4244
|
+
for (const row of rows) {
|
|
4245
|
+
if (typeof row.values["slug"] !== "string" || row.values["slug"] === "") continue;
|
|
4246
|
+
const composed = composePath(row, (id) => byId.get(id));
|
|
4247
|
+
if (!composed.ok) continue;
|
|
4248
|
+
paths.push("/" + [...mount.prefix, ...composed.segments].join("/"));
|
|
4249
|
+
}
|
|
4250
|
+
} else if (paramNames.length === 0) {
|
|
4251
|
+
paths = [route.pattern];
|
|
4252
|
+
} else {
|
|
4253
|
+
const paramSets = options.params?.({ route, locale, client });
|
|
4254
|
+
if (paramSets == null) {
|
|
4255
|
+
skip(route, "dynamic route not enumerated (no params provider entry)");
|
|
4256
|
+
continue;
|
|
4257
|
+
}
|
|
4258
|
+
paths = [];
|
|
4259
|
+
for (const paramSet of paramSets) {
|
|
4260
|
+
const path = fillPattern(route.pattern, paramSet);
|
|
4261
|
+
if (path === null) {
|
|
4262
|
+
skip(route, `params entry missing one of :${paramNames.join(", :")}`);
|
|
4263
|
+
continue;
|
|
4264
|
+
}
|
|
4265
|
+
paths.push(path);
|
|
4266
|
+
}
|
|
4267
|
+
}
|
|
4268
|
+
for (const path of paths) {
|
|
4269
|
+
const result = client.resolvePage(path, locale);
|
|
4270
|
+
if (result.status !== "ok") continue;
|
|
4271
|
+
if (result.document?.["seoNoindex"] === true) continue;
|
|
4272
|
+
const perLocale = byPath.get(path) ?? /* @__PURE__ */ new Map();
|
|
4273
|
+
perLocale.set(locale, absoluteUrl(options.baseUrl, locale, locales.default, path));
|
|
4274
|
+
byPath.set(path, perLocale);
|
|
4275
|
+
}
|
|
4276
|
+
}
|
|
4277
|
+
}
|
|
4278
|
+
const entries = [];
|
|
4279
|
+
for (const perLocale of byPath.values()) {
|
|
4280
|
+
const languages = Object.fromEntries(perLocale);
|
|
4281
|
+
for (const url of perLocale.values()) {
|
|
4282
|
+
entries.push({
|
|
4283
|
+
url,
|
|
4284
|
+
...perLocale.size > 1 ? { alternates: { languages } } : {}
|
|
4285
|
+
});
|
|
4286
|
+
}
|
|
4287
|
+
}
|
|
4288
|
+
entries.sort((a, b) => a.url < b.url ? -1 : a.url > b.url ? 1 : 0);
|
|
4289
|
+
return { entries, skipped };
|
|
4290
|
+
}
|
|
4291
|
+
function createCanvasSitemap(options) {
|
|
4292
|
+
return async () => {
|
|
4293
|
+
const source = await options.loadContent({ perspective: "published" });
|
|
4294
|
+
return canvasSitemapEntries(source, options).entries;
|
|
4295
|
+
};
|
|
4296
|
+
}
|
|
4297
|
+
var REVALIDATE_SIGNATURE_HEADER = "x-elytra-signature";
|
|
4298
|
+
var revalidatePayloadSchema = z.object({
|
|
4299
|
+
projectId: z.string().min(1),
|
|
4300
|
+
/** The operation that fired, e.g. `documents.setState`, `publishing.publish`. */
|
|
4301
|
+
event: z.string().min(1),
|
|
4302
|
+
/** Fully-formatted Next tag strings (already scoped) — revalidated verbatim. */
|
|
4303
|
+
tags: z.array(z.string().min(1)),
|
|
4304
|
+
/** ISO timestamp of emission. Informational in v1 (no replay window). */
|
|
4305
|
+
timestamp: z.string().min(1)
|
|
4306
|
+
});
|
|
4307
|
+
function signRevalidateBody(rawBody, secret) {
|
|
4308
|
+
return `sha256=${createHmac("sha256", secret).update(rawBody, "utf8").digest("hex")}`;
|
|
4309
|
+
}
|
|
4310
|
+
var SIGNATURE_PREFIX = "sha256=";
|
|
4311
|
+
var HEX_PATTERN = /^[0-9a-f]+$/;
|
|
4312
|
+
function evaluateRevalidateRequest(rawBody, signature, secret) {
|
|
4313
|
+
if (secret === void 0 || secret.length === 0) {
|
|
4314
|
+
return { ok: false, status: 401, message: "Revalidation webhook is not configured." };
|
|
4315
|
+
}
|
|
4316
|
+
if (signature === null || signature === void 0 || !signature.startsWith(SIGNATURE_PREFIX)) {
|
|
4317
|
+
return { ok: false, status: 401, message: "Missing or malformed webhook signature." };
|
|
4318
|
+
}
|
|
4319
|
+
const providedHex = signature.slice(SIGNATURE_PREFIX.length).toLowerCase();
|
|
4320
|
+
const expectedHex = createHmac("sha256", secret).update(rawBody, "utf8").digest("hex");
|
|
4321
|
+
if (providedHex.length !== expectedHex.length || !HEX_PATTERN.test(providedHex) || !timingSafeEqual(Buffer.from(providedHex, "hex"), Buffer.from(expectedHex, "hex"))) {
|
|
4322
|
+
return { ok: false, status: 401, message: "Invalid webhook signature." };
|
|
4323
|
+
}
|
|
4324
|
+
let parsed;
|
|
4325
|
+
try {
|
|
4326
|
+
parsed = JSON.parse(rawBody);
|
|
4327
|
+
} catch {
|
|
4328
|
+
return { ok: false, status: 400, message: "Webhook body is not valid JSON." };
|
|
4329
|
+
}
|
|
4330
|
+
const payload = revalidatePayloadSchema.safeParse(parsed);
|
|
4331
|
+
if (!payload.success) {
|
|
4332
|
+
return { ok: false, status: 400, message: "Webhook payload has an invalid shape." };
|
|
4333
|
+
}
|
|
4334
|
+
return { ok: true, payload: payload.data };
|
|
4335
|
+
}
|
|
4336
|
+
function corsHeaders(allowOrigin) {
|
|
4337
|
+
if (allowOrigin === void 0 || allowOrigin.length === 0) return {};
|
|
4338
|
+
return {
|
|
4339
|
+
"access-control-allow-origin": allowOrigin,
|
|
4340
|
+
"access-control-allow-methods": "POST, OPTIONS",
|
|
4341
|
+
"access-control-allow-headers": `content-type, ${REVALIDATE_SIGNATURE_HEADER}`
|
|
4342
|
+
};
|
|
4343
|
+
}
|
|
4344
|
+
function createRevalidateRoute(options) {
|
|
4345
|
+
const cors = corsHeaders(options.allowOrigin);
|
|
4346
|
+
return {
|
|
4347
|
+
async POST(request) {
|
|
4348
|
+
const rawBody = await request.text();
|
|
4349
|
+
const signature = request.headers.get(REVALIDATE_SIGNATURE_HEADER);
|
|
4350
|
+
const evaluation = evaluateRevalidateRequest(rawBody, signature, options.secret);
|
|
4351
|
+
if (!evaluation.ok) {
|
|
4352
|
+
return Response.json(
|
|
4353
|
+
{ error: evaluation.message },
|
|
4354
|
+
{ status: evaluation.status, headers: cors }
|
|
4355
|
+
);
|
|
4356
|
+
}
|
|
4357
|
+
for (const tag of evaluation.payload.tags) revalidateTag(tag, "max");
|
|
4358
|
+
return Response.json({ revalidated: evaluation.payload.tags }, { headers: cors });
|
|
4359
|
+
},
|
|
4360
|
+
async OPTIONS() {
|
|
4361
|
+
return new Response(null, { status: 204, headers: cors });
|
|
4362
|
+
}
|
|
4363
|
+
};
|
|
4364
|
+
}
|
|
4365
|
+
function ElytraImage(props) {
|
|
4366
|
+
const { asset, sizes, priority, quality, className, loader } = props;
|
|
4367
|
+
if (!asset || asset.url.length === 0) return null;
|
|
4368
|
+
const alt = asset.alt ?? "";
|
|
4369
|
+
const objectPosition = asset.focalPoint ? `${asset.focalPoint.x * 100}% ${asset.focalPoint.y * 100}%` : void 0;
|
|
4370
|
+
if (asset.width === null || asset.height === null) {
|
|
4371
|
+
return /* @__PURE__ */ jsx(
|
|
4372
|
+
"img",
|
|
4373
|
+
{
|
|
4374
|
+
"data-ec-image": "fallback",
|
|
4375
|
+
src: asset.url,
|
|
4376
|
+
alt,
|
|
4377
|
+
className,
|
|
4378
|
+
...objectPosition ? { style: { objectPosition } } : {}
|
|
4379
|
+
}
|
|
4380
|
+
);
|
|
4381
|
+
}
|
|
4382
|
+
return /* @__PURE__ */ jsx(
|
|
4383
|
+
NextImage,
|
|
4384
|
+
{
|
|
4385
|
+
"data-ec-image": "next",
|
|
4386
|
+
src: asset.url,
|
|
4387
|
+
alt,
|
|
4388
|
+
width: asset.width,
|
|
4389
|
+
height: asset.height,
|
|
4390
|
+
...sizes !== void 0 ? { sizes } : {},
|
|
4391
|
+
...priority !== void 0 ? { priority } : {},
|
|
4392
|
+
...quality !== void 0 ? { quality } : {},
|
|
4393
|
+
...className !== void 0 ? { className } : {},
|
|
4394
|
+
...loader !== void 0 ? { loader } : {},
|
|
4395
|
+
...objectPosition ? { style: { objectPosition } } : {}
|
|
4396
|
+
}
|
|
4397
|
+
);
|
|
4398
|
+
}
|
|
4399
|
+
function createAssetImageLoader(options = {}) {
|
|
4400
|
+
const widthParam = options.widthParam ?? "w";
|
|
4401
|
+
const qualityParam = options.qualityParam ?? "q";
|
|
4402
|
+
const defaultQuality = options.defaultQuality ?? 75;
|
|
4403
|
+
return ({ src, width, quality }) => {
|
|
4404
|
+
const separator = src.includes("?") ? "&" : "?";
|
|
4405
|
+
return `${src}${separator}${widthParam}=${width}&${qualityParam}=${quality ?? defaultQuality}`;
|
|
4406
|
+
};
|
|
4407
|
+
}
|
|
4408
|
+
function NextImagePrimitive(props) {
|
|
4409
|
+
const { url, width, height, alt, focalPoint } = normalizeImageSource(props);
|
|
4410
|
+
if (url.length === 0) return null;
|
|
4411
|
+
return /* @__PURE__ */ jsx(
|
|
4412
|
+
ElytraImage,
|
|
4413
|
+
{
|
|
4414
|
+
asset: { url, width: width ?? null, height: height ?? null, alt, focalPoint },
|
|
4415
|
+
...props.priority === true ? { priority: true } : {}
|
|
4416
|
+
}
|
|
4417
|
+
);
|
|
4418
|
+
}
|
|
4419
|
+
function nextImagePrimitive() {
|
|
4420
|
+
return {
|
|
4421
|
+
manifest: ImageManifest,
|
|
4422
|
+
implementation: NextImagePrimitive
|
|
4423
|
+
};
|
|
4424
|
+
}
|
|
4425
|
+
|
|
4426
|
+
// src/preview-core.ts
|
|
4427
|
+
function safeRedirectTarget(value, fallback = "/") {
|
|
4428
|
+
if (value === null || value === void 0 || value === "") return fallback;
|
|
4429
|
+
if (!value.startsWith("/") || value.startsWith("//")) return null;
|
|
4430
|
+
return value;
|
|
4431
|
+
}
|
|
4432
|
+
function evaluatePreviewRequest(requestUrl, expectedToken) {
|
|
4433
|
+
if (expectedToken === void 0 || expectedToken.length === 0) {
|
|
4434
|
+
return { ok: false, status: 401, message: "Draft preview is not configured." };
|
|
4435
|
+
}
|
|
4436
|
+
let url;
|
|
4437
|
+
try {
|
|
4438
|
+
url = typeof requestUrl === "string" ? new URL(requestUrl, "http://localhost") : requestUrl;
|
|
4439
|
+
} catch {
|
|
4440
|
+
return { ok: false, status: 400, message: "Malformed preview request URL." };
|
|
4441
|
+
}
|
|
4442
|
+
const token = url.searchParams.get("token");
|
|
4443
|
+
if (token !== expectedToken) {
|
|
4444
|
+
return { ok: false, status: 401, message: "Invalid preview token." };
|
|
4445
|
+
}
|
|
4446
|
+
const redirectTo = safeRedirectTarget(url.searchParams.get("redirect"));
|
|
4447
|
+
if (redirectTo === null) {
|
|
4448
|
+
return { ok: false, status: 400, message: "Preview redirect target must be a same-site path." };
|
|
4449
|
+
}
|
|
4450
|
+
return { ok: true, redirectTo };
|
|
4451
|
+
}
|
|
4452
|
+
function createPreviewRoute(options) {
|
|
4453
|
+
return {
|
|
4454
|
+
async GET(request) {
|
|
4455
|
+
const evaluation = evaluatePreviewRequest(request.url, options.token);
|
|
4456
|
+
if (!evaluation.ok) {
|
|
4457
|
+
return new Response(evaluation.message, { status: evaluation.status });
|
|
4458
|
+
}
|
|
4459
|
+
const draft = await draftMode();
|
|
4460
|
+
draft.enable();
|
|
4461
|
+
redirect(evaluation.redirectTo);
|
|
4462
|
+
}
|
|
4463
|
+
};
|
|
4464
|
+
}
|
|
4465
|
+
function createPreviewDisableRoute() {
|
|
4466
|
+
return {
|
|
4467
|
+
async GET(request) {
|
|
4468
|
+
const draft = await draftMode();
|
|
4469
|
+
draft.disable();
|
|
4470
|
+
let target = "/";
|
|
4471
|
+
try {
|
|
4472
|
+
target = safeRedirectTarget(new URL(request.url).searchParams.get("redirect"));
|
|
4473
|
+
} catch {
|
|
4474
|
+
target = "/";
|
|
4475
|
+
}
|
|
4476
|
+
redirect(target ?? "/");
|
|
4477
|
+
}
|
|
4478
|
+
};
|
|
4479
|
+
}
|
|
4480
|
+
var PACKAGE = "@elytracms/next";
|
|
4481
|
+
|
|
4482
|
+
export { CanvasRenderer, ElytraImage, PACKAGE, PROJECT_GRAPH_SCHEMA_VERSION, REVALIDATE_SIGNATURE_HEADER, canvasPageMetadata, canvasSitemapEntries, collectBindingSourceIds, collectionCacheTag, createAssetImageLoader, createBindingResolver, createCanvasRoute, createCanvasSitemap, createContentSnapshot, createPreviewDisableRoute, createPreviewRoute, createRevalidateRoute, createStaticContentSource, defaultBindingPayloads, defineComponent, defineHostComponents, docCacheTag, documentSchema, evaluatePreviewRequest, evaluateRevalidateRequest, isSourcePayloadError, loadCachedEntry, localeConfigSchema, materializeSourcePayloads, mergeCacheTags, mergeRouteRecords, nextCacheTags, nextImagePrimitive, parseProjectGraph, pathFromSlug, projectSweepTag, redirectRecordSchema, resolveCanvasPage, resolvePayloadToken, revalidatePayloadSchema, routeCacheTag, routeRecordSchema, safeRedirectTarget, scopeCacheTag, signRevalidateBody, sourcePayloadError, splitLocalePath };
|
|
4483
|
+
//# sourceMappingURL=index.js.map
|
|
4484
|
+
//# sourceMappingURL=index.js.map
|