@gradial/aci 0.1.19 → 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.
- package/README.md +28 -8
- package/bin/aci +0 -0
- package/dist/astro/index.d.ts +4 -2
- package/dist/astro/index.js +9 -1
- package/dist/astro/preview.d.ts +5 -0
- package/dist/astro/preview.js +33 -0
- package/dist/content/index.d.ts +1 -0
- package/dist/content/index.js +1 -0
- package/dist/content/provider.d.ts +19 -0
- package/dist/content/provider.js +7 -0
- package/dist/content/resolve-slots.d.ts +9 -0
- package/dist/content/resolve-slots.js +24 -0
- package/dist/content/types.d.ts +1 -1
- package/dist/content/validation.js +0 -1
- package/dist/define-component.js +0 -6
- package/dist/define-data-model.d.ts +2 -0
- package/dist/define-data-model.js +3 -0
- package/dist/define-layout.d.ts +2 -0
- package/dist/define-layout.js +3 -0
- package/dist/dev/index.d.ts +12 -0
- package/dist/dev/index.js +84 -9
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/next/asset-route.d.ts +2 -0
- package/dist/next/asset-route.js +10 -2
- package/dist/next/config.d.ts +1 -1
- package/dist/next/config.js +14 -2
- package/dist/next/content-watch.d.ts +11 -0
- package/dist/next/content-watch.js +106 -0
- package/dist/next/index.d.ts +2 -2
- package/dist/next/index.js +2 -2
- package/dist/next/middleware.d.ts +6 -2
- package/dist/next/middleware.js +40 -51
- package/dist/next/page.d.ts +41 -0
- package/dist/next/page.js +77 -0
- package/dist/next/preview-mode.js +4 -8
- package/dist/next/render.d.ts +5 -0
- package/dist/next/render.js +15 -0
- package/dist/next/root-layout.d.ts +4 -0
- package/dist/next/root-layout.js +5 -0
- package/dist/next/server.d.ts +3 -2
- package/dist/next/server.js +12 -8
- package/dist/preview/core.d.ts +41 -0
- package/dist/preview/core.js +116 -0
- package/dist/providers/file.d.ts +2 -0
- package/dist/providers/file.js +61 -1
- package/dist/providers/s3.d.ts +1 -0
- package/dist/providers/s3.js +14 -1
- package/dist/registry.d.ts +15 -0
- package/dist/registry.js +10 -0
- package/dist/sveltekit/index.d.ts +4 -1
- package/dist/sveltekit/index.js +9 -1
- package/dist/sveltekit/preview.d.ts +5 -0
- package/dist/sveltekit/preview.js +33 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.js +15 -2
- package/dist/types/component.d.ts +0 -9
- package/dist/types/config.d.ts +1 -1
- package/dist/types/data.d.ts +19 -0
- package/dist/types/data.js +11 -0
- package/dist/types/image.d.ts +1 -1
- package/dist/types/image.js +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/page.d.ts +1 -2
- package/dist/types/render-mode.d.ts +0 -1
- package/package.json +40 -6
- package/src/cli/compile-registry.mjs +167 -19
- package/src/types/component.ts +0 -10
- package/src/types/config.ts +1 -1
- package/src/types/data.ts +47 -0
- package/src/types/image.ts +2 -2
- package/src/types/index.ts +1 -0
- package/src/types/page.ts +1 -2
- package/src/types/render-mode.ts +0 -8
- package/dist/next/preview-banner.d.ts +0 -4
- package/dist/next/preview-banner.js +0 -51
- package/dist/next/preview.d.ts +0 -7
- package/dist/next/preview.js +0 -37
- package/dist/render.d.ts +0 -14
- package/dist/render.js +0 -33
- 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
|
+
}
|
package/dist/next/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { DevRefresh, type DevRefreshProps } from './dev-refresh.js';
|
|
2
|
-
export {
|
|
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';
|
package/dist/next/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { DevRefresh } from './dev-refresh.js';
|
|
2
|
-
export {
|
|
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
|
|
3
|
+
siteId?: string;
|
|
4
4
|
edgeConfig?: string;
|
|
5
|
+
previewSignKey?: string;
|
|
5
6
|
deploymentId?: string;
|
|
6
7
|
}
|
|
7
|
-
export declare function createGradialMiddleware(config
|
|
8
|
+
export declare function createGradialMiddleware(config?: GradialMiddlewareConfig): (request: NextRequest) => Promise<NextResponse<unknown>>;
|
|
9
|
+
export declare const config: {
|
|
10
|
+
matcher: string[];
|
|
11
|
+
};
|
package/dist/next/middleware.js
CHANGED
|
@@ -1,61 +1,78 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server.js';
|
|
2
2
|
import { getUncachedEdgeConfigValue } from './edge-config.js';
|
|
3
|
-
import {
|
|
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
9
|
// ?leave_preview=1 → clear cookies, redirect to clean URL
|
|
11
10
|
if (url.searchParams.get('leave_preview') === '1') {
|
|
12
11
|
const cleanUrl = new URL(url.pathname, url.origin);
|
|
13
12
|
const response = NextResponse.redirect(cleanUrl);
|
|
14
|
-
response.cookies.delete(
|
|
15
|
-
response.cookies.delete(
|
|
13
|
+
response.cookies.delete(PREVIEW_TOKEN_COOKIE);
|
|
14
|
+
response.cookies.delete(PREVIEW_RELEASE_COOKIE);
|
|
16
15
|
return response;
|
|
17
16
|
}
|
|
18
17
|
const requestHeaders = gradialHeaders(request.headers);
|
|
19
18
|
// Try preview token first; if valid, use that release.
|
|
20
19
|
// If no token or invalid, fall through to active release from edge config.
|
|
21
|
-
const
|
|
22
|
-
const releaseId =
|
|
20
|
+
const preview = await resolvePreviewFromRequest(request, config);
|
|
21
|
+
const releaseId = preview?.releaseId || await activeReleaseFromEdgeConfig(config);
|
|
23
22
|
if (!releaseId) {
|
|
24
23
|
return NextResponse.next({ request: { headers: requestHeaders } });
|
|
25
24
|
}
|
|
26
25
|
requestHeaders.set(RELEASE_HEADER, releaseId);
|
|
27
26
|
requestHeaders.set(ROUTE_HEADER, url.pathname);
|
|
28
27
|
requestHeaders.set(DISPATCH_HEADER, 'ssr-page');
|
|
29
|
-
if (
|
|
30
|
-
requestHeaders.set(
|
|
28
|
+
if (preview?.isPreview) {
|
|
29
|
+
requestHeaders.set(PREVIEW_HEADER, '1');
|
|
31
30
|
}
|
|
32
31
|
const response = NextResponse.next({ request: { headers: requestHeaders } });
|
|
33
32
|
// Persist preview token as cookie so navigation stays in preview
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
sameSite: 'lax',
|
|
39
|
-
maxAge: 60 * 60,
|
|
40
|
-
path: '/',
|
|
41
|
-
});
|
|
42
|
-
response.cookies.set('aci_preview_release', previewReleaseId, {
|
|
43
|
-
httpOnly: false,
|
|
44
|
-
sameSite: 'lax',
|
|
45
|
-
maxAge: 60 * 60,
|
|
46
|
-
path: '/',
|
|
47
|
-
});
|
|
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
|
+
}
|
|
48
37
|
}
|
|
49
38
|
return response;
|
|
50
39
|
};
|
|
51
40
|
}
|
|
41
|
+
export const config = {
|
|
42
|
+
matcher: [
|
|
43
|
+
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
|
|
44
|
+
],
|
|
45
|
+
};
|
|
52
46
|
function gradialHeaders(headers) {
|
|
53
47
|
const next = new Headers(headers);
|
|
54
48
|
next.delete(RELEASE_HEADER);
|
|
55
49
|
next.delete(ROUTE_HEADER);
|
|
56
50
|
next.delete(DISPATCH_HEADER);
|
|
51
|
+
next.delete(PREVIEW_HEADER);
|
|
57
52
|
return next;
|
|
58
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
|
+
}
|
|
59
76
|
async function activeReleaseFromEdgeConfig(config) {
|
|
60
77
|
const edgeConfig = config.edgeConfig || process.env.EDGE_CONFIG || '';
|
|
61
78
|
if (!edgeConfig || !config.siteId)
|
|
@@ -126,31 +143,3 @@ function clampRelease(activeReleaseId, activeSequence, compatibility) {
|
|
|
126
143
|
function edgeKeySegment(value) {
|
|
127
144
|
return value.replace(/[^A-Za-z0-9_-]/g, '_');
|
|
128
145
|
}
|
|
129
|
-
async function releaseIdFromPreviewToken(request, config) {
|
|
130
|
-
const token = previewTokenFromRequest(request);
|
|
131
|
-
if (!token)
|
|
132
|
-
return '';
|
|
133
|
-
const signKey = process.env.GRADIAL_PREVIEW_SIGN_KEY || '';
|
|
134
|
-
if (!signKey) {
|
|
135
|
-
console.warn('[gradial/mw] GRADIAL_PREVIEW_SIGN_KEY not set — preview tokens will be ignored');
|
|
136
|
-
return '';
|
|
137
|
-
}
|
|
138
|
-
const claims = await validatePreviewToken(token, signKey);
|
|
139
|
-
if (!claims || claims.scope !== 'preview' || !claims.releaseId)
|
|
140
|
-
return '';
|
|
141
|
-
if (claims.siteId && config.siteId && claims.siteId !== config.siteId)
|
|
142
|
-
return '';
|
|
143
|
-
if (!claims.expiresAt || Date.parse(claims.expiresAt) <= Date.now())
|
|
144
|
-
return '';
|
|
145
|
-
return claims.releaseId;
|
|
146
|
-
}
|
|
147
|
-
function previewTokenFromRequest(request) {
|
|
148
|
-
const queryToken = request.nextUrl.searchParams.get('preview_token');
|
|
149
|
-
if (queryToken)
|
|
150
|
-
return queryToken;
|
|
151
|
-
const cookieToken = request.cookies.get('aci_preview')?.value || '';
|
|
152
|
-
if (cookieToken)
|
|
153
|
-
return cookieToken;
|
|
154
|
-
const auth = request.headers.get('authorization') || '';
|
|
155
|
-
return auth.startsWith('Bearer ') ? auth.slice('Bearer '.length) : '';
|
|
156
|
-
}
|
|
@@ -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,13 +1,9 @@
|
|
|
1
1
|
import { headers } from 'next/headers.js';
|
|
2
|
-
|
|
3
|
-
const PREVIEW_HEADER = 'x-gradial-preview';
|
|
2
|
+
import { PREVIEW_HEADER, RELEASE_HEADER } from '../preview/core.js';
|
|
4
3
|
export async function isPreviewMode() {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
return current.get(RELEASE_HEADER) || '';
|
|
9
|
-
}
|
|
4
|
+
const current = await headers();
|
|
5
|
+
if (current.get(PREVIEW_HEADER) === '1') {
|
|
6
|
+
return current.get(RELEASE_HEADER) || '';
|
|
10
7
|
}
|
|
11
|
-
catch { }
|
|
12
8
|
return '';
|
|
13
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,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
|
+
}
|
package/dist/next/server.d.ts
CHANGED
|
@@ -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>;
|
package/dist/next/server.js
CHANGED
|
@@ -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;
|