@elytracms/next 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,2573 +1,19 @@
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
- }
1
+ import { ComponentRegistry } from '@elytracms/component-registry';
2
+ export { defineComponent } from '@elytracms/component-registry';
3
+ import { basePrimitives, getImplementation, renderCompositionInLayout, renderComposition, ImageManifest, serializeListingQuery, RenderErrorBoundary, collectReferencedAssetIds, collectReferencedRelations, collectReferencedListings, normalizeImageSource } from '@elytracms/runtime-renderer';
4
+ import { isValidElement } from 'react';
5
+ import { jsx, Fragment } from 'react/jsx-runtime';
6
+ import { cacheTags, isSectionSourceId, parseCmsSourceQueryConfig, listQueryOfCmsConfig, isDocumentHistory, resolvedAsset, createContentClient, createContentContext } from '@elytracms/content';
7
+ import { documentKey, splitPath, hierarchyMount, composePath } from '@elytracms/cms-core';
8
+ export { documentSchema, localeConfigSchema, redirectRecordSchema, routeRecordSchema } from '@elytracms/cms-core';
9
+ import { revalidateTag, unstable_cache } from 'next/cache';
10
+ import { draftMode } from 'next/headers';
11
+ import { redirect, notFound, permanentRedirect } from 'next/navigation';
12
+ import { createHmac, timingSafeEqual } from 'crypto';
13
+ import { z } from 'zod';
14
+ export { z } from 'zod';
15
+ import NextImage from 'next/image';
16
+ export { PROJECT_GRAPH_SCHEMA_VERSION, parseProjectGraph } from '@elytracms/project-graph';
2571
17
 
2572
18
  // src/components.ts
2573
19
  function defineHostComponents(components = [], options = {}) {
@@ -2601,7 +47,7 @@ function pathFromToken(token) {
2601
47
  if (!token.startsWith("/")) return void 0;
2602
48
  return token.slice(1).split("/").map(unescapeRefToken);
2603
49
  }
2604
- var isPlainObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
50
+ var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2605
51
  function resolvePayloadToken(payload, token) {
2606
52
  const path = pathFromToken(token);
2607
53
  if (path === void 0) return void 0;
@@ -2614,7 +60,7 @@ function resolvePayloadToken(payload, token) {
2614
60
  current = current[index];
2615
61
  continue;
2616
62
  }
2617
- if (isPlainObject2(current)) {
63
+ if (isPlainObject(current)) {
2618
64
  if (!Object.prototype.hasOwnProperty.call(current, segment)) return void 0;
2619
65
  current = current[segment];
2620
66
  continue;
@@ -2715,996 +161,6 @@ function compositionNodesOf(document) {
2715
161
  function isComponentNodeValue(value) {
2716
162
  return typeof value === "object" && value !== null && typeof value.id === "string" && typeof value.componentId === "string";
2717
163
  }
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
164
  function mergeCacheTags(first, ...rest) {
3709
165
  const all = [first, ...rest];
3710
166
  return cacheTags({
@@ -3715,8 +171,6 @@ function mergeCacheTags(first, ...rest) {
3715
171
  locale: first.locale
3716
172
  });
3717
173
  }
3718
-
3719
- // src/source-payloads.ts
3720
174
  function collectBindingSourceIds(node, into = /* @__PURE__ */ new Set()) {
3721
175
  for (const value of Object.values(node.props ?? {})) {
3722
176
  if (value.kind === "binding") into.add(value.binding.sourceId);
@@ -3758,8 +212,6 @@ function materializeSourcePayloads(input) {
3758
212
  }
3759
213
  return { payloads, tags };
3760
214
  }
3761
-
3762
- // src/snapshot.ts
3763
215
  var DEFAULT_LOCALES = { default: "en", locales: ["en"] };
3764
216
  function patternKey(pattern) {
3765
217
  return "/" + splitPath(pattern).join("/");
@@ -3866,8 +318,6 @@ async function createContentSnapshot(adapter, projectId, options = {}) {
3866
318
  assetUrl: (asset) => adapter.blobs.url(asset.storageKey)
3867
319
  });
3868
320
  }
3869
-
3870
- // src/route-core.ts
3871
321
  function pathFromSlug(slug) {
3872
322
  return "/" + (slug ?? []).join("/");
3873
323
  }
@@ -4069,7 +519,7 @@ async function loadCachedEntry(cache, keyParts, baselineTags, load) {
4069
519
  }
4070
520
 
4071
521
  // src/metadata.ts
4072
- function str2(value) {
522
+ function str(value) {
4073
523
  return typeof value === "string" && value.length > 0 ? value : null;
4074
524
  }
4075
525
  function looksLikeUrl(value) {
@@ -4082,13 +532,13 @@ function resolveOgImage(value, assets) {
4082
532
  }
4083
533
  function canvasPageMetadata(result, assets = []) {
4084
534
  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);
535
+ const seoTitle = str(doc?.["seoTitle"]);
536
+ const seoDescription = str(doc?.["seoDescription"]);
537
+ const seoOgTitle = str(doc?.["seoOgTitle"]);
538
+ const seoOgImage = resolveOgImage(str(doc?.["seoOgImage"]), assets);
4089
539
  const seoNoindex = doc?.["seoNoindex"] === true;
4090
- const seoCanonical = str2(doc?.["seoCanonical"]);
4091
- const docTitle = str2(doc?.["name"]) ?? str2(doc?.["title"]);
540
+ const seoCanonical = str(doc?.["seoCanonical"]);
541
+ const docTitle = str(doc?.["name"]) ?? str(doc?.["title"]);
4092
542
  const title = seoTitle ?? docTitle;
4093
543
  const metadata = {};
4094
544
  if (title !== null) metadata.title = title;
@@ -4179,8 +629,6 @@ function createCanvasRoute(options) {
4179
629
  };
4180
630
  return { Page, generateMetadata };
4181
631
  }
4182
-
4183
- // src/sitemap.ts
4184
632
  function paramNamesOf(pattern) {
4185
633
  return splitPath(pattern).filter((segment) => segment.startsWith(":")).map((segment) => segment.slice(1));
4186
634
  }
@@ -4479,6 +927,6 @@ function createPreviewDisableRoute() {
4479
927
  }
4480
928
  var PACKAGE = "@elytracms/next";
4481
929
 
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 };
930
+ export { CanvasRenderer, ElytraImage, PACKAGE, REVALIDATE_SIGNATURE_HEADER, canvasPageMetadata, canvasSitemapEntries, collectBindingSourceIds, collectionCacheTag, createAssetImageLoader, createBindingResolver, createCanvasRoute, createCanvasSitemap, createContentSnapshot, createPreviewDisableRoute, createPreviewRoute, createRevalidateRoute, createStaticContentSource, defaultBindingPayloads, defineHostComponents, docCacheTag, evaluatePreviewRequest, evaluateRevalidateRequest, isSourcePayloadError, loadCachedEntry, materializeSourcePayloads, mergeCacheTags, mergeRouteRecords, nextCacheTags, nextImagePrimitive, pathFromSlug, projectSweepTag, resolveCanvasPage, resolvePayloadToken, revalidatePayloadSchema, routeCacheTag, safeRedirectTarget, scopeCacheTag, signRevalidateBody, sourcePayloadError, splitLocalePath };
4483
931
  //# sourceMappingURL=index.js.map
4484
932
  //# sourceMappingURL=index.js.map