@gradial/aci 0.1.18 → 0.1.20-preview.1

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.
Files changed (82) hide show
  1. package/README.md +28 -8
  2. package/bin/aci +0 -0
  3. package/dist/astro/index.d.ts +4 -2
  4. package/dist/astro/index.js +9 -1
  5. package/dist/astro/preview.d.ts +5 -0
  6. package/dist/astro/preview.js +33 -0
  7. package/dist/content/index.d.ts +1 -0
  8. package/dist/content/index.js +1 -0
  9. package/dist/content/provider.d.ts +19 -0
  10. package/dist/content/provider.js +7 -0
  11. package/dist/content/resolve-slots.d.ts +9 -0
  12. package/dist/content/resolve-slots.js +24 -0
  13. package/dist/content/types.d.ts +1 -1
  14. package/dist/content/validation.js +0 -1
  15. package/dist/define-component.js +0 -6
  16. package/dist/define-data-model.d.ts +2 -0
  17. package/dist/define-data-model.js +3 -0
  18. package/dist/define-layout.d.ts +2 -0
  19. package/dist/define-layout.js +3 -0
  20. package/dist/dev/index.d.ts +12 -0
  21. package/dist/dev/index.js +84 -9
  22. package/dist/index.d.ts +2 -0
  23. package/dist/index.js +2 -0
  24. package/dist/next/asset-route.d.ts +2 -0
  25. package/dist/next/asset-route.js +10 -2
  26. package/dist/next/config.d.ts +1 -1
  27. package/dist/next/config.js +14 -2
  28. package/dist/next/content-watch.d.ts +11 -0
  29. package/dist/next/content-watch.js +106 -0
  30. package/dist/next/index.d.ts +2 -2
  31. package/dist/next/index.js +2 -2
  32. package/dist/next/middleware.d.ts +6 -2
  33. package/dist/next/middleware.js +41 -83
  34. package/dist/next/page.d.ts +41 -0
  35. package/dist/next/page.js +77 -0
  36. package/dist/next/preview-mode.js +4 -13
  37. package/dist/next/render.d.ts +5 -0
  38. package/dist/next/render.js +15 -0
  39. package/dist/next/root-layout.d.ts +4 -0
  40. package/dist/next/root-layout.js +5 -0
  41. package/dist/next/server.d.ts +3 -2
  42. package/dist/next/server.js +12 -8
  43. package/dist/preview/core.d.ts +41 -0
  44. package/dist/preview/core.js +116 -0
  45. package/dist/providers/file.d.ts +2 -0
  46. package/dist/providers/file.js +61 -1
  47. package/dist/providers/s3.d.ts +1 -0
  48. package/dist/providers/s3.js +14 -1
  49. package/dist/registry.d.ts +15 -0
  50. package/dist/registry.js +10 -0
  51. package/dist/sveltekit/index.d.ts +4 -1
  52. package/dist/sveltekit/index.js +9 -1
  53. package/dist/sveltekit/preview.d.ts +5 -0
  54. package/dist/sveltekit/preview.js +33 -0
  55. package/dist/testing/index.d.ts +3 -0
  56. package/dist/testing/index.js +15 -2
  57. package/dist/types/component.d.ts +0 -9
  58. package/dist/types/config.d.ts +1 -1
  59. package/dist/types/data.d.ts +19 -0
  60. package/dist/types/data.js +11 -0
  61. package/dist/types/image.d.ts +1 -1
  62. package/dist/types/image.js +1 -1
  63. package/dist/types/index.d.ts +1 -0
  64. package/dist/types/index.js +1 -0
  65. package/dist/types/page.d.ts +1 -2
  66. package/dist/types/render-mode.d.ts +0 -1
  67. package/package.json +40 -6
  68. package/src/cli/compile-registry.mjs +167 -19
  69. package/src/types/component.ts +0 -10
  70. package/src/types/config.ts +1 -1
  71. package/src/types/data.ts +47 -0
  72. package/src/types/image.ts +2 -2
  73. package/src/types/index.ts +1 -0
  74. package/src/types/page.ts +1 -2
  75. package/src/types/render-mode.ts +0 -8
  76. package/dist/next/preview-banner.d.ts +0 -4
  77. package/dist/next/preview-banner.js +0 -51
  78. package/dist/next/preview.d.ts +0 -7
  79. package/dist/next/preview.js +0 -51
  80. package/dist/render.d.ts +0 -14
  81. package/dist/render.js +0 -33
  82. package/src/cli/verify-renderer.mjs +0 -73
