@elytracms/next 0.0.90 → 0.0.91

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,34 +1,25 @@
1
- import { ComponentRegistry } from '@elytracms/core/component-registry';
2
- export { defineComponent } from '@elytracms/core/component-registry';
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
- import { isValidElement } from 'react';
1
+ import { registerComponents, renderDocumentBodyItems, documentBodyItems, serializeListingQuery, documentBodyRichTextValues, documentBodyCompositionNodes, richTextEmbedsInValue, collectReferencedAssetIds, collectRichTextRefsInValue, collectReferencedRelations, collectReferencedListings, collectReferencedRichTextLinks, RenderErrorBoundary, nodeListingOf, listingRequestOf } from '@elytracms/core/runtime-renderer';
2
+ import { Fragment as Fragment$1, isValidElement } from 'react';
5
3
  import { isBlockSequenceValueShape } from '@elytracms/core/rich-text';
6
- import { jsx, Fragment } from 'react/jsx-runtime';
4
+ import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
7
5
  import { cacheTags, isDocumentHistory, resolvedAsset, createContentClient, createContentContext } from '@elytracms/core/content';
8
6
  import { parseLocaleConfig, documentKey, splitLocalePath, hierarchyMount, composePath, localeUrlPath, mergeRouteRecords, fallbackHomeRoutes, presentationBodyFields, splitPath, documentSchema, redirectRecordSchema } from '@elytracms/core/cms-core';
9
7
  export { documentSchema, fallbackHomeRoutes, localeConfigSchema, localeUrlPath, mergeRouteRecords, parseLocaleConfig, redirectRecordSchema, resolveLocale, routeRecordSchema, splitLocalePath, textDirection } from '@elytracms/core/cms-core';
10
- import { revalidateTag, unstable_cache } from 'next/cache';
8
+ import { revalidateTag, cacheLife, cacheTag } from 'next/cache';
11
9
  import { draftMode } from 'next/headers';
12
10
  import { redirect, notFound, permanentRedirect } from 'next/navigation';
13
11
  import { COMPOSITE_KEY_SEPARATOR } from '@elytracms/core/project-graph';
14
12
  import { verifyRevalidationSignature, readRevalidationBody, REVALIDATE_SIGNATURE_HEADER } from '@elytracms/core/revalidation';
15
13
  export { REVALIDATE_SIGNATURE_HEADER, revalidationEventSchema, signRevalidationBody, signedRevalidationRequest } from '@elytracms/core/revalidation';
16
14
  import NextImage from 'next/image';
15
+ import { normalizeImageSource } from '@elytracms/components';
16
+ export { defineComponent } from '@elytracms/core/component-registry';
17
17
  export { z } from 'zod';
18
18
 
19
19
  // src/components.ts
