@elytracms/next 0.0.84 → 0.0.86
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 +69 -14
- package/dist/index.js +57 -10
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -3,10 +3,11 @@ import { ComponentRegistry, ComponentIssue, ComponentManifest } from '@elytracms
|
|
|
3
3
|
export { ComponentManifest, PropField, SlotSpec, defineComponent } from '@elytracms/core/component-registry';
|
|
4
4
|
import { ComponentImplementations, ResolveBinding, RelationTarget, DocumentBodyItem } from '@elytracms/core/runtime-renderer';
|
|
5
5
|
export { ComponentImplementations, RenderAsset } from '@elytracms/core/runtime-renderer';
|
|
6
|
+
import { HostComponentEntry } from '@elytracms/core/studio-core';
|
|
6
7
|
import { ResolvePageResult, ResolvedAsset, Perspective, ContentClient, CacheTags, ContentDocumentSource, ResolvedDocument } from '@elytracms/core/content';
|
|
7
8
|
export { Perspective } from '@elytracms/core/content';
|
|
8
9
|
import { LocaleConfig, Locale, RouteRecord, CollectionDef, RedirectRecord } from '@elytracms/core/cms-core';
|
|
9
|
-
export { CmsDocument, CollectionDef, FieldDef, Locale, LocaleConfig, LocaleUrlPath, RedirectRecord, RouteRecord, documentSchema, fallbackHomeRoutes, localeConfigSchema, localeUrlPath, mergeRouteRecords, redirectRecordSchema, routeRecordSchema, splitLocalePath } 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';
|
|
10
11
|
import { ComponentNode } from '@elytracms/core/project-graph';
|
|
11
12
|
export { ComponentNode } from '@elytracms/core/project-graph';
|
|
12
13
|
import { AssetRecord, PersistenceAdapter } from '@elytracms/core/persistence';
|
|
@@ -16,12 +17,18 @@ import { z } from 'zod';
|
|
|
16
17
|
export { z } from 'zod';
|
|
17
18
|
import { ImageLoader } from 'next/image';
|
|
18
19
|
import { HostImageRenderer } from './host-image.js';
|
|
19
|
-
import { HostComponentEntry } from '@elytracms/core/studio-core';
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* One host-repo component: the manifest the builder edits against plus the
|
|
23
23
|
* real React implementation that renders it (vision AD-2 — project components
|
|
24
24
|
* are real code in the host repo, never generated).
|
|
25
|
+
*
|
|
26
|
+
* This is the STRICT shape — the one a hand-authored component module should
|
|
27
|
+
* export (`export const heroComponent: HostComponent = …`), so the compiler
|
|
28
|
+
* checks the implementation where it is written. It is assignable to the
|
|
29
|
+
* config's {@link HostComponentEntry}, which is what {@link defineHostComponents}
|
|
30
|
+
* accepts, so both hand-built lists and the config's own list register through
|
|
31
|
+
* the one signature (EC-541).
|
|
25
32
|
*/
|
|
26
33
|
interface HostComponent {
|
|
27
34
|
manifest: ComponentManifest;
|
|
@@ -36,7 +43,11 @@ interface HostComponent {
|
|
|
36
43
|
interface HostComponents {
|
|
37
44
|
registry: ComponentRegistry;
|
|
38
45
|
implementations: ComponentImplementations;
|
|
39
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Structural registration issues: the registry's own (duplicate ids, invalid
|
|
48
|
+
* namespaces, …) plus `invalid-implementation` for an entry whose
|
|
49
|
+
* `implementation` is not a React component (EC-541).
|
|
50
|
+
*/
|
|
40
51
|
issues: readonly ComponentIssue[];
|
|
41
52
|
}
|
|
42
53
|
interface DefineHostComponentsOptions {
|
|
@@ -53,8 +64,29 @@ interface DefineHostComponentsOptions {
|
|
|
53
64
|
* default. A host implementation registered under a primitive id replaces the
|
|
54
65
|
* default implementation (the manifest of the first registration wins, which
|
|
55
66
|
* is the primitive's — by design, so prop schemas stay canonical).
|
|
56
|
-
|
|
57
|
-
|
|
67
|
+
*
|
|
68
|
+
* Accepts the CONFIG's entry shape (`implementation: unknown`) rather than the
|
|
69
|
+
* strict {@link HostComponent}: the config declares implementations opaquely,
|
|
70
|
+
* and EC-539 made `createElytraHost({ ...site })` the contract, so a
|
|
71
|
+
* compile-time check at this seam was already gone for the recommended path.
|
|
72
|
+
* The check is RUNTIME instead (EC-541): every implementation is tested with
|
|
73
|
+
* {@link isComponentImplementation}; a value that is not a component is
|
|
74
|
+
* recorded as an `invalid-implementation` issue naming the component id and
|
|
75
|
+
* the received kind, and is NOT put into `implementations` — so the renderer
|
|
76
|
+
* takes its missing-component path (a visible EC-015 fallback) instead of
|
|
77
|
+
* crashing the server render on `createElement` ("Element type is invalid")
|
|
78
|
+
* or, for a string, silently rendering an HTML tag. The manifest still
|
|
79
|
+
* registers, so the registry's own issues (duplicate id, invalid id) for that
|
|
80
|
+
* entry are reported alongside.
|
|
81
|
+
*
|
|
82
|
+
* One consequence of the `unknown` parameter: an INLINE
|
|
83
|
+
* `implementation: (props) => …` literal no longer gets `props` contextually
|
|
84
|
+
* typed (it is `noImplicitAny` under strict). Annotate the parameter, or
|
|
85
|
+
* author the entry as a {@link HostComponent} — which is the documented shape
|
|
86
|
+
* anyway. (An overload pair was tried and does not restore it: TypeScript
|
|
87
|
+
* resolves the context-sensitive literal against the wider overload.)
|
|
88
|
+
*/
|
|
89
|
+
declare function defineHostComponents(components?: readonly HostComponentEntry[], options?: DefineHostComponentsOptions): HostComponents;
|
|
58
90
|
|
|
59
91
|
/**
|
|
60
92
|
* Binding payloads for the embedded runtime (EC-144): one payload value per
|
|
@@ -598,6 +630,13 @@ interface CanvasCacheOptions {
|
|
|
598
630
|
* Omit only when the emitter also emits unscoped tags (see `cache.ts`).
|
|
599
631
|
*/
|
|
600
632
|
scope?: string;
|
|
633
|
+
/**
|
|
634
|
+
* Which content world `loadContent` reads — the delivery kit passes its
|
|
635
|
+
* `contentMode` (`'live' | 'fallback' | 'empty'`), so entries written in one
|
|
636
|
+
* world can never be served in another (EC-543, see `canvasCacheKey`). A host
|
|
637
|
+
* driving this helper directly with a single world may omit it.
|
|
638
|
+
*/
|
|
639
|
+
mode?: string;
|
|
601
640
|
}
|
|
602
641
|
/**
|
|
603
642
|
* What the route helper tells the content loader about the request it is
|
|
@@ -1503,13 +1542,16 @@ interface ElytraHostOptions {
|
|
|
1503
1542
|
* / `WorkspaceProjectEntry` annotation did not. Both production consumers hit
|
|
1504
1543
|
* exactly that and each carried one hand-restated `components` line (EC-539).
|
|
1505
1544
|
*
|
|
1506
|
-
* What
|
|
1507
|
-
*
|
|
1508
|
-
* component
|
|
1509
|
-
*
|
|
1510
|
-
*
|
|
1511
|
-
*
|
|
1512
|
-
*
|
|
1545
|
+
* What the widening costs, and where it is paid (EC-541): a list passed HERE
|
|
1546
|
+
* gets no compile-time check that each `implementation` is really a
|
|
1547
|
+
* component. The check is RUNTIME instead — `defineHostComponents` takes the
|
|
1548
|
+
* same `HostComponentEntry` shape (no cast, one signature) and tests every
|
|
1549
|
+
* implementation on registration; a value that is not a component becomes an
|
|
1550
|
+
* `invalid-implementation` issue in `components.issues` and is withheld from
|
|
1551
|
+
* the implementation map, so its nodes render the visible missing-component
|
|
1552
|
+
* fallback rather than crashing the server render. Authoring a component
|
|
1553
|
+
* module as the strict {@link HostComponent} keeps the compile-time check at
|
|
1554
|
+
* the place the implementation is written.
|
|
1513
1555
|
*/
|
|
1514
1556
|
components?: readonly HostComponentEntry[];
|
|
1515
1557
|
/**
|
|
@@ -1533,8 +1575,10 @@ interface ElytraHostOptions {
|
|
|
1533
1575
|
* The no-backend content fallback, used whenever `ELYTRA_CONVEX_URL` +
|
|
1534
1576
|
* `ELYTRA_PROJECT_ID` are not both set. Default: a build-safe EMPTY source —
|
|
1535
1577
|
* consumer CI builds green with no backend, and every URL is a clean 404
|
|
1536
|
-
* until content exists
|
|
1537
|
-
*
|
|
1578
|
+
* until content exists — in every cache state, not only a cold one, because
|
|
1579
|
+
* the content mode is part of the route's data-cache key (see
|
|
1580
|
+
* {@link ElytraContentMode}). The starter template overrides this with its
|
|
1581
|
+
* seeded in-memory world.
|
|
1538
1582
|
*/
|
|
1539
1583
|
fallbackContent?: (request: CanvasContentRequest) => Promise<ContentSource>;
|
|
1540
1584
|
/** Chrome-globals conventions (settings/navigation collection ids, items fields). */
|
|
@@ -1560,6 +1604,8 @@ interface HostCanvasRouteOptions {
|
|
|
1560
1604
|
* Tagged data cache (instant publish). Default: enabled, scoped to the
|
|
1561
1605
|
* kit's content scope so the studio's webhook tags match. `false` disables
|
|
1562
1606
|
* caching entirely; an explicit `CanvasCacheOptions` overrides the scope.
|
|
1607
|
+
* The kit's {@link ElytraHost.contentMode} is always part of the entry key
|
|
1608
|
+
* (EC-543) — an override can replace the scope, never drop the mode.
|
|
1563
1609
|
*/
|
|
1564
1610
|
cache?: CanvasCacheOptions | false;
|
|
1565
1611
|
}
|
|
@@ -1584,6 +1630,15 @@ interface LoadGlobalsOptions {
|
|
|
1584
1630
|
* §5 gets no signal that the thing under test was never exercised. Declared
|
|
1585
1631
|
* once, read here, so a host can *say* which world it is in instead of
|
|
1586
1632
|
* re-deriving it from `env` and drifting.
|
|
1633
|
+
*
|
|
1634
|
+
* **The mode is also part of the route's data-cache key** (EC-543). A cache
|
|
1635
|
+
* entry outlives the process that wrote it and a hit never runs the loader, so
|
|
1636
|
+
* until 0.0.85 the "clean 404" above held only on a COLD cache: measured, a
|
|
1637
|
+
* `next start` with both env vars blanked and a warm `.next/cache` from a live
|
|
1638
|
+
* run answered `200` with the live world's bytes on every cached URL while
|
|
1639
|
+
* reporting `empty` — the wrong-world failure EC-527 removed, one layer down.
|
|
1640
|
+
* Keyed on the mode (`canvasCacheKey`, `cache.ts`), a world change is a cold
|
|
1641
|
+
* miss per URL, never a stale hit, in either direction.
|
|
1587
1642
|
*/
|
|
1588
1643
|
type ElytraContentMode = 'live' | 'fallback' | 'empty';
|
|
1589
1644
|
/** The delivery host handle {@link createElytraHost} yields. */
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { isBlockSequenceValueShape } from '@elytracms/core/rich-text';
|
|
|
6
6
|
import { jsx, Fragment } from 'react/jsx-runtime';
|
|
7
7
|
import { cacheTags, isSectionSourceId, parseCmsSourceQueryConfig, listQueryOfCmsConfig, 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';
|
|
@@ -15,6 +15,34 @@ import { z } from 'zod';
|
|
|
15
15
|
export { z } from 'zod';
|
|
16
16
|
import NextImage from 'next/image';
|
|
17
17
|
|
|
18
|
+
// src/components.ts
|
|
19
|
+
|
|
20
|
+
// src/is-component.ts
|
|
21
|
+
var EXOTIC_COMPONENT_TYPES = /* @__PURE__ */ new Set([
|
|
22
|
+
/* @__PURE__ */ Symbol.for("react.memo"),
|
|
23
|
+
/* @__PURE__ */ Symbol.for("react.forward_ref"),
|
|
24
|
+
/* @__PURE__ */ Symbol.for("react.lazy")
|
|
25
|
+
]);
|
|
26
|
+
function isComponentImplementation(value) {
|
|
27
|
+
if (typeof value === "function") return true;
|
|
28
|
+
if (typeof value !== "object" || value === null) return false;
|
|
29
|
+
const marker = value.$$typeof;
|
|
30
|
+
return typeof marker === "symbol" && EXOTIC_COMPONENT_TYPES.has(marker);
|
|
31
|
+
}
|
|
32
|
+
function describeImplementation(value) {
|
|
33
|
+
if (value === null) return "null";
|
|
34
|
+
if (value === void 0) return "undefined";
|
|
35
|
+
if (typeof value === "string") {
|
|
36
|
+
return `the string ${JSON.stringify(value)} (which React would render silently as an HTML tag)`;
|
|
37
|
+
}
|
|
38
|
+
if (Array.isArray(value)) return "an array";
|
|
39
|
+
if (typeof value === "object") {
|
|
40
|
+
const marker = value.$$typeof;
|
|
41
|
+
return typeof marker === "symbol" ? `an object tagged ${String(marker)}` : "a plain object";
|
|
42
|
+
}
|
|
43
|
+
return `a ${typeof value}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
18
46
|
// src/components.ts
|
|
19
47
|
function defineHostComponents(components = [], options = {}) {
|
|
20
48
|
const includeBase = options.includeBasePrimitives !== false;
|
|
@@ -25,10 +53,20 @@ function defineHostComponents(components = [], options = {}) {
|
|
|
25
53
|
const implementation = getImplementation(base.implementations, manifest.id);
|
|
26
54
|
if (implementation) implementations[manifest.id] = implementation;
|
|
27
55
|
}
|
|
56
|
+
const implementationIssues = [];
|
|
28
57
|
for (const component of components) {
|
|
29
|
-
|
|
58
|
+
const id = component.manifest.id;
|
|
59
|
+
if (isComponentImplementation(component.implementation)) {
|
|
60
|
+
implementations[id] = component.implementation;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
implementationIssues.push({
|
|
64
|
+
code: "invalid-implementation",
|
|
65
|
+
componentId: id,
|
|
66
|
+
message: `Component "${id}": implementation is not a React component \u2014 received ${describeImplementation(component.implementation)}; expected a function or class component, or a React.memo / forwardRef / lazy wrapper. The entry is registered without an implementation, so its nodes render the missing-component fallback.`
|
|
67
|
+
});
|
|
30
68
|
}
|
|
31
|
-
return { registry, implementations, issues: registry.issues };
|
|
69
|
+
return { registry, implementations, issues: [...registry.issues, ...implementationIssues] };
|
|
32
70
|
}
|
|
33
71
|
|
|
34
72
|
// src/payloads.ts
|
|
@@ -533,6 +571,9 @@ function nextCacheTags(tags, scope) {
|
|
|
533
571
|
for (const assetId of tags.assets) all.add(assetCacheTag(assetId, scope));
|
|
534
572
|
return [...all].sort();
|
|
535
573
|
}
|
|
574
|
+
function canvasCacheKey({ scope, mode, url }) {
|
|
575
|
+
return ["elytra-route", scope ?? "", mode ?? "", url];
|
|
576
|
+
}
|
|
536
577
|
var CacheDiscoveryBail = class extends Error {
|
|
537
578
|
constructor() {
|
|
538
579
|
super("elytra cache discovery bail (never user-visible)");
|
|
@@ -582,6 +623,9 @@ function resolveOgImage(value, assets) {
|
|
|
582
623
|
if (looksLikeUrl(value)) return value;
|
|
583
624
|
return assets.find((asset) => asset.id === value)?.url ?? null;
|
|
584
625
|
}
|
|
626
|
+
function openGraphLocale(locale) {
|
|
627
|
+
return locale.replace(/-/g, "_");
|
|
628
|
+
}
|
|
585
629
|
function canvasPageMetadata(result, assets = []) {
|
|
586
630
|
const doc = result.document;
|
|
587
631
|
const seoTitle = str(doc?.["seoTitle"]);
|
|
@@ -596,11 +640,12 @@ function canvasPageMetadata(result, assets = []) {
|
|
|
596
640
|
if (title !== null) metadata.title = title;
|
|
597
641
|
if (seoDescription) metadata.description = seoDescription;
|
|
598
642
|
const ogTitle = seoOgTitle ?? title;
|
|
599
|
-
if (
|
|
643
|
+
if (doc) {
|
|
600
644
|
metadata.openGraph = {
|
|
601
645
|
...ogTitle !== null ? { title: ogTitle } : {},
|
|
602
646
|
...seoDescription ? { description: seoDescription } : {},
|
|
603
|
-
...seoOgImage ? { images: [{ url: seoOgImage }] } : {}
|
|
647
|
+
...seoOgImage ? { images: [{ url: seoOgImage }] } : {},
|
|
648
|
+
locale: openGraphLocale(result.tags.locale)
|
|
604
649
|
};
|
|
605
650
|
}
|
|
606
651
|
if (seoNoindex) metadata.robots = { index: false };
|
|
@@ -630,7 +675,7 @@ function createCanvasRoute(options) {
|
|
|
630
675
|
const scope = options.cache.scope;
|
|
631
676
|
const entry = await loadCachedEntry(
|
|
632
677
|
nextCacheWrapper,
|
|
633
|
-
|
|
678
|
+
canvasCacheKey({ scope, mode: options.cache.mode, url }),
|
|
634
679
|
[projectSweepTag(scope)],
|
|
635
680
|
async () => {
|
|
636
681
|
const outcome = await resolveFresh();
|
|
@@ -1287,9 +1332,9 @@ function createElytraHost(options) {
|
|
|
1287
1332
|
// canonical manifest stays (first registration wins in defineHostComponents).
|
|
1288
1333
|
...options.image === false ? [] : [nextImagePrimitive(options.image ?? {})],
|
|
1289
1334
|
// EC-539: the option is the CONFIG's entry shape (`implementation: unknown`)
|
|
1290
|
-
// so the recommended `{ ...site }` spread compiles
|
|
1291
|
-
//
|
|
1292
|
-
//
|
|
1335
|
+
// so the recommended `{ ...site }` spread compiles; EC-541: registration
|
|
1336
|
+
// takes that shape too and checks each implementation at runtime (see the
|
|
1337
|
+
// option's doc comment) — no narrowing cast here.
|
|
1293
1338
|
...options.components ?? []
|
|
1294
1339
|
]);
|
|
1295
1340
|
const emptySource = createStaticContentSource({
|
|
@@ -1336,7 +1381,9 @@ function createElytraHost(options) {
|
|
|
1336
1381
|
components,
|
|
1337
1382
|
// Tag scope = the project id the studio publishes under, so its webhook
|
|
1338
1383
|
// tags match this site's cached entries.
|
|
1339
|
-
|
|
1384
|
+
// EC-543: the mode is in the entry key so no world ever reads another's
|
|
1385
|
+
// entries; an explicit override may replace the scope, not drop the mode.
|
|
1386
|
+
...cache === false ? {} : { cache: { mode: contentMode, ...cache ?? { scope: contentScope } } },
|
|
1340
1387
|
...rest
|
|
1341
1388
|
});
|
|
1342
1389
|
};
|