@@ -0,0 +1,106 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import http from 'node:http';
4
+ import path from 'node:path';
5
+ import { WebSocketServer } from 'ws';
6
+ import { DEFAULT_DEV_REFRESH_PATH, DEFAULT_DEV_REFRESH_PORT, } from '../dev/browser.js';
7
+ function sourceContentRoot() {
8
+ return path.resolve(process.env.ACI_CONTENT_SOURCE || './.content');
9
+ }
10
+ function compiledContentRoot() {
11
+ return path.resolve(process.env.ACI_CONTENT_ROOT || '.aci/compiled');
12
+ }
13
+ function resolveAciBin() {
14
+ return process.env.ACI_BIN || 'aci';
15
+ }
16
+ function recompileContent(reason) {
17
+ const siteDir = process.cwd();
18
+ const src = sourceContentRoot();
19
+ const out = compiledContentRoot();
20
+ const bin = resolveAciBin();
21
+ console.log(`[aci] compiling content (${reason})`);
22
+ const result = spawnSync(bin, [
23
+ '-s', siteDir,
24
+ 'build',
25
+ '--content', src,
26
+ '--out', out,
27
+ '--skip-code',
28
+ ], {
29
+ cwd: siteDir,
30
+ stdio: 'inherit',
31
+ env: process.env,
32
+ });
33
+ if (result.status !== 0) {
34
+ console.error(`[aci] content compile failed with exit code ${result.status ?? 'unknown'}`);
35
+ return false;
36
+ }
37
+ return true;
38
+ }
39
+ let watcherStarted = false;
40
+ function startContentWatcher() {
41
+ if (watcherStarted)
42
+ return;
43
+ watcherStarted = true;
44
+ const enabled = process.env.ACI_DISABLE_CONTENT_WATCH !== '1';
45
+ if (!enabled)
46
+ return;
47
+ const src = sourceContentRoot();
48
+ const wsPort = Number(process.env.ACI_DEV_WS_PORT || process.env.NEXT_PUBLIC_ACI_DEV_WS_PORT || DEFAULT_DEV_REFRESH_PORT);
49
+ const wsPath = process.env.ACI_DEV_WS_PATH || DEFAULT_DEV_REFRESH_PATH;
50
+ let timer;
51
+ let running = false;
52
+ let queued = false;
53
+ const server = http.createServer();
54
+ const wss = new WebSocketServer({ server, path: wsPath });
55
+ server.listen(wsPort, () => {
56
+ console.log(`[aci] dev refresh WebSocket on port ${wsPort}`);
57
+ });
58
+ function notifyClients() {
59
+ for (const client of wss.clients) {
60
+ if (client.readyState === 1) {
61
+ client.send(JSON.stringify({ type: 'content-updated' }));
62
+ }
63
+ }
64
+ }
65
+ function rebuild(reason) {
66
+ if (running) {
67
+ queued = true;
68
+ return;
69
+ }
70
+ running = true;
71
+ const success = recompileContent(reason);
72
+ running = false;
73
+ if (success) {
74
+ notifyClients();
75
+ }
76
+ if (queued) {
77
+ queued = false;
78
+ rebuild('queued change');
79
+ }
80
+ }
81
+ rebuild('startup');
82
+ try {
83
+ fs.watch(src, { recursive: true }, (_event, filename) => {
84
+ if (!filename)
85
+ return;
86
+ clearTimeout(timer);
87
+ timer = setTimeout(() => {
88
+ rebuild(path.relative(src, path.resolve(src, filename)));
89
+ }, 100);
90
+ });
91
+ }
92
+ catch {
93
+ console.warn('[aci] fs.watch not supported on this platform, content watch disabled');
94
+ }
95
+ }
96
+ export function createContentWatchWebpackPlugin() {
97
+ return {
98
+ apply(compiler) {
99
+ compiler.hooks['beforeCompile']?.tap('gradial-content-watch', () => {
100
+ if (process.env.NODE_ENV !== 'production') {
101
+ startContentWatcher();
102
+ }
103
+ });
104
+ },
105
+ };
106
+ }
@@ -1,4 +1,4 @@
1
1
  export { DevRefresh, type DevRefreshProps } from './dev-refresh.js';
2
- export { withGradial, type WithGradialOptions } from './config.js';
3
- export { PreviewBanner, type PreviewBannerProps } from './preview-banner.js';
2
+ export { withGradialAci, type WithGradialOptions } from './config.js';
4
3
  export { isPreviewMode } from './preview-mode.js';