20
- function defineHostComponents(components = [], options = {}) {
21
- const includeBase = options.includeBasePrimitives !== false;
22
- const base = includeBase ? basePrimitives() : { manifests: [], implementations: {} };
23
- const registry = new ComponentRegistry([...base.manifests, ...components.map((c) => c.manifest)]);
24
- const implementations = {};
25
- for (const manifest of base.manifests) {
26
- const implementation = getImplementation(base.implementations, manifest.id);
27
- if (implementation) implementations[manifest.id] = implementation;
28
- }
29
- const host = registerImplementations(components);
30
- Object.assign(implementations, host.implementations);
31
- return { registry, implementations, issues: [...registry.issues, ...host.issues] };
20
+ function defineHostComponents(components = []) {
21
+ const { registry, implementations } = registerComponents(components);
22
+ return { registry, implementations };
32
23
  }
33
24
  function relationResolver(relations) {
34
25
  return (ref) => relations[`${ref.collection}:${ref.id}`] ?? null;
@@ -37,7 +28,22 @@ function linkResolver(links) {
37
28
  return (ref) => links[`${ref.collection}:${ref.id}`] ?? null;
38
29
  }
39
30
  function listingResolver(listings) {
40
- return (query) => listings[serializeListingQuery(query)] ?? null;
31
+ return (query) => {
32
+ const baked = listings[serializeListingQuery(query)];
33
+ return Array.isArray(baked) ? baked : null;
34
+ };
35
+ }
36
+ function listingFailureOf(node, components, listings) {
37
+ const manifest = components.registry.getManifest(node.componentId);
38
+ if (!manifest) return null;
39
+ const listing = nodeListingOf(node, manifest, components.registry);
40
+ if (!listing || listing.state === "awaiting") return null;
41
+ if (listing.state === "invalid") return listing.messages;
42
+ const baked = listings?.[serializeListingQuery(listing.query)];
43
+ return baked && !Array.isArray(baked) ? baked.errors.map((error) => error.message) : null;
44
+ }
45
+ function InvalidListingNotice(props) {
46
+ return /* @__PURE__ */ jsx("div", { "data-ec-fallback": "invalid-listing", "data-ec-node-id": props.nodeId, role: "note", children: `This listing can't run: ${props.messages.join(" ")}` });
41
47
  }
42
48
  function assetResolver(assets) {
43
49
  const byId = new Map(assets.map((asset) => [asset.id, asset]));
@@ -49,7 +55,7 @@ function assetResolver(assets) {
49
55
  height: asset.height,
50
56
  alt: asset.alt,
51
57
  focalPoint: asset.focalPoint,
52
- // EC-420: carry the LQIP so a placed base.Image blurs up on delivery.
58
+ // EC-420: carry the LQIP so a placed Image blurs up on delivery.
53
59
  lqip: asset.lqip,
54
60
  meta: asset.meta
55
61
  } : null;
@@ -86,7 +92,15 @@ async function CanvasRenderer(props) {
86
92
  // so a host that did not opt in degrades a reference link to the visible
87
93
  // `unresolved-ref` issue (EC-290) rather than a wrong href.
88
94
  ...props.links ? { resolveLink: linkResolver(props.links) } : {},
89
- decorateNode: stripServerErrorBoundary
95
+ decorateNode: (node, rendered) => {
96
+ const stripped = stripServerErrorBoundary(node, rendered);
97
+ const failure = listingFailureOf(node, components, props.listings);
98
+ if (!failure) return stripped;
99
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
100
+ /* @__PURE__ */ jsx(InvalidListingNotice, { nodeId: node.id, messages: failure }),
101
+ stripped
102
+ ] }, node.id);
103
+ }
90
104
  };
91
105
  const document = result.document;
