@elytracms/next 0.0.88 → 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.d.ts +314 -304
- package/dist/index.js +180 -173
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import { ComponentType, ReactNode } from 'react';
|
|
2
|
-
import { ComponentRegistry,
|
|
2
|
+
import { ComponentRegistry, ComponentManifest } from '@elytracms/core/component-registry';
|
|
3
3
|
export { ComponentManifest, PropField, SlotSpec, defineComponent } from '@elytracms/core/component-registry';
|
|
4
4
|
import { ComponentImplementations, RelationTarget, DocumentBodyItem } from '@elytracms/core/runtime-renderer';
|
|
5
5
|
export { ComponentImplementations, RenderAsset } from '@elytracms/core/runtime-renderer';
|
|
6
6
|
import { HostComponentEntry } from '@elytracms/core/studio-core';
|
|
7
|
-
import { ResolvePageResult, ResolvedAsset, Perspective, ContentClient, CacheTags, ContentDocumentSource, ResolvedDocument } from '@elytracms/core/content';
|
|
7
|
+
import { ContentQueryError, ResolvePageResult, ResolvedAsset, Perspective, ContentClient, CacheTags, ContentDocumentSource, ResolvedDocument } from '@elytracms/core/content';
|
|
8
8
|
export { Perspective } from '@elytracms/core/content';
|
|
9
|
-
import { LocaleConfig, Locale, RouteRecord, CollectionDef, RedirectRecord } from '@elytracms/core/cms-core';
|
|
10
|
-
export { CmsDocument, CollectionDef, FieldDef, Locale, LocaleConfig, LocaleRequest, LocaleStrategy, LocaleUrlPath, RedirectRecord, RouteRecord, documentSchema, fallbackHomeRoutes, localeConfigSchema, localeUrlPath, mergeRouteRecords, redirectRecordSchema, resolveLocale, routeRecordSchema, splitLocalePath, textDirection } from '@elytracms/core/cms-core';
|
|
9
|
+
import { LocaleConfig, Locale, RouteRecord, CollectionDef, RedirectRecord, LocaleConfigInput } from '@elytracms/core/cms-core';
|
|
10
|
+
export { CmsDocument, CollectionDef, FieldDef, Locale, LocaleConfig, LocaleConfigInput, LocaleRequest, LocaleStrategy, LocaleUrlPath, RedirectRecord, RouteRecord, documentSchema, fallbackHomeRoutes, localeConfigSchema, localeUrlPath, mergeRouteRecords, parseLocaleConfig, redirectRecordSchema, resolveLocale, routeRecordSchema, splitLocalePath, textDirection } from '@elytracms/core/cms-core';
|
|
11
11
|
import { AssetRecord, PersistenceAdapter } from '@elytracms/core/persistence';
|
|
12
12
|
export { AssetRecord } from '@elytracms/core/persistence';
|
|
13
|
+
import { Clock } from '@elytracms/core/determinism';
|
|
13
14
|
import { ResolvingMetadata, Metadata, ResolvedMetadata, MetadataRoute } from 'next';
|
|
14
|
-
import {
|
|
15
|
-
export {
|
|
15
|
+
import { RevalidationEvent } from '@elytracms/core/revalidation';
|
|
16
|
+
export { REVALIDATE_SIGNATURE_HEADER, RevalidationEvent, RevalidationEventKind, revalidationEventSchema, signRevalidationBody, signedRevalidationRequest } from '@elytracms/core/revalidation';
|
|
16
17
|
import { ImageLoader } from 'next/image';
|
|
17
18
|
import { HostImageRenderer } from './host-image.js';
|
|
19
|
+
export { z } from 'zod';
|
|
18
20
|
export { ComponentNode } from '@elytracms/core/project-graph';
|
|
19
21
|
|
|
20
22
|
/**
|
|
@@ -36,48 +38,43 @@ interface HostComponent {
|
|
|
36
38
|
/**
|
|
37
39
|
* The registered component surface a host app hands to `<CanvasRenderer />`
|
|
38
40
|
* and the catch-all route helper: a manifest registry plus the implementation
|
|
39
|
-
* map
|
|
40
|
-
*
|
|
41
|
+
* map. Registration problems — duplicate ids, invalid ids, role claims, and
|
|
42
|
+
* `invalid-implementation` for an entry whose `implementation` is not a React
|
|
43
|
+
* component — are structured issues in `registry.issues`, the ONE record every
|
|
44
|
+
* surface reads (EC-591; never thrown — an unknown component renders the EC-015
|
|
45
|
+
* fallback). The studio registers the same config list through the same core
|
|
46
|
+
* step, so it shows exactly these issues.
|
|
41
47
|
*/
|
|
42
48
|
interface HostComponents {
|
|
43
49
|
registry: ComponentRegistry;
|
|
44
50
|
implementations: ComponentImplementations;
|
|
45
|
-
/**
|
|
46
|
-
* Structural registration issues: the registry's own (duplicate ids, invalid
|
|
47
|
-
* namespaces, …) plus `invalid-implementation` for an entry whose
|
|
48
|
-
* `implementation` is not a React component (EC-541).
|
|
49
|
-
*/
|
|
50
|
-
issues: readonly ComponentIssue[];
|
|
51
|
-
}
|
|
52
|
-
interface DefineHostComponentsOptions {
|
|
53
|
-
/**
|
|
54
|
-
* Include the platform primitives (`base.*`) that ship with
|
|
55
|
-
* `@elytracms/core/runtime-renderer`. Defaults to `true` — graphs authored in the
|
|
56
|
-
* builder assume them.
|
|
57
|
-
*/
|
|
58
|
-
includeBasePrimitives?: boolean;
|
|
59
51
|
}
|
|
60
52
|
/**
|
|
61
|
-
* Component registration
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
53
|
+
* Component registration for the embedded runtime (EC-144, EC-591): the host
|
|
54
|
+
* registers EXACTLY the list it is given — the config's `components` — through
|
|
55
|
+
* core's one registration step `registerComponents`, the same step the studio
|
|
56
|
+
* registry and the preview harness call. The engine adds nothing: there are no
|
|
57
|
+
* base primitives and no override layer (EC-591 deleted `includeBasePrimitives`).
|
|
58
|
+
* The components Elytra ships are an opt-in package listed in the config like any
|
|
59
|
+
* other (`components: [...elytraComponents, ...siteComponents]`).
|
|
60
|
+
*
|
|
61
|
+
* One precedence rule: a duplicate id is refused WHOLE (`duplicate-id`, the first
|
|
62
|
+
* entry kept), so a manifest never ships with another entry's implementation.
|
|
63
|
+
* To replace a shipped component, list yours under the id and leave the shipped
|
|
64
|
+
* entry out; to swap only the Image implementation for `next/image`, wrap the
|
|
65
|
+
* list in {@link withNextImage}.
|
|
66
66
|
*
|
|
67
67
|
* Accepts the CONFIG's entry shape (`implementation: unknown`) rather than the
|
|
68
68
|
* strict {@link HostComponent}: the config declares implementations opaquely,
|
|
69
69
|
* and EC-539 made `createElytraHost({ ...site })` the contract, so a
|
|
70
70
|
* compile-time check at this seam was already gone for the recommended path.
|
|
71
|
-
* The check is RUNTIME instead (EC-541)
|
|
72
|
-
* `registerImplementations` (EC-555 — the studio registry and the preview
|
|
73
|
-
* harness call the same one): a value that is not a component is
|
|
71
|
+
* The check is RUNTIME instead (EC-541): a value that is not a component is
|
|
74
72
|
* recorded as an `invalid-implementation` issue naming the component id and
|
|
75
73
|
* the received kind, and is NOT put into `implementations` — so the renderer
|
|
76
74
|
* takes its missing-component path (a visible EC-015 fallback) instead of
|
|
77
75
|
* crashing the server render on `createElement` ("Element type is invalid")
|
|
78
76
|
* or, for a string, silently rendering an HTML tag. The manifest still
|
|
79
|
-
* registers, so the
|
|
80
|
-
* entry are reported alongside.
|
|
77
|
+
* registers, so the entry shows as broken rather than vanishing.
|
|
81
78
|
*
|
|
82
79
|
* One consequence of the `unknown` parameter: an INLINE
|
|
83
80
|
* `implementation: (props) => …` literal no longer gets `props` contextually
|
|
@@ -86,7 +83,7 @@ interface DefineHostComponentsOptions {
|
|
|
86
83
|
* anyway. (An overload pair was tried and does not restore it: TypeScript
|
|
87
84
|
* resolves the context-sensitive literal against the wider overload.)
|
|
88
85
|
*/
|
|
89
|
-
declare function defineHostComponents(components?: readonly HostComponentEntry[]
|
|
86
|
+
declare function defineHostComponents(components?: readonly HostComponentEntry[]): HostComponents;
|
|
90
87
|
|
|
91
88
|
/**
|
|
92
89
|
* `<CanvasRenderer />` (EC-144, vision AD-1): an async React Server Component
|
|
@@ -119,9 +116,10 @@ interface CanvasRendererProps {
|
|
|
119
116
|
* Documents each `listing` block resolved to, keyed by serialized query (EC-255,
|
|
120
117
|
* from the render outcome). A ProductGrid keyed to a picked `category` reads its
|
|
121
118
|
* products through this; an unknown key (or absent map) degrades to the block's
|
|
122
|
-
* own empty state — the same degradation as a missing relation.
|
|
119
|
+
* own empty state — the same degradation as a missing relation. A query that
|
|
120
|
+
* could not run is baked as its errors (EC-581 6a) and shown above the block.
|
|
123
121
|
*/
|
|
124
|
-
listings?: Record<string,
|
|
122
|
+
listings?: Record<string, BakedListing>;
|
|
125
123
|
/**
|
|
126
124
|
* URL paths for the documents this page's rich-text props LINK to, keyed
|
|
127
125
|
* `collection:id` (EC-291, from the render outcome). A self-typed internal link
|
|
@@ -154,6 +152,14 @@ interface DeliveryPresentation {
|
|
|
154
152
|
/** Field names whose values form the body, in render order. */
|
|
155
153
|
body?: readonly string[];
|
|
156
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* One listing's baked result (EC-255; EC-581 6a): the documents its query matched,
|
|
157
|
+
* or the structured errors `listDocuments` refused it with. Plain data — it rides
|
|
158
|
+
* the serializable render outcome and the RSC boundary like the other bakes.
|
|
159
|
+
*/
|
|
160
|
+
type BakedListing = RelationTarget[] | {
|
|
161
|
+
errors: readonly ContentQueryError[];
|
|
162
|
+
};
|
|
157
163
|
/**
|
|
158
164
|
* Render a resolved page server-side. Async so hosts can compose it like any
|
|
159
165
|
* other RSC; resolution itself is synchronous (the content was prefetched by
|
|
@@ -259,9 +265,9 @@ interface ContentSnapshotOptions {
|
|
|
259
265
|
* and EC-519 collapsed the two declarations into one; this was the last place
|
|
260
266
|
* in the chain still inventing the value, and it sits in the package that
|
|
261
267
|
* resolves the URLs. Hand it the project's `LocaleConfig` — the one the
|
|
262
|
-
* config declares.
|
|
268
|
+
* config declares. Parsed here (EC-580): a `default` outside `locales` throws.
|
|
263
269
|
*/
|
|
264
|
-
locales:
|
|
270
|
+
locales: LocaleConfigInput;
|
|
265
271
|
}
|
|
266
272
|
interface ContentSnapshot extends ContentSource {
|
|
267
273
|
readonly projectId: string;
|
|
@@ -275,8 +281,8 @@ interface ContentSnapshot extends ContentSource {
|
|
|
275
281
|
*/
|
|
276
282
|
interface StaticContentData {
|
|
277
283
|
projectId: string;
|
|
278
|
-
/** Locale configuration — **required**, never defaulted (see EC-530 above). */
|
|
279
|
-
locales:
|
|
284
|
+
/** Locale configuration — **required**, never defaulted (see EC-530 above); parsed (EC-580). */
|
|
285
|
+
locales: LocaleConfigInput;
|
|
280
286
|
/** Route records served by `resolvePage` (EC-019). */
|
|
281
287
|
routes?: readonly RouteRecord[];
|
|
282
288
|
/** Redirect records served by `resolvePage` (EC-019/020). */
|
|
@@ -394,8 +400,24 @@ type CanvasPageOutcome = {
|
|
|
394
400
|
* cannot ride the data cache or cross the RSC boundary. A listing depends on
|
|
395
401
|
* collection MEMBERSHIP (a product joining/leaving the category), so its
|
|
396
402
|
* invalidation rides the per-query `listDocuments` tags merged into `tags`.
|
|
403
|
+
*
|
|
404
|
+
* EC-581 6a: a query `listDocuments` refused (a non-filterable field, an
|
|
405
|
+
* operand of the wrong type, a bad limit) is baked as `{ errors }` — the
|
|
406
|
+
* structured `ContentQueryError`s — and `<CanvasRenderer />` shows them above
|
|
407
|
+
* the block (`data-ec-fallback="invalid-listing"`), never an empty grid
|
|
408
|
+
* indistinguishable from "no matching content".
|
|
397
409
|
*/
|
|
398
|
-
listings: Record<string,
|
|
410
|
+
listings: Record<string, BakedListing>;
|
|
411
|
+
/**
|
|
412
|
+
* EC-581 6a: whether resolving this page READ THE CLOCK — a listing on it
|
|
413
|
+
* binds an operand to `now` (e.g. "events whose date is still ahead"). Such a
|
|
414
|
+
* page's content changes as time passes, not only on publish: a cached entry
|
|
415
|
+
* of it is correct for the instant it was resolved and goes stale when a
|
|
416
|
+
* listed document crosses its `now` boundary, which no publish tag covers. A
|
|
417
|
+
* cache layer must bound that entry's life (the delivery cache — EC-515 —
|
|
418
|
+
* reads this). `false` for every page with no `now`-bound listing.
|
|
419
|
+
*/
|
|
420
|
+
readsNow: boolean;
|
|
399
421
|
/**
|
|
400
422
|
* The public URL path of every document a rich-text prop on this page LINKS
|
|
401
423
|
* to (EC-291), keyed `collection:id`. Baked into the outcome (serializable) so
|
|
@@ -432,6 +454,14 @@ interface ResolveCanvasPageOptions {
|
|
|
432
454
|
* resolution exactly, so no referenced image is ever dropped.
|
|
433
455
|
*/
|
|
434
456
|
registry?: ComponentRegistry;
|
|
457
|
+
/**
|
|
458
|
+
* The clock a `{ now: true }` listing operand reads (EC-581 6a) — an ISO-8601
|
|
459
|
+
* UTC instant, read at most once per resolution so every listing on the page
|
|
460
|
+
* compares against the SAME instant. Defaults to the system clock; tests and
|
|
461
|
+
* fixtures inject a fixed one. Taken as a FUNCTION, read during resolution, so a
|
|
462
|
+
* `now` value is never an argument a cache keys on.
|
|
463
|
+
*/
|
|
464
|
+
clock?: Clock;
|
|
435
465
|
}
|
|
436
466
|
/**
|
|
437
467
|
* Resolve one request URL against a content source: split the locale, run
|
|
@@ -443,9 +473,9 @@ declare function resolveCanvasPage(source: ContentSource, url: string, perspecti
|
|
|
443
473
|
|
|
444
474
|
interface CanvasCacheOptions {
|
|
445
475
|
/**
|
|
446
|
-
* Project scope prefix for every tag (`p:<scope>:…`) — must be the
|
|
447
|
-
*
|
|
448
|
-
*
|
|
476
|
+
* Project scope prefix for every tag (`p:<scope>:…`) — must be the SAME scope
|
|
477
|
+
* the revalidate receiver derives its tags with (`createRevalidateRoute`'s
|
|
478
|
+
* `scope`; the delivery kit hands its one `contentScope` to both, EC-579).
|
|
449
479
|
*/
|
|
450
480
|
scope?: string;
|
|
451
481
|
/**
|
|
@@ -462,8 +492,8 @@ interface CanvasCacheOptions {
|
|
|
462
492
|
* prefetched source (the fixture snapshot) can ignore it; a live loader uses
|
|
463
493
|
* it to fetch the draft-perspective snapshot only for draft-mode requests —
|
|
464
494
|
* it must NOT call request APIs like `draftMode()` itself, because on the
|
|
465
|
-
* cached published path the loader runs inside `
|
|
466
|
-
*
|
|
495
|
+
* cached published path the loader runs inside a `'use cache'` function, where
|
|
496
|
+
* request APIs are unavailable.
|
|
467
497
|
*/
|
|
468
498
|
interface CanvasContentRequest {
|
|
469
499
|
perspective: Perspective;
|
|
@@ -482,11 +512,12 @@ interface CanvasRouteOptions {
|
|
|
482
512
|
/** Host component surface from `defineHostComponents`. */
|
|
483
513
|
components: HostComponents;
|
|
484
514
|
/**
|
|
485
|
-
* EC-146 instant publish: cache the published-perspective resolution
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
*
|
|
515
|
+
* EC-146 instant publish: cache the published-perspective resolution as a
|
|
516
|
+
* `'use cache'` entry tagged with the response's own fetch-time tags,
|
|
517
|
+
* invalidated by the `/api/revalidate` webhook (EC-515). Draft mode and
|
|
518
|
+
* `next dev` always bypass it. Tag-driven only — no time-based revalidation
|
|
519
|
+
* (the AD-5 model, `CANVAS_CACHE_LIFE`). Requires `cacheComponents: true` in
|
|
520
|
+
* the host's `next.config`.
|
|
490
521
|
*/
|
|
491
522
|
cache?: CanvasCacheOptions;
|
|
492
523
|
/**
|
|
@@ -543,9 +574,41 @@ interface CanvasRoute {
|
|
|
543
574
|
* Next passes the parent segment's resolving metadata as the second argument;
|
|
544
575
|
* the route merges the page's tags over it (EC-610). A caller that invokes this
|
|
545
576
|
* by hand (the scaffold's `app/page.tsx`) forwards its own `parent`.
|
|
577
|
+
*
|
|
578
|
+
* `parent` is REQUIRED, not optional (EC-614): Next's webpack build validates a
|
|
579
|
+
* page's `generateMetadata` with `SecondArg<…> extends ResolvingMetadata`, and an
|
|
580
|
+
* optional parameter reads as `ResolvingMetadata | undefined`, which fails that
|
|
581
|
+
* constraint — a consumer re-exporting this hook could not `next build --webpack`
|
|
582
|
+
* on 0.0.88 (the Turbopack validator is lenient, which is how the gate missed it).
|
|
546
583
|
*/
|
|
547
|
-
generateMetadata: (props: CanvasRouteProps, parent
|
|
584
|
+
generateMetadata: (props: CanvasRouteProps, parent: ResolvingMetadata) => Promise<Metadata>;
|
|
585
|
+
/**
|
|
586
|
+
* `export const generateStaticParams = route.generateStaticParams` — REQUIRED in
|
|
587
|
+
* the catch-all page file under `cacheComponents` (EC-515, measured on Next
|
|
588
|
+
* 16.2.10): without it, awaiting the catch-all's `params` fails `next build`
|
|
589
|
+
* ("Uncached data was accessed outside of <Suspense>"). Wrapping the page in
|
|
590
|
+
* `<Suspense>` (or a root `loading.tsx`) instead also builds, but answers every
|
|
591
|
+
* 404 and every redirect with HTTP 200 — the status is committed before the
|
|
592
|
+
* content resolves. Declaring params keeps the page blocking, so `notFound()` is a
|
|
593
|
+
* real 404 and a redirect a real 308/307.
|
|
594
|
+
*
|
|
595
|
+
* It names ONE param set: {@link CANVAS_VALIDATION_SLUG}, a sample that Next
|
|
596
|
+
* prerenders to validate the route and that resolves to not-found without
|
|
597
|
+
* reading content (the build never touches the backend for it). It fits both
|
|
598
|
+
* catch-all shapes (`[[...slug]]` and the scaffold's required `[...slug]`, which
|
|
599
|
+
* rejects an empty slug), and it is never empty (Next rejects an empty list).
|
|
600
|
+
* Every real URL renders on its first request and is cached from then on.
|
|
601
|
+
*/
|
|
602
|
+
generateStaticParams: () => Promise<Array<{
|
|
603
|
+
slug: string[];
|
|
604
|
+
}>>;
|
|
548
605
|
}
|
|
606
|
+
/**
|
|
607
|
+
* The catch-all's build-time validation sample (see
|
|
608
|
+
* {@link CanvasRoute.generateStaticParams}). The path is reserved: the canvas
|
|
609
|
+
* route answers it with not-found and never resolves it against content.
|
|
610
|
+
*/
|
|
611
|
+
declare const CANVAS_VALIDATION_SLUG: readonly string[];
|
|
549
612
|
/** Create the `{ Page, generateMetadata }` pair for `app/[[...slug]]/page.tsx`. */
|
|
550
613
|
declare function createCanvasRoute(options: CanvasRouteOptions): CanvasRoute;
|
|
551
614
|
|
|
@@ -688,10 +751,20 @@ declare function createCanvasSitemap(options: CreateCanvasSitemapOptions): () =>
|
|
|
688
751
|
* Cache-tag pipeline (EC-146, vision AD-5) — the pure half.
|
|
689
752
|
*
|
|
690
753
|
* This module defines (a) the **canonical Next.js tag string format** mapping
|
|
691
|
-
* the accessor-level `CacheTags` (EC-143) onto `revalidateTag` keys,
|
|
692
|
-
* the **tag
|
|
693
|
-
*
|
|
694
|
-
*
|
|
754
|
+
* the accessor-level `CacheTags` (EC-143) onto `cacheTag` / `revalidateTag` keys,
|
|
755
|
+
* (b) the **tag derivation for a revalidation event** ({@link revalidationTags},
|
|
756
|
+
* EC-579 — the receiver's half of the one emitter), and (c) the **pure half of the
|
|
757
|
+
* route's cache entry** (EC-515): its key ({@link canvasCacheKey}), its `cacheTag`
|
|
758
|
+
* calls ({@link entryTagCalls}), its life ({@link CANVAS_CACHE_LIFE}) and the
|
|
759
|
+
* resolver registry the module-level `'use cache'` function reads its loader from
|
|
760
|
+
* ({@link registerCanvasResolver}). No Next.js imports — everything here is
|
|
761
|
+
* unit-testable; `route.tsx` holds the one `'use cache'` function that calls
|
|
762
|
+
* `cacheTag` / `cacheLife` with these values.
|
|
763
|
+
*
|
|
764
|
+
* This is the ONE tag module: the entries are tagged here and the invalidations
|
|
765
|
+
* are derived here, from the same formatters and the same scope. Before EC-579 the
|
|
766
|
+
* format was mirrored twice more (the studio's browser dispatcher and an inline
|
|
767
|
+
* literal in the Convex emitter), and each emitter brought its own scope.
|
|
695
768
|
*
|
|
696
769
|
* ## Tag format
|
|
697
770
|
*
|
|
@@ -709,8 +782,8 @@ declare function createCanvasSitemap(options: CreateCanvasSitemapOptions): () =>
|
|
|
709
782
|
* (alt/dimensions/re-upload) drops exactly the pages that baked it. Not
|
|
710
783
|
* locale-scoped — an asset record is locale-agnostic.
|
|
711
784
|
* - `project` — the whole-project sweep tag attached to **every** cached
|
|
712
|
-
* entry;
|
|
713
|
-
*
|
|
785
|
+
* entry; derived for the events whose effect no narrower tag names (a redirect
|
|
786
|
+
* change, an asset edit — see {@link revalidationTags}).
|
|
714
787
|
*
|
|
715
788
|
* ## Project scope
|
|
716
789
|
*
|
|
@@ -718,16 +791,11 @@ declare function createCanvasSitemap(options: CreateCanvasSitemapOptions): () =>
|
|
|
718
791
|
* `p:prj_aurora:doc:post:post-1`). The cost is a few bytes per tag and it
|
|
719
792
|
* keeps multi-project hosts (one Next app serving several builder projects)
|
|
720
793
|
* from cross-invalidating, so the prefix is **on whenever the host knows its
|
|
721
|
-
* project id** — decision: include it.
|
|
722
|
-
*
|
|
723
|
-
*
|
|
724
|
-
*
|
|
725
|
-
*
|
|
726
|
-
*
|
|
727
|
-
* The studio-side emitter cannot depend on this package (it would drag the
|
|
728
|
-
* `next` peer dependency into the studio bundle), so
|
|
729
|
-
* `packages/studio/src/lib/webhooks/tags.ts` mirrors these formatters; tests on
|
|
730
|
-
* both sides pin the exact literal strings to keep them in lock-step.
|
|
794
|
+
* project id** — decision: include it. Since EC-579 the scope has ONE owner: the
|
|
795
|
+
* host. The webhook carries an event, not tags, and the receiver derives the
|
|
796
|
+
* tags with the same scope its cache entries are written under (the delivery
|
|
797
|
+
* kit hands both the one `contentScope`), so emitter and host can no longer
|
|
798
|
+
* disagree about it.
|
|
731
799
|
*/
|
|
732
800
|
/** Apply the optional multi-project scope prefix to one tag. */
|
|
733
801
|
declare function scopeCacheTag(tag: string, scope?: string): string;
|
|
@@ -741,9 +809,8 @@ declare function collectionCacheTag(collection: string, locale: string, scope?:
|
|
|
741
809
|
/** Tag for one resolved route path (or redirect-chain hop) in one locale. */
|
|
742
810
|
declare function routeCacheTag(pattern: string, locale: string, scope?: string): string;
|
|
743
811
|
/**
|
|
744
|
-
* The whole-project sweep tag. Attached to every cached page entry;
|
|
745
|
-
*
|
|
746
|
-
* change's precise tag set is unknowable).
|
|
812
|
+
* The whole-project sweep tag. Attached to every cached page entry; derived for
|
|
813
|
+
* the events no narrower tag covers ({@link revalidationTags}).
|
|
747
814
|
*/
|
|
748
815
|
declare function projectSweepTag(scope?: string): string;
|
|
749
816
|
/**
|
|
@@ -752,204 +819,115 @@ declare function projectSweepTag(scope?: string): string;
|
|
|
752
819
|
* cache entry — including the project sweep tag.
|
|
753
820
|
*/
|
|
754
821
|
declare function nextCacheTags(tags: CacheTags, scope?: string): string[];
|
|
755
|
-
/**
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
*/
|
|
760
|
-
|
|
761
|
-
tags: string[];
|
|
762
|
-
/** `false` = never expire by time (tag-driven invalidation only). */
|
|
763
|
-
revalidate: number | false;
|
|
764
|
-
}
|
|
765
|
-
type CacheWrapper = <T>(cb: () => Promise<T>, keyParts: string[], options: CacheWrapperOptions) => () => Promise<T>;
|
|
766
|
-
/** What one cached request stores: the outcome plus its own discovered tags. */
|
|
767
|
-
interface CachedEntry<T> {
|
|
768
|
-
value: T;
|
|
769
|
-
/** The Next tag strings discovered at fetch time by the run that wrote this entry. */
|
|
770
|
-
tags: string[];
|
|
822
|
+
/** The receiver's own project — what its cached entries are tagged with. */
|
|
823
|
+
interface RevalidationProject {
|
|
824
|
+
/** The tag scope of this host's cache entries (`p:<scope>:…`), if any. */
|
|
825
|
+
scope?: string | undefined;
|
|
826
|
+
/** Every locale this host serves — one `collection:` tag per locale. */
|
|
827
|
+
locales: readonly string[];
|
|
771
828
|
}
|
|
772
829
|
/**
|
|
773
|
-
*
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
*
|
|
777
|
-
*
|
|
778
|
-
*
|
|
779
|
-
*
|
|
780
|
-
*
|
|
781
|
-
*
|
|
782
|
-
*
|
|
783
|
-
*
|
|
784
|
-
*
|
|
785
|
-
*
|
|
786
|
-
*
|
|
787
|
-
*
|
|
788
|
-
*
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
*
|
|
794
|
-
*
|
|
795
|
-
*
|
|
796
|
-
* 2. **Write pass** (miss only) — a second wrapper is constructed *now that
|
|
797
|
-
* the tags are known*, with `options.tags` = the discovered set, same key.
|
|
798
|
-
* Its callback just returns the already-loaded entry, so the backend is
|
|
799
|
-
* not hit again; `unstable_cache` persists the entry under the full
|
|
800
|
-
* fetch-time tag set. `revalidateTag` on any of those tags drops the
|
|
801
|
-
* entry and the next request repeats the cycle with fresh data.
|
|
802
|
-
*
|
|
803
|
-
* The loader runs **once per revalidation cycle** (per key), never once per
|
|
804
|
-
* request. Time-based revalidation is intentionally not exposed, and the reason
|
|
805
|
-
* is a real hazard rather than a preference: a stale-while-revalidate background
|
|
806
|
-
* refresh re-runs the probe callback, whose bail aborts the refresh, so the
|
|
807
|
-
* entry is never rewritten. `revalidate: false` keeps *time* from ever putting
|
|
808
|
-
* an entry into that state — which is the AD-5 model.
|
|
809
|
-
*
|
|
810
|
-
* **What `revalidate: false` does NOT buy (EC-515, measured on Next 16.2.10).**
|
|
811
|
-
* The earlier version of this comment claimed `revalidate: false` made the SWR
|
|
812
|
-
* hazard unreachable. It does not: a **tag** can put an entry into the stale
|
|
813
|
-
* state too, and then exactly the aborted-refresh loop above happens. Whether it
|
|
814
|
-
* does is decided by how the invalidator stamps the tag —
|
|
815
|
-
* `revalidateTag(tag, 'max')` marks entries stale (365-day expiry) and triggers
|
|
816
|
-
* the loop; `revalidateTag(tag, { expire: 0 })` drops them outright and does
|
|
817
|
-
* not. `revalidate.ts` therefore sends `{ expire: 0 }`, and the reasoning lives
|
|
818
|
-
* at that call site. Keep the two in step: this design only survives an
|
|
819
|
-
* invalidation that **drops** the entry, never one that merely staleness-marks
|
|
820
|
-
* it.
|
|
821
|
-
*
|
|
822
|
-
* Concurrency: wrappers and the captured closure are per-request, so there is
|
|
823
|
-
* no cross-request mutation; two racing first requests both resolve and the
|
|
824
|
-
* last write wins (deterministic content ⇒ identical entries).
|
|
825
|
-
*
|
|
826
|
-
* ## ⚠️ Self-hosted hosts: only the BASELINE tags invalidate (EC-515)
|
|
827
|
-
*
|
|
828
|
-
* **Measured on a self-hosted `next start` (Next 16.2.10, default
|
|
829
|
-
* `FileSystemCache`, `.next/cache` on disk); NOT measured on Vercel.**
|
|
830
|
-
*
|
|
831
|
-
* Fetch-time tag discovery writes the entry with the tags the response actually
|
|
832
|
-
* read — but the default self-hosted cache handler decides staleness and expiry
|
|
833
|
-
* from the tags the **reading** wrapper names (`ctx.tags`, see
|
|
834
|
-
* `IncrementalCache.get` / `FileSystemCache.get` for `kind: FETCH`), not from
|
|
835
|
-
* the tags the entry was **stored** with. The stored tag set is only ever used
|
|
836
|
-
* to decide whether to re-write the entry with a superset.
|
|
837
|
-
*
|
|
838
|
-
* Every read in {@link loadCachedEntry} goes through the **probe** wrapper,
|
|
839
|
-
* whose tags are `baselineTags`. So on such a host:
|
|
840
|
-
*
|
|
841
|
-
* - revalidating a tag that is in `baselineTags` works (the route helper passes
|
|
842
|
-
* the project sweep tag, so `project` sweeps do invalidate);
|
|
843
|
-
* - revalidating a *discovered* `doc:` / `collection:` / `route:` / `asset:` tag
|
|
844
|
-
* invalidates **nothing** — silently. No error, no log line, no loader run.
|
|
845
|
-
*
|
|
846
|
-
* Measured 2×2 that isolates it (same host, same run): a probe handed the full
|
|
847
|
-
* discovered tag set up front DOES honour a `doc:` tag, and the same probe
|
|
848
|
-
* handed only the baseline does not. So the tags, their formatting and the
|
|
849
|
-
* webhook are all correct — the read simply never consults them.
|
|
850
|
-
*
|
|
851
|
-
* Consequence for a self-hosted deployment: invalidation granularity collapses
|
|
852
|
-
* to whatever is in `baselineTags`. The studio's Convex write chokepoint emits
|
|
853
|
-
* the project sweep on every publish, so instant publish still *works* there,
|
|
854
|
-
* coarsely; the browser dispatcher's targeted `doc:`/`collection:` tag set
|
|
855
|
-
* (`packages/studio/src/lib/webhooks/tags.ts`) does not reach a self-hosted host
|
|
856
|
-
* at all. Vercel-hosted deployments are unaffected in practice — both consumer
|
|
857
|
-
* sites were measured healthy on 2026-08-01 — which is consistent with Vercel's
|
|
858
|
-
* cache handler purging by the entry's stored tags, though that has **not** been
|
|
859
|
-
* measured here.
|
|
860
|
-
*
|
|
861
|
-
* Fixing this properly means the read must be able to name the entry's own tags,
|
|
862
|
-
* which no `unstable_cache` API allows without restructuring the probe/bail
|
|
863
|
-
* seam. That is a deliberate open decision — see EC-515, do not "fix" it here
|
|
864
|
-
* incidentally.
|
|
830
|
+
* The tags one revalidation event invalidates on this host — sorted, deduplicated,
|
|
831
|
+
* scoped with the receiver's OWN scope, so the emitter never needs to know it.
|
|
832
|
+
*
|
|
833
|
+
* - `publish` / `unpublish` / `delete` → `doc:<collection>:<id>` (its detail page and
|
|
834
|
+
* every cached list that contained it) + `collection:<collection>:<locale>` for
|
|
835
|
+
* EVERY locale this host serves (list membership may have changed, and a URL
|
|
836
|
+
* cached as not-found for a document that now exists carries it too). **No
|
|
837
|
+
* sweep** — a publish drops exactly the entries that read the document or its
|
|
838
|
+
* collection (EC-515);
|
|
839
|
+
* - `redirects` → the sweep (routing can change any URL; see the `redirects` trigger
|
|
840
|
+
* in the studio's `convex/triggers.ts` for why a targeted `route:` tag is wrong);
|
|
841
|
+
* - `asset` → `asset:<id>` + the sweep (a page reading the asset through a document
|
|
842
|
+
* asset FIELD carries no `asset:` tag — EC-566 D16, EC-589);
|
|
843
|
+
* - `probe` → nothing (the doctor's no-op ping).
|
|
844
|
+
*
|
|
845
|
+
* Why the precise tags are enough now (EC-515, option D): each route entry declares
|
|
846
|
+
* its tags with `cacheTag` from INSIDE the cached function, and Next decides an
|
|
847
|
+
* entry's staleness from the tags it was STORED with — so a `doc:` tag drops the
|
|
848
|
+
* entries that read the document, on a self-hosted host as on any other. Until
|
|
849
|
+
* then the read named only the baseline sweep (`unstable_cache` judged the
|
|
850
|
+
* reading wrapper's tags), every precise tag was inert on a self-hosted host, and
|
|
851
|
+
* every publish carried the sweep to compensate (wave-0091). Measured both ways
|
|
852
|
+
* in EC-515's harness.
|
|
865
853
|
*/
|
|
866
|
-
declare function
|
|
854
|
+
declare function revalidationTags(event: RevalidationEvent, project: RevalidationProject): string[];
|
|
867
855
|
|
|
868
856
|
/**
|
|
869
|
-
* Pure
|
|
870
|
-
*
|
|
871
|
-
*
|
|
872
|
-
*
|
|
873
|
-
*
|
|
874
|
-
*
|
|
875
|
-
*
|
|
876
|
-
*
|
|
857
|
+
* Pure request logic of the revalidation webhook receiver (EC-146), kept free of
|
|
858
|
+
* Next.js imports so it is unit-testable. `revalidate.ts` wires it into a route
|
|
859
|
+
* handler with `revalidateTag`.
|
|
860
|
+
*
|
|
861
|
+
* The wire — event schema, signer, header — is NOT declared here: it is the one
|
|
862
|
+
* module `@elytracms/core/revalidation`, shared with the emitter (the studio's
|
|
863
|
+
* Convex write chokepoint) and the doctor probe (EC-579). This file only decides
|
|
864
|
+
* the HTTP answer: auth before parsing, then the event.
|
|
877
865
|
*/
|
|
878
|
-
/** Header carrying the HMAC signature. */
|
|
879
|
-
declare const REVALIDATE_SIGNATURE_HEADER = "x-elytra-signature";
|
|
880
|
-
/** The webhook body: which tags to revalidate, and why. */
|
|
881
|
-
declare const revalidatePayloadSchema: z.ZodObject<{
|
|
882
|
-
projectId: z.ZodString;
|
|
883
|
-
event: z.ZodString;
|
|
884
|
-
tags: z.ZodArray<z.ZodString>;
|
|
885
|
-
timestamp: z.ZodString;
|
|
886
|
-
}, z.core.$strip>;
|
|
887
|
-
type RevalidatePayload = z.infer<typeof revalidatePayloadSchema>;
|
|
888
|
-
/** Compute the signature header value for a raw body (emitter/test side). */
|
|
889
|
-
declare function signRevalidateBody(rawBody: string, secret: string): string;
|
|
890
866
|
type RevalidateEvaluation = {
|
|
891
867
|
ok: true;
|
|
892
|
-
|
|
868
|
+
event: RevalidationEvent;
|
|
893
869
|
} | {
|
|
894
870
|
ok: false;
|
|
895
871
|
status: 400 | 401;
|
|
896
872
|
message: string;
|
|
897
873
|
};
|
|
898
874
|
/**
|
|
899
|
-
* Evaluate one webhook request: verify the HMAC first (auth before parsing),
|
|
900
|
-
*
|
|
901
|
-
*
|
|
902
|
-
* - unconfigured (empty) secret → 401 for every request: the endpoint stays
|
|
903
|
-
*
|
|
904
|
-
* - missing/malformed/mismatched signature → 401 (
|
|
905
|
-
* - unparseable or schema-invalid body → 400
|
|
875
|
+
* Evaluate one webhook request: verify the HMAC first (auth before parsing), then
|
|
876
|
+
* read the event.
|
|
877
|
+
*
|
|
878
|
+
* - unconfigured (empty) secret → 401 for every request: the endpoint stays closed
|
|
879
|
+
* until a secret is deliberately configured (mirrors EC-144 preview);
|
|
880
|
+
* - missing/malformed/mismatched signature → 401 (constant-time verify);
|
|
881
|
+
* - unparseable or schema-invalid body → 400, nothing revalidated; a body in the
|
|
882
|
+
* pre-EC-579 tag-list shape gets its own named 400 (an older Elytra emitter —
|
|
883
|
+
* deploy the matching studio functions), because a version skew between the
|
|
884
|
+
* Convex deployment and this host is the one way a correctly signed body can
|
|
885
|
+
* arrive in the wrong shape.
|
|
906
886
|
*/
|
|
907
|
-
declare function evaluateRevalidateRequest(rawBody: string, signature: string | null | undefined, secret: string | undefined): RevalidateEvaluation
|
|
887
|
+
declare function evaluateRevalidateRequest(rawBody: string, signature: string | null | undefined, secret: string | undefined): Promise<RevalidateEvaluation>;
|
|
908
888
|
|
|
909
889
|
/**
|
|
910
890
|
* Revalidation webhook route handler (EC-146): the receiving end of the
|
|
911
|
-
* publish → live-in-seconds pipeline. The studio's
|
|
912
|
-
* `{
|
|
913
|
-
* HMAC over the raw body
|
|
914
|
-
*
|
|
915
|
-
*
|
|
916
|
-
*
|
|
917
|
-
*
|
|
891
|
+
* publish → live-in-seconds pipeline. The studio's Convex write chokepoint — the ONE
|
|
892
|
+
* emitter (EC-579) — POSTs a signed EVENT (`{ kind, collection?, id? }`, the wire in
|
|
893
|
+
* `@elytracms/core/revalidation`); this handler verifies the HMAC over the raw body,
|
|
894
|
+
* derives the tags from the event with this host's OWN scope and locales
|
|
895
|
+
* (`revalidationTags`, `cache.ts`) and calls `revalidateTag` for each, dropping the
|
|
896
|
+
* cached entries whose fetch-time tag sets contained them.
|
|
897
|
+
*
|
|
898
|
+
* Server-to-server only: the emitter runs in Convex, so there is no CORS and no
|
|
899
|
+
* `OPTIONS` preflight (the studio's browser dispatcher that needed them is deleted).
|
|
900
|
+
* Server-only by construction — consumed from an App Router route handler file,
|
|
901
|
+
* never from client bundles.
|
|
918
902
|
*/
|
|
919
|
-
interface RevalidateRouteOptions {
|
|
903
|
+
interface RevalidateRouteOptions extends RevalidationProject {
|
|
920
904
|
/**
|
|
921
905
|
* Shared HMAC secret. An empty/undefined secret rejects every request with
|
|
922
906
|
* 401 — the endpoint stays closed until deliberately configured.
|
|
923
907
|
*/
|
|
924
908
|
secret: string | undefined;
|
|
925
|
-
/**
|
|
926
|
-
* CORS origin allowed to call this endpoint (e.g. the studio's origin).
|
|
927
|
-
* The v1 dispatcher is a browser `fetch` from the studio, and the custom
|
|
928
|
-
* signature header always triggers a CORS preflight — so cross-origin
|
|
929
|
-
* studio → host dispatch only works when this is set. Server-side emitters
|
|
930
|
-
* (the Convex action variant, CLIs, CI) need no CORS and may leave it
|
|
931
|
-
* unset. Never use `*` with a real secret-bearing deployment unless you
|
|
932
|
-
* accept that any origin may *attempt* deliveries (they still need the
|
|
933
|
-
* secret to have any effect).
|
|
934
|
-
*/
|
|
935
|
-
allowOrigin?: string;
|
|
936
909
|
}
|
|
937
910
|
interface RevalidateRouteHandlers {
|
|
938
911
|
POST(request: Request): Promise<Response>;
|
|
939
|
-
OPTIONS(): Promise<Response>;
|
|
940
912
|
}
|
|
941
913
|
/**
|
|
942
|
-
* Create the webhook route
|
|
914
|
+
* Create the webhook route handler for `app/api/revalidate/route.ts`. Through the
|
|
915
|
+
* delivery kit it is `export const { POST } = elytra.revalidateRoute()`, which hands
|
|
916
|
+
* it the kit's own scope and locales — the ones its cached entries carry. Wiring it
|
|
917
|
+
* by hand, pass the SAME scope your canvas route caches under:
|
|
943
918
|
*
|
|
944
919
|
* ```ts
|
|
945
|
-
* export const { POST
|
|
920
|
+
* export const { POST } = createRevalidateRoute({
|
|
946
921
|
* secret: process.env.REVALIDATE_SECRET,
|
|
922
|
+
* scope: process.env.ELYTRA_PROJECT_ID,
|
|
923
|
+
* locales: ['en', 'de'],
|
|
947
924
|
* })
|
|
948
925
|
* ```
|
|
949
926
|
*
|
|
950
|
-
* Unsigned/invalid requests are rejected with 401 (
|
|
951
|
-
*
|
|
952
|
-
*
|
|
927
|
+
* Unsigned/invalid requests are rejected with 401 (constant-time HMAC verify, see
|
|
928
|
+
* `evaluateRevalidateRequest`), signed-but-unreadable ones with 400; a valid event
|
|
929
|
+
* revalidates its derived tags and answers `{ revalidated: tags }` (`[]` for the
|
|
930
|
+
* doctor's `probe`).
|
|
953
931
|
*/
|
|
954
932
|
declare function createRevalidateRoute(options: RevalidateRouteOptions): RevalidateRouteHandlers;
|
|
955
933
|
|
|
@@ -1055,20 +1033,12 @@ interface AssetImageLoaderOptions {
|
|
|
1055
1033
|
*/
|
|
1056
1034
|
declare function createAssetImageLoader(options?: AssetImageLoaderOptions): ImageLoader;
|
|
1057
1035
|
/**
|
|
1058
|
-
*
|
|
1059
|
-
* canonical manifest, rendered optimized through {@link ElytraImage}. The
|
|
1060
|
-
* renderer resolves an `asset`-typed `src` (EC-195) to `{ url, width, height,
|
|
1061
|
-
* alt }` before this runs, so `normalizeImageSource` yields intrinsic dimensions
|
|
1062
|
-
* (→ no layout shift, sized variants) whether the editor picked an uploaded asset
|
|
1063
|
-
* or pasted a URL. `priority` opts an above-the-fold image out of lazy-loading.
|
|
1064
|
-
*/
|
|
1065
|
-
/**
|
|
1066
|
-
* Per-deployment image-delivery options for {@link nextImagePrimitive} (EC-271).
|
|
1036
|
+
* Per-deployment image-delivery options for {@link withNextImage} (EC-271).
|
|
1067
1037
|
* The default is the host's own Next.js optimizer (Vercel) — zero config. A host
|
|
1068
1038
|
* on a different CDN supplies its own `loader` (a plain function) to route variants
|
|
1069
1039
|
* through that CDN instead, without touching component code.
|
|
1070
1040
|
*/
|
|
1071
|
-
interface
|
|
1041
|
+
interface NextImageOptions {
|
|
1072
1042
|
/**
|
|
1073
1043
|
* Custom `next/image` loader — the override seam for non-Vercel hosting. Omit
|
|
1074
1044
|
* for the Vercel default (Next's own `/_next/image` optimizer). Supply a
|
|
@@ -1087,31 +1057,35 @@ interface NextImagePrimitiveOptions {
|
|
|
1087
1057
|
quality?: number;
|
|
1088
1058
|
}
|
|
1089
1059
|
/**
|
|
1090
|
-
*
|
|
1091
|
-
*
|
|
1092
|
-
* (
|
|
1093
|
-
*
|
|
1094
|
-
*
|
|
1060
|
+
* Batteries included, opted into (EC-591): return the component list with the
|
|
1061
|
+
* implementation of the component that fills the `image` role swapped for
|
|
1062
|
+
* `next/image` delivery ({@link ElytraImage}) — the manifest is kept, so the
|
|
1063
|
+
* studio and the host still register the same component. Nothing is added to the
|
|
1064
|
+
* list and nothing is registered by the engine: no image-role component in the
|
|
1065
|
+
* list → the list comes back unchanged.
|
|
1066
|
+
*
|
|
1067
|
+
* Which entry fills the role is decided by core's one registration rule (the first
|
|
1068
|
+
* valid claimant — `ComponentRegistry.roleComponent('image')`), so the swap can
|
|
1069
|
+
* never land on a different entry than the one the registry resolves. Use it on
|
|
1070
|
+
* the DELIVERY side only (the studio's canvas runs on Vite, where `next/image`
|
|
1071
|
+
* does not exist) — the scaffold's host seam file does it in one line:
|
|
1095
1072
|
*
|
|
1096
1073
|
* ```ts
|
|
1097
|
-
* export const
|
|
1098
|
-
* nextImagePrimitive(), // Vercel default optimizer
|
|
1099
|
-
* // ...project components
|
|
1100
|
-
* ])
|
|
1074
|
+
* export const elytra = createElytraHost({ ...site, components: withNextImage(site.components) })
|
|
1101
1075
|
* ```
|
|
1102
1076
|
*
|
|
1103
|
-
* Pass {@link
|
|
1104
|
-
*
|
|
1077
|
+
* Pass {@link NextImageOptions} to delegate image delivery to another CDN
|
|
1078
|
+
* (EC-271) — the v1 default is Vercel, with room to grow:
|
|
1105
1079
|
*
|
|
1106
1080
|
* ```ts
|
|
1107
|
-
*
|
|
1108
|
-
*
|
|
1081
|
+
* withNextImage(site.components, { loader: createAssetImageLoader() }) // ?w=&q= CDN
|
|
1082
|
+
* withNextImage(site.components, { loader: cloudflareImageLoader }) // Cloudflare Images
|
|
1109
1083
|
* ```
|
|
1110
1084
|
*/
|
|
1111
|
-
declare function
|
|
1085
|
+
declare function withNextImage(components: readonly HostComponentEntry[] | undefined, options?: NextImageOptions): HostComponentEntry[];
|
|
1112
1086
|
/** Per-deployment options for {@link elytraHostImage} (the EC-271 loader seam). */
|
|
1113
1087
|
interface ElytraHostImageOptions {
|
|
1114
|
-
/** Custom `next/image` loader for non-Vercel CDNs (see {@link
|
|
1088
|
+
/** Custom `next/image` loader for non-Vercel CDNs (see {@link NextImageOptions}). */
|
|
1115
1089
|
loader?: ImageLoader;
|
|
1116
1090
|
/** Default quality (1–100) when a call site doesn't set one (Next defaults to 75). */
|
|
1117
1091
|
quality?: number;
|
|
@@ -1152,7 +1126,9 @@ declare function elytraHostImage(options?: ElytraHostImageOptions): HostImageRen
|
|
|
1152
1126
|
* | | equal the Convex deployment's own env var) |
|
|
1153
1127
|
* | `REVALIDATE_SECRET` | Instant-publish webhook HMAC secret (must equal the |
|
|
1154
1128
|
* | | deployment's `CANVAS_REVALIDATE_SECRET`) |
|
|
1155
|
-
*
|
|
1129
|
+
*
|
|
1130
|
+
* EC-579 retired `STUDIO_ORIGIN` (the CORS origin of the studio's deleted browser
|
|
1131
|
+
* dispatcher): the one emitter is the Convex deployment, server to server.
|
|
1156
1132
|
*/
|
|
1157
1133
|
/** The kit's documented env names (single source of truth, also used in docs/tests). */
|
|
1158
1134
|
declare const ELYTRA_HOST_ENV_KEYS: {
|
|
@@ -1160,14 +1136,12 @@ declare const ELYTRA_HOST_ENV_KEYS: {
|
|
|
1160
1136
|
readonly projectId: "ELYTRA_PROJECT_ID";
|
|
1161
1137
|
readonly draftToken: "DRAFT_PREVIEW_TOKEN";
|
|
1162
1138
|
readonly revalidateSecret: "REVALIDATE_SECRET";
|
|
1163
|
-
readonly studioOrigin: "STUDIO_ORIGIN";
|
|
1164
1139
|
};
|
|
1165
1140
|
interface ElytraHostEnv {
|
|
1166
1141
|
convexUrl?: string;
|
|
1167
1142
|
projectId?: string;
|
|
1168
1143
|
draftToken?: string;
|
|
1169
1144
|
revalidateSecret?: string;
|
|
1170
|
-
studioOrigin?: string;
|
|
1171
1145
|
}
|
|
1172
1146
|
/**
|
|
1173
1147
|
* Resolve the kit's env bag from a raw env record (`process.env` by default at
|
|
@@ -1336,9 +1310,10 @@ declare function createPreviewDisableRoute(): PreviewRouteHandlers;
|
|
|
1336
1310
|
* const route = elytra.canvasRoute()
|
|
1337
1311
|
* export default route.Page
|
|
1338
1312
|
* export const generateMetadata = route.generateMetadata
|
|
1313
|
+
* export const generateStaticParams = route.generateStaticParams // cacheComponents (EC-515)
|
|
1339
1314
|
*
|
|
1340
1315
|
* // app/api/revalidate/route.ts
|
|
1341
|
-
* export const { POST
|
|
1316
|
+
* export const { POST } = elytra.revalidateRoute()
|
|
1342
1317
|
*
|
|
1343
1318
|
* // app/api/draft/route.ts — GET /api/draft?token=…&redirect=/path
|
|
1344
1319
|
* export const { GET } = elytra.previewRoute()
|
|
@@ -1354,7 +1329,12 @@ declare function createPreviewDisableRoute(): PreviewRouteHandlers;
|
|
|
1354
1329
|
* keeps ownership of project config (AD-11), host components, form endpoints,
|
|
1355
1330
|
* and anything genuinely bespoke.
|
|
1356
1331
|
*/
|
|
1357
|
-
|
|
1332
|
+
/**
|
|
1333
|
+
* EC-581 (ratified 2b): the options are generic over the config's `collections`, so a
|
|
1334
|
+
* project declared with `defineCollection` keeps its literal types through the host and
|
|
1335
|
+
* `InferDocument<typeof host, 'post'>` (`@elytracms/core/cms-core`) is a delivered post.
|
|
1336
|
+
*/
|
|
1337
|
+
interface ElytraHostOptions<Collections extends readonly CollectionDef[] = readonly CollectionDef[]> {
|
|
1358
1338
|
/**
|
|
1359
1339
|
* The project slug (config-as-code identity, mirrors `elytra.config.ts`).
|
|
1360
1340
|
* Used as the content scope when no live backend is configured.
|
|
@@ -1364,7 +1344,7 @@ interface ElytraHostOptions {
|
|
|
1364
1344
|
* Locale config — config-owned (AD-11), never read from the snapshot.
|
|
1365
1345
|
*
|
|
1366
1346
|
* **Import it, do not restate it** (EC-519). This is the *same*
|
|
1367
|
-
* {@link
|
|
1347
|
+
* {@link LocaleConfigInput} `elytra.config.ts` takes, so a project declares its
|
|
1368
1348
|
* locales once and hands that one binding to both mounts. It stays a host
|
|
1369
1349
|
* option because the engine cannot know where a repo keeps the value — but it
|
|
1370
1350
|
* is no longer an option you have to *retype*: since EC-532 the delivery
|
|
@@ -1376,17 +1356,26 @@ interface ElytraHostOptions {
|
|
|
1376
1356
|
* fact `locales` + `defaultLocale`, and in our own demo the two drifted — the
|
|
1377
1357
|
* host said `['en']`, the config said `['en','de']`, and every `/de` URL 404'd
|
|
1378
1358
|
* silently for weeks (EC-512).
|
|
1359
|
+
*
|
|
1360
|
+
* The DECLARED shape (EC-580): the kit parses it into the engine's `LocaleConfig`
|
|
1361
|
+
* where every content source is assembled (`createStaticContentSource` — the
|
|
1362
|
+
* empty source is built at boot), so a `default` outside `locales` stops the host
|
|
1363
|
+
* with a located message instead of serving — the same rule the studio's
|
|
1364
|
+
* workspace gate applies to the same value.
|
|
1379
1365
|
*/
|
|
1380
|
-
locales:
|
|
1366
|
+
locales: LocaleConfigInput;
|
|
1381
1367
|
/** CMS collection schema — config-owned (AD-11). */
|
|
1382
|
-
collections:
|
|
1368
|
+
collections: Collections;
|
|
1383
1369
|
/** Routes as code (AD-3/AD-11): records, or the `(collections) => records` derive. */
|
|
1384
1370
|
routes: readonly RouteRecord[] | ((collections: CollectionDef[]) => RouteRecord[]);
|
|
1385
1371
|
/**
|
|
1386
1372
|
* The repo's page-builder components (manifest + implementation pairs — the
|
|
1387
|
-
* SAME
|
|
1388
|
-
*
|
|
1389
|
-
*
|
|
1373
|
+
* SAME list the studio composes with, usually `site.components` via the
|
|
1374
|
+
* `{ ...site }` spread). The host registers exactly this list (EC-591): the
|
|
1375
|
+
* engine adds no components of its own — the shipped ones are the opt-in
|
|
1376
|
+
* `@elytracms/components` package, listed in the config. For `next/image`
|
|
1377
|
+
* delivery of the image-role component, pass the list through
|
|
1378
|
+
* `withNextImage(site.components)` (one line, opt-in).
|
|
1390
1379
|
*
|
|
1391
1380
|
* Typed as the CONFIG's `HostComponentEntry` (`implementation: unknown`), not
|
|
1392
1381
|
* the stricter {@link HostComponent}, so that `createElytraHost({ ...site })`
|
|
@@ -1403,19 +1392,13 @@ interface ElytraHostOptions {
|
|
|
1403
1392
|
* component. The check is RUNTIME instead — `defineHostComponents` takes the
|
|
1404
1393
|
* same `HostComponentEntry` shape (no cast, one signature) and tests every
|
|
1405
1394
|
* implementation on registration; a value that is not a component becomes an
|
|
1406
|
-
* `invalid-implementation` issue in `components.issues` and is withheld from
|
|
1395
|
+
* `invalid-implementation` issue in `components.registry.issues` and is withheld from
|
|
1407
1396
|
* the implementation map, so its nodes render the visible missing-component
|
|
1408
1397
|
* fallback rather than crashing the server render. Authoring a component
|
|
1409
1398
|
* module as the strict {@link HostComponent} keeps the compile-time check at
|
|
1410
1399
|
* the place the implementation is written.
|
|
1411
1400
|
*/
|
|
1412
1401
|
components?: readonly HostComponentEntry[];
|
|
1413
|
-
/**
|
|
1414
|
-
* `base.Image` delivery options (loader/sizes/quality — the non-Vercel CDN
|
|
1415
|
-
* seam), or `false` to keep the plain-`<img>` primitive. Default: Next's own
|
|
1416
|
-
* optimizer, registered out of the box.
|
|
1417
|
-
*/
|
|
1418
|
-
image?: NextImagePrimitiveOptions | false;
|
|
1419
1402
|
/**
|
|
1420
1403
|
* The public base URL of the delivery site (mirrors the config's
|
|
1421
1404
|
* `frontendUrl`) — exposed on the handle for sitemap/canonical use.
|
|
@@ -1424,7 +1407,7 @@ interface ElytraHostOptions {
|
|
|
1424
1407
|
/**
|
|
1425
1408
|
* Env bag; defaults to `process.env`. Documented names (see `host-env.ts`):
|
|
1426
1409
|
* `ELYTRA_CONVEX_URL`, `ELYTRA_PROJECT_ID`, `DRAFT_PREVIEW_TOKEN`,
|
|
1427
|
-
* `REVALIDATE_SECRET
|
|
1410
|
+
* `REVALIDATE_SECRET`.
|
|
1428
1411
|
*/
|
|
1429
1412
|
env?: Record<string, string | undefined>;
|
|
1430
1413
|
/**
|
|
@@ -1455,13 +1438,18 @@ interface HostCanvasRouteOptions {
|
|
|
1455
1438
|
*/
|
|
1456
1439
|
wrap?: (children: ReactNode, ctx: CanvasWrapContext) => ReactNode;
|
|
1457
1440
|
/**
|
|
1458
|
-
* Tagged
|
|
1459
|
-
*
|
|
1460
|
-
*
|
|
1461
|
-
*
|
|
1462
|
-
*
|
|
1441
|
+
* Tagged cache entry (instant publish): one `'use cache'` entry per URL, tagged
|
|
1442
|
+
* with what its resolution read (EC-515; the host enables `cacheComponents`).
|
|
1443
|
+
* Always on and always scoped to the kit's {@link ElytraHost.contentScope}, with
|
|
1444
|
+
* the kit's {@link ElytraHost.contentMode} in the entry key (EC-543). `false`
|
|
1445
|
+
* disables caching entirely.
|
|
1446
|
+
*
|
|
1447
|
+
* There is no scope override (EC-579): the kit's revalidate receiver derives its
|
|
1448
|
+
* tags with that same `contentScope`, so one value scopes both the entries and
|
|
1449
|
+
* their invalidation. An override here was the one way left for the two to
|
|
1450
|
+
* disagree — and a disagreement drops nothing, silently.
|
|
1463
1451
|
*/
|
|
1464
|
-
cache?:
|
|
1452
|
+
cache?: false;
|
|
1465
1453
|
}
|
|
1466
1454
|
interface LoadGlobalsOptions {
|
|
1467
1455
|
/** Perspective of the chrome read. Default `'published'` (live is published-only). */
|
|
@@ -1485,21 +1473,28 @@ interface LoadGlobalsOptions {
|
|
|
1485
1473
|
* once, read here, so a host can *say* which world it is in instead of
|
|
1486
1474
|
* re-deriving it from `env` and drifting.
|
|
1487
1475
|
*
|
|
1488
|
-
* **The mode is also part of the route's
|
|
1489
|
-
*
|
|
1490
|
-
* until 0.0.85 the "clean 404" above held
|
|
1491
|
-
* `next start` with both env vars blanked and a
|
|
1492
|
-
* run answered `200` with the live world's bytes
|
|
1493
|
-
* reporting `empty` — the wrong-world failure EC-527
|
|
1494
|
-
* Keyed on the mode (`canvasCacheKey`, `cache.ts`), a
|
|
1495
|
-
* miss per URL, never a stale hit, in either direction.
|
|
1476
|
+
* **The mode is also part of the route's cache-entry key** (EC-543; since EC-515
|
|
1477
|
+
* an argument of the cached function). An entry can outlive the world that wrote
|
|
1478
|
+
* it and a hit never runs the loader, so until 0.0.85 the "clean 404" above held
|
|
1479
|
+
* only on a COLD cache: measured, a `next start` with both env vars blanked and a
|
|
1480
|
+
* warm `.next/cache` from a live run answered `200` with the live world's bytes
|
|
1481
|
+
* on every cached URL while reporting `empty` — the wrong-world failure EC-527
|
|
1482
|
+
* removed, one layer down. Keyed on the mode (`canvasCacheKey`, `cache.ts`), a
|
|
1483
|
+
* world change is a cold miss per URL, never a stale hit, in either direction.
|
|
1496
1484
|
*/
|
|
1497
1485
|
type ElytraContentMode = 'live' | 'fallback' | 'empty';
|
|
1498
1486
|
/** The delivery host handle {@link createElytraHost} yields. */
|
|
1499
|
-
interface ElytraHost {
|
|
1487
|
+
interface ElytraHost<Collections extends readonly CollectionDef[] = readonly CollectionDef[]> {
|
|
1500
1488
|
/**
|
|
1501
|
-
* The
|
|
1502
|
-
*
|
|
1489
|
+
* The config's collections, as passed (EC-581) — typed by their literals when the
|
|
1490
|
+
* project declares them with `defineCollection`, so `InferDocument<typeof host, 'post'>`
|
|
1491
|
+
* types a delivered post with no generation step.
|
|
1492
|
+
*/
|
|
1493
|
+
readonly collections: Collections;
|
|
1494
|
+
/**
|
|
1495
|
+
* The cache-tag scope: `ELYTRA_PROJECT_ID` in live mode, else the config slug.
|
|
1496
|
+
* The ONE scope of this host (EC-579): its cache entries are written under it and
|
|
1497
|
+
* its revalidate receiver derives every invalidation tag with it.
|
|
1503
1498
|
*/
|
|
1504
1499
|
contentScope: string;
|
|
1505
1500
|
/**
|
|
@@ -1513,7 +1508,7 @@ interface ElytraHost {
|
|
|
1513
1508
|
env: ElytraHostEnv;
|
|
1514
1509
|
/** Materialized route records (a derive is run once, at factory time). */
|
|
1515
1510
|
routes: RouteRecord[];
|
|
1516
|
-
/** The registered component surface
|
|
1511
|
+
/** The registered component surface — exactly the `components` option's list (EC-591). */
|
|
1517
1512
|
components: HostComponents;
|
|
1518
1513
|
/** The public base URL of the delivery site, when configured. */
|
|
1519
1514
|
frontendUrl?: string;
|
|
@@ -1521,22 +1516,33 @@ interface ElytraHost {
|
|
|
1521
1516
|
* The content loader (the seam every kit surface reads through): live
|
|
1522
1517
|
* delivery snapshot when env is configured — published, or the token-gated
|
|
1523
1518
|
* draft for draft-mode requests — else the fallback source.
|
|
1519
|
+
*
|
|
1520
|
+
* An UNCACHED read (EC-515): only the canvas route's entry caches. Under
|
|
1521
|
+
* `cacheComponents`, live-mode I/O in a layout or page outside `<Suspense>` fails
|
|
1522
|
+
* `next build` — call it inside a `<Suspense>`'d component (route handlers and
|
|
1523
|
+
* `sitemap.ts` just render dynamically).
|
|
1524
1524
|
*/
|
|
1525
1525
|
loadContent(request: CanvasContentRequest): Promise<ContentSource>;
|
|
1526
1526
|
/**
|
|
1527
1527
|
* Load + resolve the site-chrome globals (settings singleton, navigation by
|
|
1528
1528
|
* slot alias, link/asset resolution — see `host-globals.ts`). One content
|
|
1529
1529
|
* load per call; published perspective by default.
|
|
1530
|
+
*
|
|
1531
|
+
* An UNCACHED read, like {@link loadContent}: under `cacheComponents`, render the
|
|
1532
|
+
* chrome that awaits it inside `<Suspense>` — and keep the canvas page OUTSIDE
|
|
1533
|
+
* every boundary, or its 404s and redirects answer HTTP 200 (measured, EC-515).
|
|
1530
1534
|
*/
|
|
1531
1535
|
loadGlobals(options?: LoadGlobalsOptions): Promise<SiteGlobals>;
|
|
1532
1536
|
/** The catch-all canvas route pair for `app/[[...slug]]/page.tsx`. */
|
|
1533
1537
|
canvasRoute(options?: HostCanvasRouteOptions): CanvasRoute;
|
|
1534
1538
|
/**
|
|
1535
|
-
* The instant-publish webhook
|
|
1536
|
-
*
|
|
1537
|
-
*
|
|
1539
|
+
* The instant-publish webhook handler for `app/api/revalidate/route.ts`, wired to
|
|
1540
|
+
* `REVALIDATE_SECRET` and to this host's own project — {@link contentScope} and the
|
|
1541
|
+
* config locales — which it derives the tags of every event with (EC-579). Unset
|
|
1542
|
+
* secret = endpoint closed (401), never a default secret. Only the secret can be
|
|
1543
|
+
* overridden: the scope and locales are the ones this kit's cache entries carry.
|
|
1538
1544
|
*/
|
|
1539
|
-
revalidateRoute(overrides?: Partial<RevalidateRouteOptions
|
|
1545
|
+
revalidateRoute(overrides?: Partial<Pick<RevalidateRouteOptions, 'secret'>>): RevalidateRouteHandlers;
|
|
1540
1546
|
/**
|
|
1541
1547
|
* The draft-mode ENABLE route for e.g. `app/api/draft/route.ts` —
|
|
1542
1548
|
* `GET /api/draft?token=…&redirect=/path`, gated by the ONE
|
|
@@ -1549,12 +1555,13 @@ interface ElytraHost {
|
|
|
1549
1555
|
/**
|
|
1550
1556
|
* Does the page for this catch-all `slug` OPEN full-bleed? Manifest-driven
|
|
1551
1557
|
* (`presentation.fullBleed`, EC-368) and draft-mode aware — the host lays out
|
|
1552
|
-
* header clearance from data instead of a hand-kept component-id set.
|
|
1558
|
+
* header clearance from data instead of a hand-kept component-id set. An
|
|
1559
|
+
* uncached read (see {@link loadContent}).
|
|
1553
1560
|
*/
|
|
1554
1561
|
pageOpensFullBleed(slug: readonly string[] | undefined): Promise<boolean>;
|
|
1555
1562
|
}
|
|
1556
1563
|
/** Create the delivery host handle — see the module docs for the full wiring. */
|
|
1557
|
-
declare function createElytraHost(options: ElytraHostOptions): ElytraHost
|
|
1564
|
+
declare function createElytraHost<const Collections extends readonly CollectionDef[]>(options: ElytraHostOptions<Collections>): ElytraHost<Collections>;
|
|
1558
1565
|
|
|
1559
1566
|
/**
|
|
1560
1567
|
* The LIVE half of the delivery host kit (EC-368): fetch the delivery snapshot
|
|
@@ -1582,8 +1589,11 @@ interface LiveContentOptions {
|
|
|
1582
1589
|
/** Convex deployment URL, e.g. `https://your-deployment-123.convex.cloud`. */
|
|
1583
1590
|
convexUrl: string;
|
|
1584
1591
|
projectId: string;
|
|
1585
|
-
/**
|
|
1586
|
-
|
|
1592
|
+
/**
|
|
1593
|
+
* Project locale config — config-owned (AD-11), never read from the snapshot.
|
|
1594
|
+
* The declared shape; parsed where the source is assembled (EC-580).
|
|
1595
|
+
*/
|
|
1596
|
+
locales: LocaleConfigInput;
|
|
1587
1597
|
/** CMS collection schema — config-owned (AD-11); drives depth-1 relation population. */
|
|
1588
1598
|
collections: readonly CollectionDef[];
|
|
1589
1599
|
/** Route records — config-owned (AD-3/AD-11); merged with the home fallback. */
|
|
@@ -1708,4 +1718,4 @@ declare function evaluatePreviewRequest(requestUrl: string | URL, expectedToken:
|
|
|
1708
1718
|
*/
|
|
1709
1719
|
declare const PACKAGE = "@elytracms/next";
|
|
1710
1720
|
|
|
1711
|
-
export { type AssetImageLoaderOptions, type
|
|
1721
|
+
export { type AssetImageLoaderOptions, type BakedListing, CANVAS_VALIDATION_SLUG, type CanvasCacheOptions, type CanvasContentRequest, type CanvasPageOutcome, CanvasRenderer, type CanvasRendererProps, type CanvasRequestInfo, type CanvasRoute, type CanvasRouteOptions, type CanvasRouteProps, type CanvasSitemapOptions, type CanvasSitemapResult, type CanvasWrapContext, type ContentSnapshot, type ContentSnapshotOptions, type ContentSource, type CreateCanvasSitemapOptions, type DeliveryPresentation, ELYTRA_HOST_ENV_KEYS, type ElytraContentMode, type ElytraHost, type ElytraHostEnv, type ElytraHostImageOptions, type ElytraHostOptions, ElytraImage, type ElytraImageAsset, type ElytraImageProps, type GlobalsConventions, type HostCanvasRouteOptions, type HostComponent, type HostComponents, type LiveContentOptions, type LoadGlobalsOptions, type NextImageOptions, PACKAGE, type PreviewEvaluation, type PreviewRouteHandlers, type PreviewRouteOptions, type ResolveCanvasPageOptions, type ResolveGlobalsOptions, type ResolvedLink, type ResolvedNavEntry, type ResolvedNavLink, type RevalidateEvaluation, type RevalidateRouteHandlers, type RevalidateRouteOptions, type RevalidationProject, type SiteGlobals, type SitemapParamsProvider, type SitemapRouteContext, type SitemapSkippedRoute, type StaticContentData, 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 };
|