4
+ export { createPreviewBannerHTML, createPreviewCookies, extractPreviewToken, resolvePreviewContext, PREVIEW_HEADER, RELEASE_HEADER, PREVIEW_TOKEN_COOKIE, PREVIEW_RELEASE_COOKIE, type PreviewContext, type PreviewClaims, type PreviewCookie, } from '../preview/core.js';
@@ -1,4 +1,4 @@
1
1
  export { DevRefresh } from './dev-refresh.js';
2
- export { withGradial } from './config.js';
3
- export { PreviewBanner } from './preview-banner.js';
2
+ export { withGradialAci } from './config.js';
4
3
  export { isPreviewMode } from './preview-mode.js';
4
+ export { createPreviewBannerHTML, createPreviewCookies, extractPreviewToken, resolvePreviewContext, PREVIEW_HEADER, RELEASE_HEADER, PREVIEW_TOKEN_COOKIE, PREVIEW_RELEASE_COOKIE, } from '../preview/core.js';
@@ -1,7 +1,11 @@
1
1
  import { NextResponse, type NextRequest } from 'next/server.js';
2
2
  export interface GradialMiddlewareConfig {
3
- siteId: string;
3
+ siteId?: string;
4
4
  edgeConfig?: string;
5
+ previewSignKey?: string;
5
6
  deploymentId?: string;
6
7
  }
7
- export declare function createGradialMiddleware(config: GradialMiddlewareConfig): (request: NextRequest) => Promise<NextResponse<unknown>>;
8
+ export declare function createGradialMiddleware(config?: GradialMiddlewareConfig): (request: NextRequest) => Promise<NextResponse<unknown>>;
9
+ export declare const config: {
10
+ matcher: string[];
11
+ };
@@ -1,92 +1,89 @@
1
1
  import { NextResponse } from 'next/server.js';
2
2
  import { getUncachedEdgeConfigValue } from './edge-config.js';
3
- import { validatePreviewToken } from './preview.js';
4
- const RELEASE_HEADER = 'x-gradial-release-id';
3
+ import { PREVIEW_TOKEN_COOKIE, PREVIEW_RELEASE_COOKIE, PREVIEW_HEADER, RELEASE_HEADER, extractPreviewToken, resolvePreviewContext, createPreviewCookies, previewSignKey, } from '../preview/core.js';
5
4
  const ROUTE_HEADER = 'x-gradial-route';
6
5
  const DISPATCH_HEADER = 'x-gradial-dispatch';
