@elytracms/next 0.0.85 → 0.0.87

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,15 +1,16 @@
1
1
  import { ComponentRegistry } from '@elytracms/core/component-registry';
2
2
  export { defineComponent } from '@elytracms/core/component-registry';
3
- import { basePrimitives, getImplementation, renderDocumentBodyItems, documentBodyItems, ImageManifest, serializeListingQuery, RenderErrorBoundary, documentBodyRichTextValues, documentBodyCompositionNodes, richTextEmbedsInValue, collectReferencedAssetIds, collectRichTextRefsInValue, collectReferencedRelations, collectReferencedListings, collectReferencedRichTextLinks, normalizeImageSource } from '@elytracms/core/runtime-renderer';
3
+ import { basePrimitives, getImplementation, registerImplementations, renderDocumentBodyItems, documentBodyItems, ImageManifest, serializeListingQuery, RenderErrorBoundary, documentBodyRichTextValues, documentBodyCompositionNodes, richTextEmbedsInValue, collectReferencedAssetIds, collectRichTextRefsInValue, collectReferencedRelations, collectReferencedListings, collectReferencedRichTextLinks, normalizeImageSource } from '@elytracms/core/runtime-renderer';
4
4
  import { isValidElement } from 'react';
5
5
  import { isBlockSequenceValueShape } from '@elytracms/core/rich-text';
6
6
  import { jsx, Fragment } from 'react/jsx-runtime';
7
- import { cacheTags, isSectionSourceId, parseCmsSourceQueryConfig, listQueryOfCmsConfig, isDocumentHistory, resolvedAsset, createContentClient, createContentContext } from '@elytracms/core/content';
7
+ import { cacheTags, isDocumentHistory, resolvedAsset, createContentClient, createContentContext } from '@elytracms/core/content';
8
8
  import { documentKey, splitLocalePath, hierarchyMount, composePath, localeUrlPath, mergeRouteRecords, fallbackHomeRoutes, presentationBodyFields, splitPath, documentSchema, redirectRecordSchema } from '@elytracms/core/cms-core';
9
- export { documentSchema, fallbackHomeRoutes, localeConfigSchema, localeUrlPath, mergeRouteRecords, redirectRecordSchema, routeRecordSchema, splitLocalePath } from '@elytracms/core/cms-core';
9
+ export { documentSchema, fallbackHomeRoutes, localeConfigSchema, localeUrlPath, mergeRouteRecords, redirectRecordSchema, resolveLocale, routeRecordSchema, splitLocalePath, textDirection } from '@elytracms/core/cms-core';
10
10
  import { revalidateTag, unstable_cache } from 'next/cache';
11
11
  import { draftMode } from 'next/headers';
12
12
  import { redirect, notFound, permanentRedirect } from 'next/navigation';
13
+ import { COMPOSITE_KEY_SEPARATOR } from '@elytracms/core/project-graph';
13
14
  import { createHmac, timingSafeEqual } from 'crypto';
14
15
  import { z } from 'zod';
15
16
  export { z } from 'zod';
@@ -25,64 +26,9 @@ function defineHostComponents(components = [], options = {}) {
25
26
  const implementation = getImplementation(base.implementations, manifest.id);
26
27
  if (implementation) implementations[manifest.id] = implementation;
27
28
  }
