@elytracms/next 0.0.53 → 0.0.54

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
@@ -4,7 +4,7 @@ import { basePrimitives, getImplementation, renderComposition, ImageManifest, se
4
4
  import { isValidElement } from 'react';
5
5
  import { jsx, Fragment } from 'react/jsx-runtime';
6
6
  import { cacheTags, isSectionSourceId, parseCmsSourceQueryConfig, listQueryOfCmsConfig, isDocumentHistory, resolvedAsset, createContentClient, createContentContext } from '@elytracms/core/content';
7
- import { documentKey, splitPath, hierarchyMount, composePath } from '@elytracms/core/cms-core';
7
+ import { documentKey, splitPath, hierarchyMount, composePath, documentSchema, redirectRecordSchema } from '@elytracms/core/cms-core';
8
8
  export { documentSchema, localeConfigSchema, redirectRecordSchema, routeRecordSchema } from '@elytracms/core/cms-core';
9
9
  import { revalidateTag, unstable_cache } from 'next/cache';
10
10
  import { draftMode } from 'next/headers';
@@ -252,7 +252,7 @@ function createStaticContentSource(data) {
252
252
  byCollection.set(doc.collection, list);
253
253
  }
254
254
  const assets = /* @__PURE__ */ new Map();
255
- for (const record of data.assets ?? []) assets.set(record.id, record);
255
+ for (const record2 of data.assets ?? []) assets.set(record2.id, record2);
256
256
  const lookupFor = (_perspective) => ({
257
257
  collection: (id) => collections.get(id),
258
258
  document: (ref) => documents.get(documentKey(ref)),
@@ -279,7 +279,7 @@ function createStaticContentSource(data) {
279
279
  // `asset`-typed Image prop (an asset id) to a usable url + intrinsic
280
280
  // dimensions. Baked into the (serializable) render outcome by the route
281
281
  // helper, so a cached page keeps its images during a backend outage.
282
- assets: () => [...assets.values()].map((record) => resolvedAsset(record, record.url ?? assetUrl(record))),
282
+ assets: () => [...assets.values()].map((record2) => resolvedAsset(record2, record2.url ?? assetUrl(record2))),
283
283
  ...data.sources ? { sources: data.sources } : {}
284
284
  };
285
285
  }
@@ -289,26 +289,26 @@ async function createContentSnapshot(adapter, projectId, options = {}) {
289
289
  const versionRecords = await adapter.cms.listAllDocumentVersions(projectId);
290
290
  const assetRecords = await adapter.assets.listAssets(projectId);
291
291
  const redirectRecords = await adapter.redirects.listRedirects(projectId);
292
- const redirects = redirectRecords.filter((record) => record.enabled).map((record) => ({
293
- id: record.id,
294
- from: record.from,
295
- to: record.to,
296
- permanent: record.permanent
292
+ const redirects = redirectRecords.filter((record2) => record2.enabled).map((record2) => ({
293
+ id: record2.id,
294
+ from: record2.from,
295
+ to: record2.to,
296
+ permanent: record2.permanent
297
297
  }));
298
298
  const versionsByKey = /* @__PURE__ */ new Map();
299
- for (const record of versionRecords) {
300
- const key = `${record.collection}:${record.docId}`;
299
+ for (const record2 of versionRecords) {
300
+ const key = `${record2.collection}:${record2.docId}`;
301
301
  let byVersion = versionsByKey.get(key);
302
302
  if (!byVersion) {
303
303
  byVersion = /* @__PURE__ */ new Map();
304
304
  versionsByKey.set(key, byVersion);
305
305
  }
306
- byVersion.set(record.version, record.snapshot);
306
+ byVersion.set(record2.version, record2.snapshot);
307
307
  }
308
- const documents = documentRecords.map((record) => {
309
- if (record.publishedVersion === void 0) return record.document;
310
- const published = versionsByKey.get(`${record.document.collection}:${record.document.id}`)?.get(record.publishedVersion);
311
- return { draft: record.document, published, versions: [] };
308
+ const documents = documentRecords.map((record2) => {
309
+ if (record2.publishedVersion === void 0) return record2.document;
310
+ const published = versionsByKey.get(`${record2.document.collection}:${record2.document.id}`)?.get(record2.publishedVersion);
311
+ return { draft: record2.document, published, versions: [] };
312
312
  });
313
313
  return createStaticContentSource({
314
314
  projectId,
@@ -898,6 +898,258 @@ function nextImagePrimitive(options = {}) {
898
898
  implementation: createNextImagePrimitive(options)
899
899
  };
900
900
  }
901
+ function elytraHostImage(options = {}) {
902
+ return ({ asset, alt, className, priority, sizes }) => /* @__PURE__ */ jsx(
903
+ ElytraImage,
904
+ {
905
+ asset: { ...asset, alt },
906
+ ...className !== void 0 ? { className } : {},
907
+ ...priority ? { priority: true } : {},
908
+ ...sizes !== void 0 ? { sizes } : {},
909
+ ...options.loader !== void 0 ? { loader: options.loader } : {},
910
+ ...options.quality !== void 0 ? { quality: options.quality } : {}
911
+ }
912
+ );
913
+ }
914
+
915
+ // src/full-bleed.ts
916
+ function firstBlockFullBleed(document, registry) {
917
+ const first = compositionNodesOf(document)[0];
918
+ if (!first) return false;
919
+ return registry.getManifest(first.componentId)?.presentation?.fullBleed === true;
920
+ }
921
+
922
+ // src/host-env.ts
923
+ var ELYTRA_HOST_ENV_KEYS = {
924
+ convexUrl: "ELYTRA_CONVEX_URL",
925
+ projectId: "ELYTRA_PROJECT_ID",
926
+ draftToken: "DRAFT_PREVIEW_TOKEN",
927
+ revalidateSecret: "REVALIDATE_SECRET",
928
+ studioOrigin: "STUDIO_ORIGIN"
929
+ };
930
+ function cleaned(value) {
931
+ const trimmed = value?.trim();
932
+ return trimmed !== void 0 && trimmed.length > 0 ? trimmed : void 0;
933
+ }
934
+ function resolveHostEnv(env) {
935
+ const resolved = {};
936
+ for (const [key, name] of Object.entries(ELYTRA_HOST_ENV_KEYS)) {
937
+ const value = cleaned(env[name]);
938
+ if (value !== void 0) resolved[key] = value;
939
+ }
940
+ return resolved;
941
+ }
942
+ async function runConvexQuery(fetchImpl, convexUrl, path, args) {
943
+ const response = await fetchImpl(`${convexUrl.replace(/\/$/, "")}/api/query`, {
944
+ method: "POST",
945
+ headers: { "content-type": "application/json" },
946
+ body: JSON.stringify({ path, args, format: "json" }),
947
+ cache: "no-store"
948
+ });
949
+ const body = await response.json().catch(() => void 0);
950
+ if (!response.ok || body?.status !== "success") {
951
+ throw new Error(
952
+ `Convex query "${path}" failed (${response.status}): ${body?.errorMessage ?? body?.message ?? "no error message"}`
953
+ );
954
+ }
955
+ return body.value;
956
+ }
957
+ function parseDocuments(rows) {
958
+ const documents = [];
959
+ for (const row of rows) {
960
+ const parsed = documentSchema.safeParse(row);
961
+ if (parsed.success) documents.push(parsed.data);
962
+ }
963
+ return documents;
964
+ }
965
+ function parseRedirects(rows) {
966
+ const redirects = [];
967
+ for (const row of rows) {
968
+ const parsed = redirectRecordSchema.safeParse(row);
969
+ if (parsed.success) redirects.push(parsed.data);
970
+ }
971
+ return redirects;
972
+ }
973
+ var canvasDataSourceSchema = z.object({
974
+ kind: z.string().min(1),
975
+ id: z.string().min(1),
976
+ label: z.string().optional(),
977
+ config: z.unknown().optional()
978
+ });
979
+ function parseSources(rows) {
980
+ if (!Array.isArray(rows)) return [];
981
+ const sources = [];
982
+ for (const row of rows) {
983
+ const parsed = canvasDataSourceSchema.safeParse(row);
984
+ if (parsed.success) sources.push(parsed.data);
985
+ }
986
+ return sources;
987
+ }
988
+ function fallbackHomeRoutes(documents, locales) {
989
+ const homePage = [...documents].filter((doc) => doc.collection === "page").sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0)[0];
990
+ if (!homePage) return [];
991
+ return locales.locales.map((locale) => ({
992
+ id: `r-live-home-${locale}`,
993
+ pattern: "/",
994
+ locale,
995
+ document: { collection: "page", id: homePage.id }
996
+ }));
997
+ }
998
+ function assembleLiveSource(payload, options, perspective) {
999
+ const locales = options.locales;
1000
+ const documents = parseDocuments(payload.documents);
1001
+ const routes = mergeRouteRecords([...options.routes], fallbackHomeRoutes(documents, locales));
1002
+ const documentSources = perspective === "published" ? documents.map((doc) => ({ draft: doc, published: doc, versions: [] })) : documents;
1003
+ return createStaticContentSource({
1004
+ projectId: payload.projectId,
1005
+ locales,
1006
+ routes,
1007
+ // Redirects are editorial DATA (EC-303): the ENABLED ones ship in the
1008
+ // snapshot, so they change dynamically (no redeploy). Native next.config
1009
+ // redirects run before this.
1010
+ redirects: parseRedirects(payload.redirects ?? []),
1011
+ collections: [...options.collections],
1012
+ documents: documentSources,
1013
+ assets: payload.assets,
1014
+ // EC-166 LIVE mode: the builder's persisted section/template sources, so
1015
+ // the route helper materializes their payloads.
1016
+ sources: parseSources(payload.sources)
1017
+ });
1018
+ }
1019
+ async function loadLiveContent(options, perspective) {
1020
+ const fetchImpl = options.fetch ?? globalThis.fetch;
1021
+ const args = { projectId: options.projectId };
1022
+ let payload;
1023
+ if (perspective === "draft") {
1024
+ if (!options.draftToken) {
1025
+ throw new Error(
1026
+ "Draft preview requested but DRAFT_PREVIEW_TOKEN is not configured on the host."
1027
+ );
1028
+ }
1029
+ payload = await runConvexQuery(fetchImpl, options.convexUrl, "delivery:draftSnapshot", {
1030
+ ...args,
1031
+ token: options.draftToken
1032
+ });
1033
+ } else {
1034
+ payload = await runConvexQuery(
1035
+ fetchImpl,
1036
+ options.convexUrl,
1037
+ "delivery:snapshot",
1038
+ args
1039
+ );
1040
+ }
1041
+ return assembleLiveSource(payload, options, perspective);
1042
+ }
1043
+
1044
+ // src/host-globals.ts
1045
+ var DEFAULT_ITEMS_FIELDS = ["items", "links"];
1046
+ function record(value) {
1047
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
1048
+ }
1049
+ function str2(value) {
1050
+ return typeof value === "string" ? value : "";
1051
+ }
1052
+ function items(value) {
1053
+ return Array.isArray(value) ? value.map(record) : [];
1054
+ }
1055
+ function resolveGlobals(source, options = {}) {
1056
+ const perspective = options.perspective ?? "published";
1057
+ const conventions = options.conventions ?? {};
1058
+ const settingsCollection = conventions.settingsCollection ?? "settings";
1059
+ const navigationCollection = conventions.navigationCollection ?? "navigation";
1060
+ const itemsFields = conventions.navigationItemsFields ?? DEFAULT_ITEMS_FIELDS;
1061
+ const client = source.client({ perspective, ...options.locale ? { locale: options.locale } : {} });
1062
+ const assets = source.assets?.() ?? [];
1063
+ const assetById = new Map(assets.map((asset) => [asset.id, asset]));
1064
+ const listDocs = (collection) => {
1065
+ const result = client.listDocuments(collection);
1066
+ return result.ok ? result.documents : [];
1067
+ };
1068
+ const settings = listDocs(settingsCollection)[0] ?? null;
1069
+ const navDocs = listDocs(navigationCollection);
1070
+ const navigation = (slot) => navDocs.find((doc) => doc.alias === slot) ?? null;
1071
+ const assetUrl = (value) => {
1072
+ if (typeof value === "string") {
1073
+ const found = assetById.get(value);
1074
+ if (found) return found.url;
1075
+ return value.startsWith("http") || value.startsWith("/") ? value : void 0;
1076
+ }
1077
+ const url = record(value).url;
1078
+ return typeof url === "string" && url.length > 0 ? url : void 0;
1079
+ };
1080
+ const refOf = (value) => {
1081
+ const ref = record(value);
1082
+ return typeof ref.collection === "string" && typeof ref.id === "string" ? { collection: ref.collection, id: ref.id } : null;
1083
+ };
1084
+ const resolveLink = (value) => {
1085
+ const link = record(value);
1086
+ const openInNewTab = link.openInNewTab === true;
1087
+ const href = str2(link.href) || str2(link.url);
1088
+ const linkType = str2(link.linkType) || (href ? "href" : "");
1089
+ if (linkType === "href" || linkType === "external") {
1090
+ return href ? { href, openInNewTab } : null;
1091
+ }
1092
+ if (linkType === "file") {
1093
+ const url = assetUrl(link.file);
1094
+ return url ? { href: url, openInNewTab } : null;
1095
+ }
1096
+ if (linkType === "page") {
1097
+ const page = link.page;
1098
+ const ref = refOf(page);
1099
+ if (ref) {
1100
+ const path = client.resolveLink(ref);
1101
+ if (path !== null) return { href: path, openInNewTab };
1102
+ }
1103
+ const shaped = record(page);
1104
+ if (typeof shaped.path === "string" && shaped.path.startsWith("/")) {
1105
+ return { href: shaped.path, openInNewTab };
1106
+ }
1107
+ const slug = str2(shaped.slug);
1108
+ if (slug) return { href: `/${slug}`, openInNewTab };
1109
+ return null;
1110
+ }
1111
+ return null;
1112
+ };
1113
+ const toNavLink = (entry) => {
1114
+ const label = str2(entry.label);
1115
+ const resolved = resolveLink("link" in entry ? entry.link : entry);
1116
+ if (!label || !resolved) return null;
1117
+ const description = str2(entry.description);
1118
+ return { label, ...resolved, ...description ? { description } : {} };
1119
+ };
1120
+ const toNavEntry = (entry) => {
1121
+ const label = str2(entry.label);
1122
+ if (!label) return null;
1123
+ const resolved = resolveLink("link" in entry ? entry.link : entry);
1124
+ const description = str2(entry.description);
1125
+ const children = items(entry.children).map(toNavLink).filter((child) => child !== null);
1126
+ if (!resolved && children.length === 0) return null;
1127
+ return {
1128
+ label,
1129
+ ...resolved ? { href: resolved.href } : {},
1130
+ openInNewTab: resolved?.openInNewTab ?? false,
1131
+ ...description ? { description } : {},
1132
+ children
1133
+ };
1134
+ };
1135
+ const navigationEntries = (slot) => {
1136
+ const doc = navigation(slot);
1137
+ if (!doc) return [];
1138
+ const values = doc;
1139
+ const field = itemsFields.find((name) => Array.isArray(values[name]));
1140
+ if (!field) return [];
1141
+ return items(values[field]).map(toNavEntry).filter((entry) => entry !== null);
1142
+ };
1143
+ return {
1144
+ settings,
1145
+ navigation,
1146
+ navigationEntries,
1147
+ resolveLink,
1148
+ assetUrl,
1149
+ client,
1150
+ source
1151
+ };
1152
+ }
901
1153
 
902
1154
  // src/preview-core.ts
903
1155
  function safeRedirectTarget(value, fallback = "/") {
@@ -925,6 +1177,8 @@ function evaluatePreviewRequest(requestUrl, expectedToken) {
925
1177
  }
926
1178
  return { ok: true, redirectTo };
927
1179
  }
1180
+
1181
+ // src/preview.ts
928
1182
  function createPreviewRoute(options) {
929
1183
  return {
930
1184
  async GET(request) {
@@ -953,8 +1207,105 @@ function createPreviewDisableRoute() {
953
1207
  }
954
1208
  };
955
1209
  }
1210
+
1211
+ // src/host-kit.ts
1212
+ function materializeRoutes(routes, collections) {
1213
+ return typeof routes === "function" ? routes([...collections]) : [...routes];
1214
+ }
1215
+ function createElytraHost(options) {
1216
+ const env = resolveHostEnv(options.env ?? process.env);
1217
+ const routes = materializeRoutes(options.routes, options.collections);
1218
+ const live = env.convexUrl !== void 0 && env.projectId !== void 0;
1219
+ const contentScope = live ? env.projectId : options.slug;
1220
+ const components = defineHostComponents([
1221
+ // Batteries included (EC-368 image defaults): graph `base.Image` nodes render
1222
+ // through next/image — resized variants via the host's own optimizer. The
1223
+ // canonical manifest stays (first registration wins in defineHostComponents).
1224
+ ...options.image === false ? [] : [nextImagePrimitive(options.image ?? {})],
1225
+ ...options.components ?? []
1226
+ ]);
1227
+ const emptySource = createStaticContentSource({
1228
+ projectId: contentScope,
1229
+ locales: options.locales,
1230
+ routes,
1231
+ // Redirects are editorial DATA (EC-303) — the no-backend build has none.
1232
+ redirects: [],
1233
+ collections: [...options.collections],
1234
+ documents: [],
1235
+ assets: [],
1236
+ sources: []
1237
+ });
1238
+ const loadContent = (request) => {
1239
+ if (live) {
1240
+ return loadLiveContent(
1241
+ {
1242
+ convexUrl: env.convexUrl,
1243
+ projectId: env.projectId,
1244
+ locales: options.locales,
1245
+ collections: options.collections,
1246
+ routes,
1247
+ ...env.draftToken ? { draftToken: env.draftToken } : {},
1248
+ ...options.fetch ? { fetch: options.fetch } : {}
1249
+ },
1250
+ request.perspective
1251
+ );
1252
+ }
1253
+ if (options.fallbackContent) return options.fallbackContent(request);
1254
+ return Promise.resolve(emptySource);
1255
+ };
1256
+ const loadGlobals = async (globalsOptions = {}) => {
1257
+ const perspective = globalsOptions.perspective ?? "published";
1258
+ const source = await loadContent({ perspective });
1259
+ return resolveGlobals(source, {
1260
+ perspective,
1261
+ ...options.globals ? { conventions: options.globals } : {}
1262
+ });
1263
+ };
1264
+ const canvasRoute = (routeOptions = {}) => {
1265
+ const { cache, ...rest } = routeOptions;
1266
+ return createCanvasRoute({
1267
+ loadContent,
1268
+ components,
1269
+ // Tag scope = the project id the studio publishes under, so its webhook
1270
+ // tags match this site's cached entries.
1271
+ ...cache === false ? {} : { cache: cache ?? { scope: contentScope } },
1272
+ ...rest
1273
+ });
1274
+ };
1275
+ const revalidateRoute = (overrides = {}) => createRevalidateRoute({
1276
+ // An unset secret keeps the endpoint CLOSED (401) — the kit never ships a
1277
+ // default secret (an auth-shaped no-op is worse than an explicit gap).
1278
+ secret: "secret" in overrides ? overrides.secret : env.revalidateSecret,
1279
+ ...overrides.allowOrigin !== void 0 ? { allowOrigin: overrides.allowOrigin } : env.studioOrigin !== void 0 ? { allowOrigin: env.studioOrigin } : {}
1280
+ });
1281
+ const previewRoute = () => createPreviewRoute({ token: env.draftToken });
1282
+ const previewDisableRoute = () => createPreviewDisableRoute();
1283
+ const pageOpensFullBleed = async (slug) => {
1284
+ const { isEnabled } = await draftMode();
1285
+ const perspective = isEnabled ? "draft" : "published";
1286
+ const source = await loadContent({ perspective });
1287
+ const { locale, path } = splitLocalePath(pathFromSlug(slug), source.locales);
1288
+ const client = source.client({ perspective, locale });
1289
+ const resolved = client.resolvePage(path);
1290
+ return firstBlockFullBleed(resolved.document, components.registry);
1291
+ };
1292
+ return {
1293
+ contentScope,
1294
+ env,
1295
+ routes,
1296
+ components,
1297
+ ...options.frontendUrl !== void 0 ? { frontendUrl: options.frontendUrl } : {},
1298
+ loadContent,
1299
+ loadGlobals,
1300
+ canvasRoute,
1301
+ revalidateRoute,
1302
+ previewRoute,
1303
+ previewDisableRoute,
1304
+ pageOpensFullBleed
1305
+ };
1306
+ }
956
1307
  var PACKAGE = "@elytracms/next";
957
1308
 
958
- export { CanvasRenderer, ElytraImage, PACKAGE, REVALIDATE_SIGNATURE_HEADER, canvasPageMetadata, canvasSitemapEntries, collectBindingSourceIds, collectionCacheTag, createAssetImageLoader, createBindingResolver, createCanvasRoute, createCanvasSitemap, createContentSnapshot, createPreviewDisableRoute, createPreviewRoute, createRevalidateRoute, createStaticContentSource, defaultBindingPayloads, defineHostComponents, docCacheTag, evaluatePreviewRequest, evaluateRevalidateRequest, isSourcePayloadError, loadCachedEntry, materializeSourcePayloads, mergeCacheTags, mergeRouteRecords, nextCacheTags, nextImagePrimitive, pathFromSlug, projectSweepTag, resolveCanvasPage, resolvePayloadToken, revalidatePayloadSchema, routeCacheTag, safeRedirectTarget, scopeCacheTag, signRevalidateBody, sourcePayloadError, splitLocalePath };
1309
+ export { CanvasRenderer, ELYTRA_HOST_ENV_KEYS, ElytraImage, PACKAGE, REVALIDATE_SIGNATURE_HEADER, assembleLiveSource, canvasPageMetadata, canvasSitemapEntries, collectBindingSourceIds, collectionCacheTag, createAssetImageLoader, createBindingResolver, createCanvasRoute, createCanvasSitemap, createContentSnapshot, createElytraHost, createPreviewDisableRoute, createPreviewRoute, createRevalidateRoute, createStaticContentSource, defaultBindingPayloads, defineHostComponents, docCacheTag, elytraHostImage, evaluatePreviewRequest, evaluateRevalidateRequest, fallbackHomeRoutes, firstBlockFullBleed, isSourcePayloadError, loadCachedEntry, loadLiveContent, materializeSourcePayloads, mergeCacheTags, mergeRouteRecords, nextCacheTags, nextImagePrimitive, pathFromSlug, projectSweepTag, resolveCanvasPage, resolveGlobals, resolveHostEnv, resolvePayloadToken, revalidatePayloadSchema, routeCacheTag, safeRedirectTarget, scopeCacheTag, signRevalidateBody, sourcePayloadError, splitLocalePath };
959
1310
  //# sourceMappingURL=index.js.map
960
1311
  //# sourceMappingURL=index.js.map