7
- export function createGradialMiddleware(config) {
6
+ export function createGradialMiddleware(config = {}) {
8
7
  return async function gradialMiddleware(request) {
9
8
  const url = request.nextUrl;
10
- console.log('[gradial/mw] === middleware start ===');
11
- console.log('[gradial/mw] path:', url.pathname, 'search:', url.search.slice(0, 100));
12
- console.log('[gradial/mw] siteId:', config.siteId);
13
- console.log('[gradial/mw] EDGE_CONFIG:', process.env.EDGE_CONFIG ? 'set' : 'NOT SET');
14
- console.log('[gradial/mw] GRADIAL_PREVIEW_SIGN_KEY:', process.env.GRADIAL_PREVIEW_SIGN_KEY ? `set (${process.env.GRADIAL_PREVIEW_SIGN_KEY.length} chars, prefix: ${process.env.GRADIAL_PREVIEW_SIGN_KEY.slice(0, 6)})` : 'NOT SET');
15
- console.log('[gradial/mw] VERCEL_DEPLOYMENT_ID:', process.env.VERCEL_DEPLOYMENT_ID || 'NOT SET');
16
9
  // ?leave_preview=1 → clear cookies, redirect to clean URL
17
10
  if (url.searchParams.get('leave_preview') === '1') {
18
- console.log('[gradial/mw] leave_preview=1, clearing cookies and redirecting');
19
11
  const cleanUrl = new URL(url.pathname, url.origin);
20
12
  const response = NextResponse.redirect(cleanUrl);
21
- response.cookies.delete('aci_preview');
22
- response.cookies.delete('aci_preview_release');
13
+ response.cookies.delete(PREVIEW_TOKEN_COOKIE);
14
+ response.cookies.delete(PREVIEW_RELEASE_COOKIE);
23
15
  return response;
24
16
  }
25
17
  const requestHeaders = gradialHeaders(request.headers);
26
18
  // Try preview token first; if valid, use that release.
27
19
  // If no token or invalid, fall through to active release from edge config.
28
- const previewReleaseId = await releaseIdFromPreviewToken(request, config);
29
- console.log('[gradial/mw] previewReleaseId from token:', previewReleaseId || '(none)');
30
- const releaseId = previewReleaseId || await activeReleaseFromEdgeConfig(config);
31
- console.log('[gradial/mw] final releaseId:', releaseId || '(none)');
20
+ const preview = await resolvePreviewFromRequest(request, config);
21
+ const releaseId = preview?.releaseId || await activeReleaseFromEdgeConfig(config);
32
22
  if (!releaseId) {
33
- console.log('[gradial/mw] no releaseId, passing through with no headers');
34
23
  return NextResponse.next({ request: { headers: requestHeaders } });
35
24
  }
36
25
  requestHeaders.set(RELEASE_HEADER, releaseId);
37
26
  requestHeaders.set(ROUTE_HEADER, url.pathname);
38
27
  requestHeaders.set(DISPATCH_HEADER, 'ssr-page');
39
- if (previewReleaseId) {
40
- requestHeaders.set('x-gradial-preview', '1');
41
- console.log('[gradial/mw] set x-gradial-preview=1 and x-gradial-release-id:', releaseId);
42
- }
43
- else {
44
- console.log('[gradial/mw] set x-gradial-release-id (active):', releaseId);
28
+ if (preview?.isPreview) {
29
+ requestHeaders.set(PREVIEW_HEADER, '1');
45
30
  }
46
31
  const response = NextResponse.next({ request: { headers: requestHeaders } });
47
32
  // Persist preview token as cookie so navigation stays in preview
48
- const queryToken = url.searchParams.get('preview_token');
49
- if (queryToken && previewReleaseId) {
50
- console.log('[gradial/mw] setting preview cookies for release:', previewReleaseId);
51
- response.cookies.set('aci_preview', queryToken, {
52
- httpOnly: true,
53
- sameSite: 'lax',
54
- maxAge: 60 * 60,
55
- path: '/',
56
- });
57
- response.cookies.set('aci_preview_release', previewReleaseId, {
58
- httpOnly: false,
59
- sameSite: 'lax',
60
- maxAge: 60 * 60,
61
- path: '/',
62
- });
33
+ if (preview?.token && preview.isPreview) {
34
+ for (const cookie of createPreviewCookies(preview.token, releaseId)) {
35
+ response.cookies.set(cookie.name, cookie.value, cookie.options);
36
+ }
63
37
  }
64
- console.log('[gradial/mw] === middleware end ===');
65
38
  return response;
66
39
  };
67
40
  }
41
+ export const config = {
42
+ matcher: [
43
+ '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
44
+ ],
45
+ };
68
46
  function gradialHeaders(headers) {
69
47
  const next = new Headers(headers);
70
48
  next.delete(RELEASE_HEADER);
71
49
  next.delete(ROUTE_HEADER);
72
50
  next.delete(DISPATCH_HEADER);
51
+ next.delete(PREVIEW_HEADER);
73
52
  return next;
74
53
  }
54
+ async function resolvePreviewFromRequest(request, config) {
55
+ const token = extractPreviewToken({
56
+ query: request.nextUrl.searchParams,
57
+ cookies: cookieMap(request.cookies),
58
+ authorization: request.headers.get('authorization') || undefined,
59
+ });
60
+ if (!token)
61
+ return null;
62
+ const signKey = previewSignKey();
63
+ if (!signKey) {
64
+ console.warn('[gradial/mw] GRADIAL_PREVIEW_SIGN_KEY not set — preview tokens will be ignored');
65
+ return null;
66
+ }
67
+ return await resolvePreviewContext({ token, siteId: config.siteId || '', signKey });
68
+ }
69
+ function cookieMap(cookies) {
70
+ const map = new Map();
71
+ for (const cookie of cookies.getAll()) {
72
+ map.set(cookie.name, cookie.value);
73
+ }
74
+ return map;
75
+ }
75
76
  async function activeReleaseFromEdgeConfig(config) {
76
77
  const edgeConfig = config.edgeConfig || process.env.EDGE_CONFIG || '';
77
- if (!edgeConfig || !config.siteId) {
78
- console.log('[gradial/mw] activeRelease: no edgeConfig or siteId, skipping');
78
+ if (!edgeConfig || !config.siteId)
79
79
  return '';
80
- }
81
80
  const active = await activeReleasePointer(edgeConfig, config.siteId);
82
- console.log('[gradial/mw] activeRelease pointer:', JSON.stringify(active));
83
81
  if (!active.releaseId)
84
82
  return '';
85
83
  const deploymentId = config.deploymentId || process.env.VERCEL_DEPLOYMENT_ID || '';
86
84
  if (!deploymentId)
87
85
  return active.releaseId;
88
86
  const compatibility = await getEdgeConfigJSON(edgeConfig, deploymentCompatibilityKey(config.siteId, deploymentId));
89
- console.log('[gradial/mw] compatibility:', compatibility ? JSON.stringify(compatibility) : '(none)');
90
87
  if (!compatibility)
91
88
  return active.releaseId;
92
89
  return clampRelease(active.releaseId, active.sequence || 0, compatibility);
@@ -146,42 +143,3 @@ function clampRelease(activeReleaseId, activeSequence, compatibility) {
146
143
  function edgeKeySegment(value) {
147
144
  return value.replace(/[^A-Za-z0-9_-]/g, '_');
148
145
  }
149
- async function releaseIdFromPreviewToken(request, config) {
150
- const token = previewTokenFromRequest(request);
151
- console.log('[gradial/mw] previewToken: token found:', !!token, 'len:', token.length);
152
- if (!token)
153
- return '';
154
- const signKey = process.env.GRADIAL_PREVIEW_SIGN_KEY || '';
155
- if (!signKey) {
156
- console.warn('[gradial/mw] GRADIAL_PREVIEW_SIGN_KEY not set — preview tokens will be ignored');
157
- return '';
158
- }
159
- console.log('[gradial/mw] signKey present, len:', signKey.length, 'prefix:', signKey.slice(0, 6));
160
- console.log('[gradial/mw] calling validatePreviewToken...');
161
- const claims = await validatePreviewToken(token, signKey);
162
- console.log('[gradial/mw] validatePreviewToken result:', claims ? JSON.stringify(claims) : 'null');
163
- if (!claims || claims.scope !== 'preview' || !claims.releaseId) {
164
- console.log('[gradial/mw] claims invalid: scope=', claims?.scope, 'releaseId=', claims?.releaseId);
165
- return '';
166
- }
167
- if (claims.siteId && config.siteId && claims.siteId !== config.siteId) {
168
- console.log('[gradial/mw] siteId mismatch: token=', claims.siteId, 'config=', config.siteId);
169
- return '';
170
- }
171
- if (!claims.expiresAt || Date.parse(claims.expiresAt) <= Date.now()) {
172
- console.log('[gradial/mw] token expired or no expiresAt:', claims.expiresAt);
173
- return '';
174
- }
175
- console.log('[gradial/mw] preview token valid! releaseId:', claims.releaseId);
176
- return claims.releaseId;
177
- }
178
- function previewTokenFromRequest(request) {
179
- const queryToken = request.nextUrl.searchParams.get('preview_token');
180
- if (queryToken)
181
- return queryToken;
182
- const cookieToken = request.cookies.get('aci_preview')?.value || '';
183
- if (cookieToken)
184
- return cookieToken;
185
- const auth = request.headers.get('authorization') || '';
186
- return auth.startsWith('Bearer ') ? auth.slice('Bearer '.length) : '';
187
- }
@@ -0,0 +1,41 @@
1
+ import type { Metadata } from 'next';
2
+ import { type KernelPage } from '../content/index.js';
3
+ import type { Registry } from '../registry.js';
4
+ type NextPageProps = {
5
+ params: Promise<{
6
+ slug?: string[];
7
+ }>;
8
+ };
9
+ export type SlotPropsFn = (slotName: string, page: KernelPage) => Record<string, unknown> | undefined;
10
+ export interface CreatePageOptions {
11
+ slotProps?: SlotPropsFn;
12
+ }
13
+ /**
14
+ * Creates the catch-all route handler for a Next.js ACI site.
15
+ *
16
+ * Usage in `src/app/[[...slug]]/page.tsx`:
17
+ *
18
+ * ```tsx
19
+ * import { createPage } from '@gradial/aci/next/page';
20
+ * import { registry } from '@/cms/registry';
21
+ *
22
+ * const page = createPage(registry);
23
+ * export const dynamic = page.dynamic;
24
+ * export const generateMetadata = page.generateMetadata;
25
+ * export default page.default;
26
+ * ```
27
+ *
28
+ * The SDK owns param normalization, content loading, metadata generation,
29
+ * notFound handling, layout resolution, and block rendering via the registry.
30
+ *
31
+ * Layout slots: createPage loads the page's layout, iterates all slots, and
32
+ * fills each from the page's regions. If a slot is empty, it falls back to
33
+ * the layout's fragment defaults (loaded via getFragment). This lets chrome
34
+ * be CMS-managed fragments instead of hardcoded component imports.
35
+ */
36
+ export declare function createPage(registry: Registry, options?: CreatePageOptions): {
37
+ dynamic: "force-dynamic";
38
+ generateMetadata: ({ params }: NextPageProps) => Promise<Metadata>;
39
+ default: ({ params }: NextPageProps) => Promise<import("react").JSX.Element>;
40
+ };
41
+ export {};
@@ -0,0 +1,77 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { notFound } from 'next/navigation.js';
3
+ import { Fragment } from 'react';
4
+ import { PageNotFoundError, routeFromNextParams, getRenderInput, getLayout, getFragment, } from './server.js';
5
+ import { routeMetadataForContent, resolveSlots, } from '../content/index.js';
6
+ import { renderBlocks } from './render.js';
7
+ /**
8
+ * Creates the catch-all route handler for a Next.js ACI site.
9
+ *
10
+ * Usage in `src/app/[[...slug]]/page.tsx`:
11
+ *
12
+ * ```tsx
13
+ * import { createPage } from '@gradial/aci/next/page';
14
+ * import { registry } from '@/cms/registry';
15
+ *
16
+ * const page = createPage(registry);
17
+ * export const dynamic = page.dynamic;
18
+ * export const generateMetadata = page.generateMetadata;
19
+ * export default page.default;
20
+ * ```
21
+ *
22
+ * The SDK owns param normalization, content loading, metadata generation,
23
+ * notFound handling, layout resolution, and block rendering via the registry.
24
+ *
25
+ * Layout slots: createPage loads the page's layout, iterates all slots, and
26
+ * fills each from the page's regions. If a slot is empty, it falls back to
27
+ * the layout's fragment defaults (loaded via getFragment). This lets chrome
28
+ * be CMS-managed fragments instead of hardcoded component imports.
29
+ */
30
+ export function createPage(registry, options) {
31
+ const dynamic = 'force-dynamic';
32
+ async function generateMetadata({ params }) {
33
+ const route = await routeFromNextParams(params);
34
+ const input = await getRenderInput(route);
35
+ const meta = routeMetadataForContent(input.siteConfig, input.page);
36
+ return {
37
+ title: meta.title,
38
+ description: meta.description,
39
+ alternates: meta.canonical ? { canonical: meta.canonical } : undefined,
40
+ openGraph: {
41
+ title: meta.title,
42
+ description: meta.description,
43
+ siteName: meta.siteName,
44
+ },
45
+ };
46
+ }
47
+ async function CatchAllPage({ params }) {
48
+ const route = await routeFromNextParams(params);
49
+ try {
50
+ const input = await getRenderInput(route);
51
+ if (!input.page || input.page.status !== 'published')
52
+ notFound();
53
+ const layout = await getLayout(input.page.layout);
54
+ const resolvedSlots = await resolveSlots(layout, input.page.regions || {}, (id) => getFragment(id));
55
+ return (_jsx(_Fragment, { children: layout.slots.map((slot) => {
56
+ const blocks = resolvedSlots[slot.name];
57
+ if (!blocks?.length)
58
+ return null;
59
+ const extraProps = options?.slotProps?.(slot.name, input.page);
60
+ if (slot.name === 'main') {
61
+ return (_jsx("main", { id: "content-main", children: renderBlocks(blocks, registry, extraProps) }, "main"));
62
+ }
63
+ return (_jsx(Fragment, { children: renderBlocks(blocks, registry, extraProps) }, slot.name));
64
+ }) }));
65
+ }
66
+ catch (error) {
67
+ if (error instanceof PageNotFoundError)
68
+ notFound();
69
+ throw error;
70
+ }
71
+ }
72
+ return {
73
+ dynamic,
74
+ generateMetadata,
75
+ default: CatchAllPage,
76
+ };
77
+ }
@@ -1,18 +1,9 @@
1
1
  import { headers } from 'next/headers.js';
2
- const RELEASE_HEADER = 'x-gradial-release-id';
3
- const PREVIEW_HEADER = 'x-gradial-preview';
2
+ import { PREVIEW_HEADER, RELEASE_HEADER } from '../preview/core.js';
4
3
  export async function isPreviewMode() {
5
- try {
6
- const current = await headers();
7
- const previewFlag = current.get(PREVIEW_HEADER);
8
- const releaseId = current.get(RELEASE_HEADER);
9
- console.log('[gradial/server] isPreviewMode: x-gradial-preview=', previewFlag, 'x-gradial-release-id=', releaseId || '(none)');
10
- if (previewFlag === '1') {
11
- return releaseId || '';
12
- }
13
- }
14
- catch (err) {
15
- console.log('[gradial/server] isPreviewMode error:', String(err));
4
+ const current = await headers();
5
+ if (current.get(PREVIEW_HEADER) === '1') {
6
+ return current.get(RELEASE_HEADER) || '';
16
7
  }
17
8
  return '';
18
9
  }
@@ -0,0 +1,5 @@
1
+ import type { ReactElement } from 'react';
2
+ import type { Registry } from '../registry.js';
3
+ import type { KernelPageBlock } from '../content/types.js';
4
+ export declare function renderBlock(block: KernelPageBlock, registry: Registry, extraProps?: Record<string, unknown>): ReactElement | null;
5
+ export declare function renderBlocks(blocks: readonly KernelPageBlock[], registry: Registry, extraProps?: Record<string, unknown>): (ReactElement | null)[];
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export function renderBlock(block, registry, extraProps) {
3
+ const entry = registry.find((r) => r.name === block.component);
4
+ if (!entry) {
5
+ if (process.env.NODE_ENV !== 'production') {
6
+ return (_jsxs("div", { className: "mx-auto my-4 max-w-[var(--max-width-container)] rounded-card border-2 border-red-600 bg-red-50 px-5 py-4 font-mono text-sm leading-relaxed text-red-900", children: [_jsx("strong", { children: "Unknown block:" }), " ", _jsx("code", { children: block.component }), _jsx("br", {}), _jsxs("span", { className: "text-red-800 opacity-70", children: ["id: ", block.id] })] }, block.id));
7
+ }
8
+ return null;
9
+ }
10
+ const Component = entry.component;
11
+ return _jsx(Component, { ...block.props, ...extraProps }, block.id);
12
+ }
13
+ export function renderBlocks(blocks, registry, extraProps) {
14
+ return blocks.map((block) => renderBlock(block, registry, extraProps));
15
+ }
@@ -0,0 +1,4 @@
1
+ import type { ReactNode } from 'react';
2
+ export declare function RootLayout({ children }: {
3
+ children: ReactNode;
4
+ }): import("react").JSX.Element;
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { DevRefresh } from './dev-refresh.js';
3
+ export function RootLayout({ children }) {
4
+ return (_jsx("html", { lang: "en", children: _jsxs("body", { children: [children, _jsx(DevRefresh, {})] }) }));
5
+ }
@@ -1,6 +1,6 @@
1
- import { FragmentNotFoundError, PageNotFoundError } from '../content/provider.js';
1
+ import { FragmentNotFoundError, LayoutNotFoundError, PageNotFoundError } from '../content/provider.js';
2
2
  import type { RenderInput } from '../content/types.js';
3
- export { FragmentNotFoundError, PageNotFoundError };
3
+ export { FragmentNotFoundError, PageNotFoundError, LayoutNotFoundError };
4
4
  export interface GradialFetchConfig {
5
5
  siteId?: string;
6
6
  bucket?: string;
@@ -29,6 +29,7 @@ export interface GradialSiteConfig {
29
29
  export declare function getPage<T = unknown>(route?: string, config?: GradialFetchConfig): Promise<T>;
30
30
  export declare function getSiteConfig<T extends GradialSiteConfig = GradialSiteConfig>(config?: GradialFetchConfig): Promise<T>;
31
31
  export declare function getFragment<T = unknown>(name: string, config?: GradialFetchConfig): Promise<T>;
32
+ export declare function getLayout<T = unknown>(name: string, config?: GradialFetchConfig): Promise<T>;
32
33
  export declare function getRoutes(config?: GradialFetchConfig): Promise<string[]>;
33
34
  export declare function getRenderInput<TPage, TSite extends GradialSiteConfig = GradialSiteConfig>(route?: string, config?: GradialFetchConfig): Promise<GradialRenderInput<TPage, TSite>>;
34
35
  export declare function getPageRuntimeRenderInput(): Promise<RenderInput | null>;
@@ -1,12 +1,12 @@
1
1
  import { headers } from 'next/headers.js';
2
- import { FragmentNotFoundError, PageNotFoundError, } from '../content/provider.js';
2
+ import { FragmentNotFoundError, LayoutNotFoundError, PageNotFoundError, } from '../content/provider.js';
3
3
  import { getCachedManifest, getCachedSiteConfig } from '../content/cache.js';
4
4
  import { S3ContentProvider } from '../providers/s3.js';
5
5
  import { GRADIAL_RENDER_INPUT_HEADER, getPendingRenderInput } from '../runtime/page.js';
6
6
  import { getUncachedEdgeConfigValue } from './edge-config.js';
7
7
  const RELEASE_HEADER = 'x-gradial-release-id';
8
8
  const PREVIEW_HEADER = 'x-gradial-preview';
9
- export { FragmentNotFoundError, PageNotFoundError };
9
+ export { FragmentNotFoundError, PageNotFoundError, LayoutNotFoundError };
10
10
  export async function getPage(route = '', config = {}) {
11
11
  const runtimeInput = await getPageRuntimeRenderInput();
12
12
  if (runtimeInput)
@@ -31,29 +31,33 @@ export async function getFragment(name, config = {}) {
31
31
  const provider = await resolveProviderWithFallback(config);
32
32
  return await provider.getFragment(name);
33
33
  }
34
+ export async function getLayout(name, config = {}) {
35
+ const runtimeInput = await getPageRuntimeRenderInput();
36
+ if (runtimeInput) {
37
+ const layouts = runtimeInput.layouts || {};
38
+ if (name in layouts)
39
+ return layouts[name];
40
+ }
41
+ const provider = await resolveProviderWithFallback(config);
42
+ return await provider.getLayout(name);
43
+ }
34
44
  export async function getRoutes(config = {}) {
35
45
  const provider = await resolveProviderWithFallback(config);
36
46
  const routes = await provider.listRoutes();
37
47
  return routes.map((route) => route.path).sort();
38
48
  }
39
49
  export async function getRenderInput(route = '/', config = {}) {
40
- const startTime = Date.now();
41
50
  const runtimeInput = await getPageRuntimeRenderInput();
42
51
  if (runtimeInput) {
43
52
  return runtimeInput;
44
53
  }
45
- const providerStart = Date.now();
46
54
  const provider = await resolveProviderWithFallback(config);
47
- console.log(`[ACI SSR] Provider resolved in ${Date.now() - providerStart}ms`);
48
55
  // Cache shared resources (manifest, siteConfig) but not per-page content
49
56
  // Page-level caching is handled by CDN
50
- const fetchStart = Date.now();
51
57
  const [page, siteConfig] = await Promise.all([
52
58
  provider.getPage(route),
53
59
  getCachedSiteConfig(provider),
54
60
  ]);
55
- console.log(`[ACI SSR] Content fetched in ${Date.now() - fetchStart}ms`);
56
- console.log(`[ACI SSR] Total getRenderInput for ${route}: ${Date.now() - startTime}ms`);
57
61
  return {
58
62
  route,
59
63
  domain: siteConfig.domain || 'www.aci.local',
@@ -0,0 +1,41 @@
1
+ export interface PreviewClaims {
2
+ scope?: string;
3
+ releaseId?: string;
4
+ siteId?: string;
5
+ expiresAt?: string;
6
+ }
7
+ export interface PreviewTokenSource {
8
+ query?: URLSearchParams;
9
+ cookies?: Map<string, string> | Record<string, string>;
10
+ authorization?: string;
11
+ }
12
+ export interface PreviewResolveOptions {
13
+ siteId: string;
14
+ signKey?: string;
15
+ token?: string;
16
+ }
17
+ export interface PreviewContext {
18
+ isPreview: boolean;
19
+ releaseId?: string;
20
+ token?: string;
21
+ }
22
+ export interface PreviewCookie {
23
+ name: string;
24
+ value: string;
25
+ options: {
26
+ httpOnly: boolean;
27
+ sameSite: 'lax' | 'strict' | 'none';
28
+ maxAge: number;
29
+ path: string;
30
+ };
31
+ }
32
+ export declare const PREVIEW_HEADER = "x-gradial-preview";
33
+ export declare const RELEASE_HEADER = "x-gradial-release-id";
34
+ export declare const PREVIEW_TOKEN_COOKIE = "aci_preview";
35
+ export declare const PREVIEW_RELEASE_COOKIE = "aci_preview_release";
36
+ export declare function validatePreviewToken(token: string, signKey: string): Promise<PreviewClaims | null>;
37
+ export declare function extractPreviewToken(source: PreviewTokenSource): string | undefined;
38
+ export declare function previewSignKey(): string | undefined;
39
+ export declare function resolvePreviewContext(options: PreviewResolveOptions): Promise<PreviewContext>;
40
+ export declare function createPreviewCookies(token: string, releaseId: string): PreviewCookie[];
41
+ export declare function createPreviewBannerHTML(releaseId: string): string;