92
106
  if (document) {
@@ -235,6 +249,7 @@ function canvasWrapContext(result, info) {
235
249
  function pathFromSlug(slug) {
236
250
  return "/" + (slug ?? []).join("/");
237
251
  }
252
+ var systemClock = () => (/* @__PURE__ */ new Date()).toISOString();
238
253
  function scopeAssetsToPage(allAssets, registry, result, body, resolveRelation) {
239
254
  if (allAssets.length === 0) return allAssets;
240
255
  const ids = /* @__PURE__ */ new Set();
@@ -281,27 +296,33 @@ function populatePageRelations(body, client, registry) {
281
296
  );
282
297
  return { relations, docKeys: [...refKeys] };
283
298
  }
284
- function populatePageListings(body, client, registry) {
285
- if (!registry) return { listings: {}, tags: [] };
299
+ function populatePageListings(body, client, registry, clock) {
300
+ if (!registry) return { listings: {}, tags: [], readsNow: false };
286
301
  const tags = [];
302
+ let instant;
303
+ const now = () => instant ??= clock();
287
304
  const listings = collectReferencedListings(
288
305
  body.nodes,
289
306
  registry,
290
307
  (query) => {
291
- const listed = client.listDocuments(query.collection, {
292
- where: query.where,
293
- ...query.sort ? { sort: query.sort } : {},
294
- ...query.limit !== void 0 ? { limit: query.limit } : {}
295
- });
308
+ const listed = client.listDocuments(query.collection, listDocumentsQueryOf(query, now));
296
309
  tags.push(listed.tags);
297
- return listed.ok ? listed.documents : [];
310
+ return listed.ok ? listed.documents : { errors: listed.errors };
298
311
  },
299
312
  // EC-255: reach listings nested inside transcluded reusable content (the same
300
313
  // depth-1 populate the relation bake uses), so a grid inside a reusable bakes —
301
314
  // and its membership tag is folded in — instead of rendering empty on the host.
302
315
  (ref) => client.resolveRelation(ref)
303
316
  );
304
- return { listings, tags };
317
+ return { listings, tags, readsNow: instant !== void 0 };
318
+ }
319
+ function listDocumentsQueryOf(query, now) {
320
+ const request = listingRequestOf(query, now);
321
+ return {
322
+ where: request.where,
323
+ ...request.sort ? { sort: request.sort } : {},
324
+ ...request.limit !== void 0 ? { limit: request.limit } : {}
325
+ };
305
326
  }
306
327
  function populatePageLinks(body, client, registry) {
307
328
  if (!registry) return { links: {}, docKeys: [] };
@@ -359,11 +380,11 @@ async function resolveCanvasPage(source, url, perspective, options = {}) {
359
380
  options.registry
360
381
  );
361
382
  const relationTags = cacheTags({ docs: relationDocKeys, locale: result.tags.locale });
362
- const { listings, tags: listingTags } = populatePageListings(
363
- body,
364
- client,
365
- options.registry
366
- );
383
+ const {
384
+ listings,
385
+ tags: listingTags,
386
+ readsNow
387
+ } = populatePageListings(body, client, options.registry, options.clock ?? systemClock);
367
388
  const { links, docKeys: linkDocKeys } = populatePageLinks(body, client, options.registry);
368
389
  const linkTags = cacheTags({ docs: linkDocKeys, locale: result.tags.locale });
369
390
  return {
@@ -372,6 +393,7 @@ async function resolveCanvasPage(source, url, perspective, options = {}) {
372
393
  assets: bakedAssets,
373
394
  relations,
374
395
  listings,
396
+ readsNow,
375
397
  links,
376
398
  ...presentation ? { presentation } : {},
377
399
  tags: mergeCacheTags(
@@ -423,7 +445,6 @@ function revalidationTags(event, project) {
423
445
  case "delete":
424
446
  tags.add(docCacheTag(`${event.collection}:${event.id}`, scope));
425
447
  for (const locale of project.locales) tags.add(collectionCacheTag(event.collection, locale, scope));
426
- tags.add(projectSweepTag(scope));
427
448
  break;
428
449
  case "redirects":
429
450
  tags.add(projectSweepTag(scope));
@@ -436,39 +457,40 @@ function revalidationTags(event, project) {
436
457
  return [...tags].sort();
437
458
  }
438
459
  function canvasCacheKey({ scope, mode, url }) {
439
- return ["elytra-route", scope ?? "", mode ?? "", url];
440
- }
441
- var CacheDiscoveryBail = class extends Error {
442
- constructor() {
443
- super("elytra cache discovery bail (never user-visible)");
444
- this.name = "CacheDiscoveryBail";
460
+ return [scope ?? "", mode ?? "", url];
461
+ }
462
+ var CANVAS_CACHE_LIFE = { revalidate: Infinity, expire: Infinity };
463
+ var CANVAS_DEV_CACHE_LIFE = { revalidate: 1, expire: 300 };
464
+ var CANVAS_CLOCK_CACHE_LIFE = { revalidate: 60, expire: Infinity };
465
+ function canvasEntryLife(nodeEnv, readsClock = false) {
466
+ if (nodeEnv === "development") return CANVAS_DEV_CACHE_LIFE;
467
+ return readsClock ? CANVAS_CLOCK_CACHE_LIFE : CANVAS_CACHE_LIFE;
468
+ }
469
+ var NEXT_CACHE_TAG_CALL_LIMIT = 128;
470
+ function entryTagCalls(tags, scope) {
471
+ const sweep = projectSweepTag(scope);
472
+ const rest = nextCacheTags(tags, scope).filter((tag) => tag !== sweep);
473
+ const calls = [[sweep]];
474
+ for (let start = 0; start < rest.length; start += NEXT_CACHE_TAG_CALL_LIMIT) {
475
+ calls.push(rest.slice(start, start + NEXT_CACHE_TAG_CALL_LIMIT));
445
476
  }
446
- };
447
- function passthrough(impl) {
448
- return async () => impl();
449
- }
450
- async function loadCachedEntry(cache, keyParts, baselineTags, load) {
451
- let fresh;
452
- const probe = cache(
453
- passthrough(async () => {
454
- fresh = await load();
455
- throw new CacheDiscoveryBail();
456
- }),
457
- keyParts,
458
- { tags: [...baselineTags], revalidate: false }
459
- );
460
- try {
461
- return await probe();
462
- } catch (error) {
463
- if (!(error instanceof CacheDiscoveryBail)) throw error;
477
+ return calls;
478
+ }
479
+ var canvasResolvers = /* @__PURE__ */ new Map();
480
+ function resolverKey(scope, mode) {
481
+ return JSON.stringify([scope, mode]);
482
+ }
483
+ function registerCanvasResolver(scope, mode, resolve) {
484
+ canvasResolvers.set(resolverKey(scope, mode), resolve);
485
+ }
486
+ function canvasResolverFor(scope, mode) {
487
+ const resolve = canvasResolvers.get(resolverKey(scope, mode));
488
+ if (!resolve) {
489
+ throw new Error(
490
+ `Elytra delivery cache: no canvas route registered for scope "${scope}" and content mode "${mode}" in this server process \u2014 the cached route function has no loader to run. This is a wiring defect: create the route with createCanvasRoute / elytra.canvasRoute() before its Page renders.`
491
+ );
464
492
  }
465
- const entry = fresh;
466
- const write = cache(
467
- passthrough(async () => entry),
468
- keyParts,
469
- { tags: entry.tags, revalidate: false }
470
- );
471
- return write();
493
+ return resolve;
472
494
  }
473
495
 
474
496
  // src/metadata.ts
@@ -531,6 +553,8 @@ function mergeMetadataOverParent(parent, page) {
531
553
  function isPlainObject(value) {
532
554
  return typeof value === "object" && value !== null && !Array.isArray(value);
533
555
  }
556
+ var CANVAS_VALIDATION_SLUG = ["_elytra"];
557
+ var CANVAS_VALIDATION_PATH = pathFromSlug([...CANVAS_VALIDATION_SLUG]);
534
558
  function defaultMetadata(result, assets) {
535
559
  return canvasPageMetadata(result, assets);
536
560
  }
@@ -538,33 +562,35 @@ async function requestPerspective() {
538
562
  const { isEnabled } = await draftMode();
539
563
  return isEnabled ? "draft" : "published";
540
564
  }
541
- var nextCacheWrapper = (cb, keyParts, options) => unstable_cache(cb, keyParts, { tags: options.tags, revalidate: options.revalidate });
565
+ async function cachedCanvasOutcome(scope, mode, url) {
566
+ "use cache";
567
+ const outcome = await canvasResolverFor(scope, mode)(url);
568
+ cacheLife(canvasEntryLife(process.env.NODE_ENV, outcome.kind === "render" && outcome.readsNow));
569
+ for (const tags of entryTagCalls(outcome.tags, scope)) cacheTag(...tags);
570
+ return outcome;
571
+ }
542
572
  function createCanvasRoute(options) {
573
+ const resolveFresh = async (url, perspective) => {
574
+ const source = await options.loadContent({ perspective });
575
+ return resolveCanvasPage(source, url, perspective, {
576
+ // EC-205: page-scope the baked assets to what the page actually uses.
577
+ registry: options.components.registry
578
+ });
579
+ };
580
+ const cache = options.cache;
581
+ const entryKey = (url) => canvasCacheKey({ scope: cache?.scope, mode: cache?.mode, url });
582
+ if (cache) {
583
+ const [scope, mode] = entryKey("");
584
+ registerCanvasResolver(scope, mode, (url) => resolveFresh(url, "published"));
585
+ }
543
586
  const loadOutcome = async (url, perspective) => {
544
- const resolveFresh = async () => {
545
- const source = await options.loadContent({ perspective });
546
- return resolveCanvasPage(source, url, perspective, {
547
- // EC-205: page-scope the baked assets to what the page actually uses.
548
- registry: options.components.registry
549
- });
550
- };
551
- const devBypass = process.env.NODE_ENV === "development";
552
- if (!options.cache || perspective !== "published" || devBypass) return resolveFresh();
553
- const scope = options.cache.scope;
554
- const entry = await loadCachedEntry(
555
- nextCacheWrapper,
556
- canvasCacheKey({ scope, mode: options.cache.mode, url }),
557
- [projectSweepTag(scope)],
558
- async () => {
559
- const outcome = await resolveFresh();
560
- return { value: outcome, tags: nextCacheTags(outcome.tags, scope) };
561
- }
562
- );
563
- return entry.value;
587
+ if (!cache || perspective !== "published") return resolveFresh(url, perspective);
588
+ return cachedCanvasOutcome(...entryKey(url));
564
589
  };
565
590
  const Page = async (props) => {
566
591
  const { slug } = await props.params;
567
592
  const url = pathFromSlug(slug);
593
+ if (url === CANVAS_VALIDATION_PATH) return notFound();
568
594
  const perspective = await requestPerspective();
569
595
  const outcome = await loadOutcome(url, perspective);
570
596
  options.onTags?.(outcome.tags, { url, perspective });
@@ -594,8 +620,9 @@ function createCanvasRoute(options) {
594
620
  const pageMetadata = async (props) => {
595
621
  const { slug } = await props.params;
596
622
  const url = pathFromSlug(slug);
623
+ if (url === CANVAS_VALIDATION_PATH) return {};
597
624
  const perspective = await requestPerspective();
598
- if (options.cache && perspective === "published") {
625
+ if (cache && perspective === "published") {
599
626
  const outcome2 = await loadOutcome(url, perspective);
600
627
  if (outcome2.kind !== "render") return {};
601
628
  return options.metadata ? options.metadata(outcome2.result, { url, perspective }) : defaultMetadata(outcome2.result, outcome2.assets);
@@ -609,7 +636,10 @@ function createCanvasRoute(options) {
609
636
  const [page, resolvedParent] = await Promise.all([pageMetadata(props), parent]);
610
637
  return mergeMetadataOverParent(resolvedParent, page);
611
638
  };
612
- return { Page, generateMetadata };
639
+ const generateStaticParams = async () => [
640
+ { slug: [...CANVAS_VALIDATION_SLUG] }
641
+ ];
642
+ return { Page, generateMetadata, generateStaticParams };
613
643
  }
614
644
  function paramNamesOf(pattern) {
615
645
  return splitPath(pattern).filter((segment) => segment.startsWith(":")).map((segment) => segment.slice(1));
@@ -828,8 +858,8 @@ function createAssetImageLoader(options = {}) {
828
858
  return `${src}${separator}${widthParam}=${width}&${qualityParam}=${quality ?? defaultQuality}`;
829
859
  };
830
860
  }
831
- function createNextImagePrimitive(options) {
832
- return function NextImagePrimitive(props) {
861
+ function createNextImage(options) {
862
+ return function ElytraNextImage(props) {
833
863
  const { url, width, height, alt, focalPoint, lqip } = normalizeImageSource(props);
834
864
  if (url.length === 0) return null;
835
865
  return /* @__PURE__ */ jsx(
@@ -844,11 +874,12 @@ function createNextImagePrimitive(options) {
844
874
  );
845
875
  };
846
876
  }
847
- function nextImagePrimitive(options = {}) {
848
- return {
849
- manifest: ImageManifest,
850
- implementation: createNextImagePrimitive(options)
851
- };
877
+ function withNextImage(components, options = {}) {
878
+ const list = [...components ?? []];
879
+ const image = registerComponents(list).registry.roleComponent("image");
880
+ if (!image) return list;
881
+ const implementation = createNextImage(options);
882
+ return list.map((entry) => entry.manifest === image ? { manifest: entry.manifest, implementation } : entry);
852
883
  }
853
884
  function elytraHostImage(options = {}) {
854
885
  return ({ asset, alt, className, priority, sizes, loading }) => /* @__PURE__ */ jsx(
@@ -1171,17 +1202,7 @@ function createElytraHost(options) {
1171
1202
  }
1172
1203
  const contentMode = live ? "live" : options.fallbackContent ? "fallback" : "empty";
1173
1204
  const contentScope = live ? env.projectId : options.slug;
1174
- const components = defineHostComponents([
1175
- // Batteries included (EC-368 image defaults): graph `base.Image` nodes render
1176
- // through next/image — resized variants via the host's own optimizer. The
1177
- // canonical manifest stays (first registration wins in defineHostComponents).
1178
- ...options.image === false ? [] : [nextImagePrimitive(options.image ?? {})],
1179
- // EC-539: the option is the CONFIG's entry shape (`implementation: unknown`)
1180
- // so the recommended `{ ...site }` spread compiles; EC-541: registration
1181
- // takes that shape too and checks each implementation at runtime (see the
1182
- // option's doc comment) — no narrowing cast here.
1183
- ...options.components ?? []
1184
- ]);
1205
+ const components = defineHostComponents(options.components ?? []);
1185
1206
  const emptySource = createStaticContentSource({
1186
1207
  projectId: contentScope,
1187
1208
  locales: options.locales,
@@ -1252,6 +1273,7 @@ function createElytraHost(options) {
1252
1273
  return firstBlockFullBleed(resolved.document, components.registry);
1253
1274
  };
1254
1275
  return {
1276
+ collections: options.collections,
1255
1277
  contentScope,
1256
1278
  contentMode,
1257
1279
  env,
@@ -1269,6 +1291,6 @@ function createElytraHost(options) {
1269
1291
  }
1270
1292
  var PACKAGE = "@elytracms/next";
1271
1293
 
1272
- export { CanvasRenderer, ELYTRA_HOST_ENV_KEYS, ElytraImage, PACKAGE, assembleLiveSource, bodyItemsOf, canvasPageMetadata, canvasSitemapEntries, collectionCacheTag, createAssetImageLoader, createCanvasRoute, createCanvasSitemap, createContentSnapshot, createElytraHost, createPreviewDisableRoute, createPreviewRoute, createRevalidateRoute, createStaticContentSource, defineHostComponents, docCacheTag, elytraHostImage, evaluatePreviewRequest, evaluateRevalidateRequest, firstBlockFullBleed, loadCachedEntry, loadLiveContent, mergeCacheTags, mergeMetadataOverParent, nextCacheTags, nextImagePrimitive, openGraphLocale, pathFromSlug, projectSweepTag, resolveCanvasPage, resolveGlobals, resolveHostEnv, revalidationTags, routeCacheTag, safeRedirectTarget, scopeCacheTag };
1294
+ export { CANVAS_VALIDATION_SLUG, CanvasRenderer, ELYTRA_HOST_ENV_KEYS, ElytraImage, PACKAGE, assembleLiveSource, bodyItemsOf, canvasPageMetadata, canvasSitemapEntries, collectionCacheTag, createAssetImageLoader, createCanvasRoute, createCanvasSitemap, createContentSnapshot, createElytraHost, createPreviewDisableRoute, createPreviewRoute, createRevalidateRoute, createStaticContentSource, defineHostComponents, docCacheTag, elytraHostImage, evaluatePreviewRequest, evaluateRevalidateRequest, firstBlockFullBleed, loadLiveContent, mergeCacheTags, mergeMetadataOverParent, nextCacheTags, openGraphLocale, pathFromSlug, projectSweepTag, resolveCanvasPage, resolveGlobals, resolveHostEnv, revalidationTags, routeCacheTag, safeRedirectTarget, scopeCacheTag, withNextImage };
1273
1295
  //# sourceMappingURL=index.js.map
1274
1296
  //# sourceMappingURL=index.js.map