28
- for (const component of components) {
29
- implementations[component.manifest.id] = component.implementation;
30
- }
31
- return { registry, implementations, issues: registry.issues };
32
- }
33
-
34
- // src/payloads.ts
35
- var SOURCE_PAYLOAD_ERROR_KEY = "__elytraSourcePayloadError";
36
- function sourcePayloadError(sourceId, messages) {
37
- return { [SOURCE_PAYLOAD_ERROR_KEY]: true, sourceId, messages: [...messages] };
38
- }
39
- function isSourcePayloadError(value) {
40
- return typeof value === "object" && value !== null && value[SOURCE_PAYLOAD_ERROR_KEY] === true && typeof value.sourceId === "string" && Array.isArray(value.messages);
41
- }
42
- function unescapeRefToken(token) {
43
- return token.replace(/~1/g, "/").replace(/~0/g, "~");
44
- }
45
- function pathFromToken(token) {
46
- if (token === "" || token === "/") return [];
47
- if (!token.startsWith("/")) return void 0;
48
- return token.slice(1).split("/").map(unescapeRefToken);
49
- }
50
- var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
51
- function resolvePayloadToken(payload, token) {
52
- const path = pathFromToken(token);
53
- if (path === void 0) return void 0;
54
- let current = payload;
55
- for (const segment of path) {
56
- if (Array.isArray(current)) {
57
- if (!/^(0|[1-9][0-9]*)$/.test(segment)) return void 0;
58
- const index = Number(segment);
59
- if (index >= current.length) return void 0;
60
- current = current[index];
61
- continue;
62
- }
63
- if (isPlainObject(current)) {
64
- if (!Object.prototype.hasOwnProperty.call(current, segment)) return void 0;
65
- current = current[segment];
66
- continue;
67
- }
68
- return void 0;
69
- }
70
- return current;
71
- }
72
- function createBindingResolver(payloads) {
73
- return (ref, item) => {
74
- if (ref.mode === "repeaterItem") {
75
- return item === void 0 ? void 0 : resolvePayloadToken(item.item, ref.token);
76
- }
77
- if (!Object.prototype.hasOwnProperty.call(payloads, ref.sourceId)) return void 0;
78
- const payload = payloads[ref.sourceId];
79
- if (isSourcePayloadError(payload)) {
80
- throw new Error(
81
- `Source "${payload.sourceId}" query was rejected: ${payload.messages.join("; ")}`
82
- );
83
- }
84
- return resolvePayloadToken(payload, ref.token);
85
- };
29
+ const host = registerImplementations(components);
30
+ Object.assign(implementations, host.implementations);
31
+ return { registry, implementations, issues: [...registry.issues, ...host.issues] };
86
32
  }
87
33
  function relationResolver(relations) {
88
34
  return (ref) => relations[`${ref.collection}:${ref.id}`] ?? null;
@@ -121,19 +67,14 @@ function stripServerErrorBoundary(_node, rendered) {
121
67
  }
122
68
  return rendered;
123
69
  }
124
- function defaultBindingPayloads(result) {
125
- return { document: result.document, params: result.dynamicParams };
126
- }
127
70
  async function CanvasRenderer(props) {
128
71
  const { result, components } = props;
129
72
  if (result.status !== "ok") {
130
73
  return /* @__PURE__ */ jsx(UnresolvedPageFallback, { status: result.status });
131
74
  }
132
- const resolveBinding = props.resolveBinding ?? createBindingResolver(props.payloads ?? defaultBindingPayloads(result));
133
75
  const ctx = {
134
76
  registry: components.registry,
135
77
  implementations: components.implementations,
136
- resolveBinding,
137
78
  resolveAsset: assetResolver(props.assets ?? []),
138
79
  // EC-254: wire relation population only when the host baked the populated
139
80
  // targets, so a host that did not opt in keeps the raw-ref passthrough.
@@ -192,42 +133,6 @@ function mergeCacheTags(first, ...rest) {
192
133
  locale: first.locale
193
134
  });
194
135
  }
195
- function collectBindingSourceIds(node, into = /* @__PURE__ */ new Set()) {
196
- for (const value of Object.values(node.props ?? {})) {
197
- if (value.kind === "binding") into.add(value.binding.sourceId);
198
- }
199
- if (node.condition) into.add(node.condition.source.sourceId);
200
- for (const children of Object.values(node.slots ?? {})) {
201
- for (const child of children) collectBindingSourceIds(child, into);
202
- }
203
- return into;
204
- }
205
- function materializeSourcePayloads(input) {
206
- const { client, result, sources } = input;
207
- const payloads = {};
208
- const tags = [];
209
- if (result.status !== "ok") return { payloads, tags };
210
- const referenced = /* @__PURE__ */ new Set();
211
- const byId = new Map(sources.map((source) => [source.id, source]));
212
- for (const sourceId of [...referenced].sort()) {
213
- if (!isSectionSourceId(sourceId)) continue;
214
- const definition = byId.get(sourceId);
215
- if (!definition || definition.kind !== "cms") continue;
216
- const config = parseCmsSourceQueryConfig(definition.config);
217
- if (!config || config.documentId !== void 0) continue;
218
- const list = client.listDocuments(config.collectionId, listQueryOfCmsConfig(config));
219
- tags.push(list.tags);
220
- if (list.ok) {
221
- payloads[sourceId] = list.documents;
222
- } else {
223
- payloads[sourceId] = sourcePayloadError(
224
- sourceId,
225
- list.errors.map((error) => error.message)
226
- );
227
- }
228
- }
229
- return { payloads, tags };
230
- }
231
136
  function createStaticContentSource(data) {
232
137
  const locales = data.locales;
233
138
  const routes = data.routes ?? [];
@@ -273,7 +178,6 @@ function createStaticContentSource(data) {
273
178
  // dimensions. Baked into the (serializable) render outcome by the route
274
179
  // helper, so a cached page keeps its images during a backend outage.
275
180
  assets: () => [...assets.values()].map((record2) => resolvedAsset(record2, record2.url ?? assetUrl(record2))),
276
- ...data.sources ? { sources: data.sources } : {},
277
181
  // EC-499: the schema the route helper reads a collection's declared
278
182
  // `presentation` from (which field is the delivery body, which component
279
183
  // wraps it). Already prefetched for the lookup above — exposing it costs
@@ -313,7 +217,6 @@ async function createContentSnapshot(adapter, projectId, options) {
313
217
  locales: options.locales,
314
218
  ...options.routes ? { routes: options.routes } : {},
315
219
  redirects: options.redirects ? [...options.redirects] : redirects,
316
- ...options.sources ? { sources: options.sources } : {},
317
220
  ...schema ? { collections: schema.collections } : {},
318
221
  documents,
319
222
  assets: assetRecords,
@@ -436,26 +339,6 @@ async function resolveCanvasPage(source, url, perspective, options = {}) {
436
339
  if (result.status !== "ok") {
437
340
  return { kind: "not-found", tags: result.tags };
438
341
  }
439
- const extraTags = [];
440
- let payloads = defaultBindingPayloads(result);
441
- const materialized = materializeSourcePayloads({
442
- client,
443
- result,
444
- sources: source.sources ?? []
445
- });
446
- payloads = { ...payloads, ...materialized.payloads };
447
- extraTags.push(...materialized.tags);
448
- if (options.payloads) {
449
- const custom = await options.payloads({
450
- client,
451
- result,
452
- perspective,
453
- path,
454
- params: result.dynamicParams,
455
- addTags: (tags) => extraTags.push(tags)
456
- });
457
- payloads = { ...payloads, ...custom };
458
- }
459
342
  const presentation = presentationFor(source, result);
460
343
  const body = pageBodyOf(result, presentation);
461
344
  const allAssets = source.assets?.() ?? [];
@@ -486,7 +369,6 @@ async function resolveCanvasPage(source, url, perspective, options = {}) {
486
369
  return {
487
370
  kind: "render",
488
371
  result,
489
- payloads,
490
372
  assets: bakedAssets,
491
373
  relations,
492
374
  listings,
@@ -494,7 +376,6 @@ async function resolveCanvasPage(source, url, perspective, options = {}) {
494
376
  ...presentation ? { presentation } : {},
495
377
  tags: mergeCacheTags(
496
378
  result.tags,
497
- ...extraTags,
498
379
  ...listingTags,
499
380
  assetTags,
500
381
  relationTags,
@@ -533,6 +414,9 @@ function nextCacheTags(tags, scope) {
533
414
  for (const assetId of tags.assets) all.add(assetCacheTag(assetId, scope));
534
415
  return [...all].sort();
535
416
  }
417
+ function canvasCacheKey({ scope, mode, url }) {
418
+ return ["elytra-route", scope ?? "", mode ?? "", url];
419
+ }
536
420
  var CacheDiscoveryBail = class extends Error {
537
421
  constructor() {
538
422
  super("elytra cache discovery bail (never user-visible)");
@@ -582,6 +466,9 @@ function resolveOgImage(value, assets) {
582
466
  if (looksLikeUrl(value)) return value;
583
467
  return assets.find((asset) => asset.id === value)?.url ?? null;
584
468
  }
469
+ function openGraphLocale(locale) {
470
+ return locale.replace(/-/g, "_");
471
+ }
585
472
  function canvasPageMetadata(result, assets = []) {
586
473
  const doc = result.document;
587
474
  const seoTitle = str(doc?.["seoTitle"]);
@@ -596,11 +483,12 @@ function canvasPageMetadata(result, assets = []) {
596
483
  if (title !== null) metadata.title = title;
597
484
  if (seoDescription) metadata.description = seoDescription;
598
485
  const ogTitle = seoOgTitle ?? title;
599
- if (ogTitle !== null || seoDescription || seoOgImage) {
486
+ if (doc) {
600
487
  metadata.openGraph = {
601
488
  ...ogTitle !== null ? { title: ogTitle } : {},
602
489
  ...seoDescription ? { description: seoDescription } : {},
603
- ...seoOgImage ? { images: [{ url: seoOgImage }] } : {}
490
+ ...seoOgImage ? { images: [{ url: seoOgImage }] } : {},
491
+ locale: openGraphLocale(result.tags.locale)
604
492
  };
605
493
  }
606
494
  if (seoNoindex) metadata.robots = { index: false };
@@ -620,7 +508,6 @@ function createCanvasRoute(options) {
620
508
  const resolveFresh = async () => {
621
509
  const source = await options.loadContent({ perspective });
622
510
  return resolveCanvasPage(source, url, perspective, {
623
- ...options.payloads ? { payloads: options.payloads } : {},
624
511
  // EC-205: page-scope the baked assets to what the page actually uses.
625
512
  registry: options.components.registry
626
513
  });
@@ -630,7 +517,7 @@ function createCanvasRoute(options) {
630
517
  const scope = options.cache.scope;
631
518
  const entry = await loadCachedEntry(
632
519
  nextCacheWrapper,
633
- ["elytra-route", scope ?? "", url],
520
+ canvasCacheKey({ scope, mode: options.cache.mode, url }),
634
521
  [projectSweepTag(scope)],
635
522
  async () => {
636
523
  const outcome = await resolveFresh();
@@ -656,7 +543,6 @@ function createCanvasRoute(options) {
656
543
  {
657
544
  result: outcome.result,
658
545
  components: options.components,
659
- payloads: outcome.payloads,
660
546
  assets: outcome.assets,
661
547
  relations: outcome.relations,
662
548
  listings: outcome.listings,
@@ -785,7 +671,7 @@ function canvasSitemapEntries(source, options) {
785
671
  continue;
786
672
  }
787
673
  const absolute = originOf(options.baseUrl) + urlPath;
788
- const group = result.document ? `${route.id}\0doc\0${result.document.collection}\0${result.document.id}` : `${route.id}\0path\0${path}`;
674
+ const group = result.document ? `${route.id}${COMPOSITE_KEY_SEPARATOR}doc${COMPOSITE_KEY_SEPARATOR}${result.document.collection}${COMPOSITE_KEY_SEPARATOR}${result.document.id}` : `${route.id}${COMPOSITE_KEY_SEPARATOR}path${COMPOSITE_KEY_SEPARATOR}${path}`;
789
675
  if (!byUrl.has(absolute)) byUrl.set(absolute, { group, locale });
790
676
  const perLocale = byGroup.get(group) ?? /* @__PURE__ */ new Map();
791
677
  if (!perLocale.has(locale)) perLocale.set(locale, absolute);
@@ -1035,21 +921,6 @@ function parseRedirects(rows) {
1035
921
  }
1036
922
  return redirects;
1037
923
  }
1038
- var canvasDataSourceSchema = z.object({
1039
- kind: z.string().min(1),
1040
- id: z.string().min(1),
1041
- label: z.string().optional(),
1042
- config: z.unknown().optional()
1043
- });
1044
- function parseSources(rows) {
1045
- if (!Array.isArray(rows)) return [];
1046
- const sources = [];
1047
- for (const row of rows) {
1048
- const parsed = canvasDataSourceSchema.safeParse(row);
1049
- if (parsed.success) sources.push(parsed.data);
1050
- }
1051
- return sources;
1052
- }
1053
924
  function assembleLiveSource(payload, options, perspective) {
1054
925
  const locales = options.locales;
1055
926
  const documents = parseDocuments(payload.documents);
@@ -1065,10 +936,7 @@ function assembleLiveSource(payload, options, perspective) {
1065
936
  redirects: parseRedirects(payload.redirects ?? []),
1066
937
  collections: [...options.collections],
1067
938
  documents: documentSources,
1068
- assets: payload.assets,
1069
- // EC-166 LIVE mode: the builder's persisted section/template sources, so
1070
- // the route helper materializes their payloads.
1071
- sources: parseSources(payload.sources)
939
+ assets: payload.assets
1072
940
  });
1073
941
  }
1074
942
  async function loadLiveContent(options, perspective) {
@@ -1207,12 +1075,30 @@ function resolveGlobals(source, options = {}) {
1207
1075
  }
1208
1076
 
1209
1077
  // src/preview-core.ts
1078
+ var RESOLUTION_ORIGIN = "https://redirect-target.invalid";
1079
+ function resolveOnOrigin(value) {
1080
+ let resolved;
1081
+ try {
1082
+ resolved = new URL(value, RESOLUTION_ORIGIN);
1083
+ } catch {
1084
+ return null;
1085
+ }
1086
+ return resolved.origin === RESOLUTION_ORIGIN ? resolved : null;
1087
+ }
1088
+ function hasControlCharacter(value) {
1089
+ for (let i = 0; i < value.length; i++) {
1090
+ const code = value.charCodeAt(i);
1091
+ if (code <= 31 || code === 127) return true;
1092
+ }
1093
+ return false;
1094
+ }
1210
1095
  function safeRedirectTarget(value, fallback = "/") {
1211
1096
  if (value === null || value === void 0 || value === "") return fallback;
1212
- if (!value.startsWith("/")) return null;
1213
- if (value.includes("\\")) return null;
1214
- if (value.startsWith("//")) return null;
1215
- return value;
1097
+ if (!value.startsWith("/") || hasControlCharacter(value)) return null;
1098
+ const resolved = resolveOnOrigin(value);
1099
+ if (resolved === null) return null;
1100
+ const target = resolved.pathname + resolved.search + resolved.hash;
1101
+ return resolveOnOrigin(target) === null ? null : target;
1216
1102
  }
1217
1103
  function evaluatePreviewRequest(requestUrl, expectedToken) {
1218
1104
  if (expectedToken === void 0 || expectedToken.length === 0) {
@@ -1234,6 +1120,15 @@ function evaluatePreviewRequest(requestUrl, expectedToken) {
1234
1120
  }
1235
1121
  return { ok: true, redirectTo };
1236
1122
  }
1123
+ function previewDisableTarget(requestUrl) {
1124
+ let url;
1125
+ try {
1126
+ url = typeof requestUrl === "string" ? new URL(requestUrl, "http://localhost") : requestUrl;
1127
+ } catch {
1128
+ return "/";
1129
+ }
1130
+ return safeRedirectTarget(url.searchParams.get("redirect")) ?? "/";
1131
+ }
1237
1132
 
1238
1133
  // src/preview.ts
1239
1134
  function createPreviewRoute(options) {
@@ -1254,13 +1149,7 @@ function createPreviewDisableRoute() {
1254
1149
  async GET(request) {
1255
1150
  const draft = await draftMode();
1256
1151
  draft.disable();
1257
- let target;
1258
- try {
1259
- target = safeRedirectTarget(new URL(request.url).searchParams.get("redirect"));
1260
- } catch {
1261
- target = "/";
1262
- }
1263
- redirect(target ?? "/");
1152
+ redirect(previewDisableTarget(request.url));
1264
1153
  }
1265
1154
  };
1266
1155
  }
@@ -1287,9 +1176,9 @@ function createElytraHost(options) {
1287
1176
  // canonical manifest stays (first registration wins in defineHostComponents).
1288
1177
  ...options.image === false ? [] : [nextImagePrimitive(options.image ?? {})],
1289
1178
  // EC-539: the option is the CONFIG's entry shape (`implementation: unknown`)
1290
- // so the recommended `{ ...site }` spread compiles. Narrowing here is the
1291
- // one place that widening is paid for; see the option's doc comment and
1292
- // EC-541 for the runtime check that should back it.
1179
+ // so the recommended `{ ...site }` spread compiles; EC-541: registration
1180
+ // takes that shape too and checks each implementation at runtime (see the
1181
+ // option's doc comment) — no narrowing cast here.
1293
1182
  ...options.components ?? []
1294
1183
  ]);
1295
1184
  const emptySource = createStaticContentSource({
@@ -1300,8 +1189,7 @@ function createElytraHost(options) {
1300
1189
  redirects: [],
1301
1190
  collections: [...options.collections],
1302
1191
  documents: [],
1303
- assets: [],
1304
- sources: []
1192
+ assets: []
1305
1193
  });
1306
1194
  const loadContent = (request) => {
1307
1195
  if (live) {
@@ -1336,7 +1224,9 @@ function createElytraHost(options) {
1336
1224
  components,
1337
1225
  // Tag scope = the project id the studio publishes under, so its webhook
1338
1226
  // tags match this site's cached entries.
1339
- ...cache === false ? {} : { cache: cache ?? { scope: contentScope } },
1227
+ // EC-543: the mode is in the entry key so no world ever reads another's
1228
+ // entries; an explicit override may replace the scope, not drop the mode.
1229
+ ...cache === false ? {} : { cache: { mode: contentMode, ...cache ?? { scope: contentScope } } },
1340
1230
  ...rest
1341
1231
  });
1342
1232
  };
@@ -1375,6 +1265,6 @@ function createElytraHost(options) {
1375
1265
  }
1376
1266
  var PACKAGE = "@elytracms/next";
1377
1267
 
1378
- export { CanvasRenderer, ELYTRA_HOST_ENV_KEYS, ElytraImage, PACKAGE, REVALIDATE_SIGNATURE_HEADER, assembleLiveSource, bodyItemsOf, canvasPageMetadata, canvasSitemapEntries, collectBindingSourceIds, collectionCacheTag, createAssetImageLoader, createBindingResolver, createCanvasRoute, createCanvasSitemap, createContentSnapshot, createElytraHost, createPreviewDisableRoute, createPreviewRoute, createRevalidateRoute, createStaticContentSource, defaultBindingPayloads, defineHostComponents, docCacheTag, elytraHostImage, evaluatePreviewRequest, evaluateRevalidateRequest, firstBlockFullBleed, isSourcePayloadError, loadCachedEntry, loadLiveContent, materializeSourcePayloads, mergeCacheTags, nextCacheTags, nextImagePrimitive, pathFromSlug, projectSweepTag, resolveCanvasPage, resolveGlobals, resolveHostEnv, resolvePayloadToken, revalidatePayloadSchema, routeCacheTag, safeRedirectTarget, scopeCacheTag, signRevalidateBody, sourcePayloadError };
1268
+ export { CanvasRenderer, ELYTRA_HOST_ENV_KEYS, ElytraImage, PACKAGE, REVALIDATE_SIGNATURE_HEADER, assembleLiveSource, bodyItemsOf, canvasPageMetadata, canvasSitemapEntries, collectionCacheTag, createAssetImageLoader, createCanvasRoute, createCanvasSitemap, createContentSnapshot, createElytraHost, createPreviewDisableRoute, createPreviewRoute, createRevalidateRoute, createStaticContentSource, defineHostComponents, docCacheTag, elytraHostImage, evaluatePreviewRequest, evaluateRevalidateRequest, firstBlockFullBleed, loadCachedEntry, loadLiveContent, mergeCacheTags, nextCacheTags, nextImagePrimitive, pathFromSlug, projectSweepTag, resolveCanvasPage, resolveGlobals, resolveHostEnv, revalidatePayloadSchema, routeCacheTag, safeRedirectTarget, scopeCacheTag, signRevalidateBody };
1379
1269
  //# sourceMappingURL=index.js.map
1380
1270
  //# sourceMappingURL=index.js.map