@gradial/aci 0.1.20-preview.1 → 0.1.20-preview.2
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 +10 -8
- package/bin/aci +0 -0
- package/dist/astro/index.d.ts +0 -2
- package/dist/astro/index.js +0 -4
- package/dist/content/cache.test.d.ts +1 -0
- package/dist/content/cache.test.js +86 -0
- package/dist/content/index.d.ts +0 -1
- package/dist/content/index.js +0 -1
- package/dist/content/provider.d.ts +0 -19
- package/dist/content/provider.js +0 -7
- package/dist/content/types.d.ts +1 -1
- package/dist/content/validation.js +1 -0
- package/dist/define-component.js +6 -0
- package/dist/define-layout.d.ts +0 -2
- package/dist/define-layout.js +0 -3
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/next/asset-route.d.ts +0 -2
- package/dist/next/asset-route.js +2 -10
- package/dist/next/config.js +1 -1
- package/dist/next/middleware.d.ts +2 -6
- package/dist/next/middleware.js +2 -7
- package/dist/next/page.d.ts +15 -16
- package/dist/next/page.js +9 -27
- package/dist/next/preview-banner.d.ts +4 -0
- package/dist/next/preview-banner.js +51 -0
- package/dist/next/preview.d.ts +7 -0
- package/dist/next/preview.js +37 -0
- package/dist/next/server.d.ts +2 -3
- package/dist/next/server.js +2 -12
- package/dist/providers/file.d.ts +0 -2
- package/dist/providers/file.js +1 -61
- package/dist/providers/s3.d.ts +0 -1
- package/dist/providers/s3.js +1 -14
- package/dist/sveltekit/index.d.ts +0 -2
- package/dist/sveltekit/index.js +0 -4
- package/dist/testing/index.d.ts +0 -3
- package/dist/testing/index.js +2 -15
- package/dist/types/component.d.ts +9 -0
- package/dist/types/config.d.ts +1 -0
- package/dist/types/page.d.ts +2 -1
- package/dist/types/render-mode.d.ts +1 -0
- package/package.json +7 -18
- package/src/cli/compile-registry.mjs +18 -127
- package/src/cli/verify-renderer.mjs +73 -0
- package/src/types/component.ts +10 -0
- package/src/types/config.ts +1 -0
- package/src/types/page.ts +2 -1
- package/src/types/render-mode.ts +8 -0
- package/dist/content/resolve-slots.d.ts +0 -9
- package/dist/content/resolve-slots.js +0 -24
- package/dist/next/render.d.ts +0 -5
- package/dist/next/render.js +0 -15
- package/dist/next/root-layout.d.ts +0 -4
- package/dist/next/root-layout.js +0 -5
- package/dist/registry.d.ts +0 -15
- package/dist/registry.js +0 -10
package/README.md
CHANGED
|
@@ -10,9 +10,9 @@ content provider helpers, and framework-specific subpaths from one package.
|
|
|
10
10
|
The fastest way to create a new ACI site is via the scaffolding script:
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
|
|
13
|
+
npx tsx scripts/create-site.ts --framework astro --name my-site --dir ./my-site
|
|
14
14
|
cd my-site
|
|
15
|
-
|
|
15
|
+
npm run dev
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
Available frameworks: `astro`, `nextjs`, `sveltekit`.
|
|
@@ -28,8 +28,8 @@ See the [templates directory](templates/) for the canonical starter source.
|
|
|
28
28
|
## Installation
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
npm install @gradial/aci zod@^4
|
|
32
|
+
npm install -D typescript tsx
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
Common imports:
|
|
@@ -78,6 +78,7 @@ export const heroContract = defineComponentContract({
|
|
|
78
78
|
headline: z.string(),
|
|
79
79
|
image: GradialImageSchema,
|
|
80
80
|
}),
|
|
81
|
+
renderModes: { canStatic: true, canSSR: true, canClientIsland: false },
|
|
81
82
|
});
|
|
82
83
|
|
|
83
84
|
export type HeroProps = z.infer<typeof heroContract.schema>;
|
|
@@ -106,6 +107,7 @@ const content = new FixtureContentProvider({
|
|
|
106
107
|
$type: 'page',
|
|
107
108
|
status: 'published',
|
|
108
109
|
layout: 'marketing',
|
|
110
|
+
renderMode: 'static',
|
|
109
111
|
metadata: { title: 'Test Home' },
|
|
110
112
|
regions: { main: [] }
|
|
111
113
|
}
|
|
@@ -121,7 +123,7 @@ Use this in unit tests when you want the same provider API as
|
|
|
121
123
|
Publish from this package directory:
|
|
122
124
|
|
|
123
125
|
```bash
|
|
124
|
-
|
|
126
|
+
npm publish --access public
|
|
125
127
|
```
|
|
126
128
|
|
|
127
129
|
The package includes a `prepack` script that builds `dist` before packing or
|
|
@@ -129,15 +131,15 @@ publishing.
|
|
|
129
131
|
|
|
130
132
|
## Verify the Published Shape Locally
|
|
131
133
|
|
|
132
|
-
Before publishing, inspect the exact tarball
|
|
134
|
+
Before publishing, inspect the exact tarball npm will upload. This catches
|
|
133
135
|
package `files` mistakes where local source imports work from the repo but fail
|
|
134
|
-
after
|
|
136
|
+
after npm filters the packed files.
|
|
135
137
|
|
|
136
138
|
From `packages/sdk`:
|
|
137
139
|
|
|
138
140
|
```bash
|
|
139
141
|
tmpdir="$(mktemp -d /tmp/gradial-aci-pack.XXXXXX)"
|
|
140
|
-
filename="$(
|
|
142
|
+
filename="$(npm pack --pack-destination "$tmpdir" | tail -n 1)"
|
|
141
143
|
tar -tzf "$tmpdir/$filename" | sort | rg '^(package/src/cli/|package/src/types/|package/dist/types/)'
|
|
142
144
|
echo "tarball=$tmpdir/$filename"
|
|
143
145
|
```
|
package/bin/aci
CHANGED
|
Binary file
|
package/dist/astro/index.d.ts
CHANGED
|
@@ -33,8 +33,6 @@ export declare function getGradialStaticPaths(): Promise<Array<{
|
|
|
33
33
|
};
|
|
34
34
|
}>>;
|
|
35
35
|
export declare function getPageRuntimeRenderInput(request: Request): RenderInput | null;
|
|
36
|
-
export { createRegistry, registryLookup } from '../registry.js';
|
|
37
|
-
export type { Registry, RegistryEntry, AnyComponent } from '../registry.js';
|
|
38
36
|
export { getPreviewMode, createPreviewBannerHTML, createPreviewCookies, type PreviewContext, type PreviewCookie, } from './preview.js';
|
|
39
37
|
export interface GradialAstroOptions extends GradialContentWatchOptions {
|
|
40
38
|
/** Path to compiled content root. Defaults to ACI_CONTENT_ROOT env or '.aci/compiled' */
|
package/dist/astro/index.js
CHANGED
|
@@ -32,10 +32,6 @@ export function getPageRuntimeRenderInput(request) {
|
|
|
32
32
|
return getPendingRenderInputFromHeaders(request.headers);
|
|
33
33
|
}
|
|
34
34
|
// ---------------------------------------------------------------------------
|
|
35
|
-
// Registry helpers
|
|
36
|
-
// ---------------------------------------------------------------------------
|
|
37
|
-
export { createRegistry, registryLookup } from '../registry.js';
|
|
38
|
-
// ---------------------------------------------------------------------------
|
|
39
35
|
// Preview support
|
|
40
36
|
// ---------------------------------------------------------------------------
|
|
41
37
|
export { getPreviewMode, createPreviewBannerHTML, createPreviewCookies, } from './preview.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from '@jest/globals';
|
|
2
|
+
import { getCachedSiteConfig, getCachedManifest, clearProviderCache, getCacheStats, resetCacheStats } from './cache.js';
|
|
3
|
+
describe('Content Cache', () => {
|
|
4
|
+
let mockProvider;
|
|
5
|
+
let getSiteConfigCallCount = 0;
|
|
6
|
+
let manifestCallCount = 0;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
getSiteConfigCallCount = 0;
|
|
9
|
+
manifestCallCount = 0;
|
|
10
|
+
resetCacheStats();
|
|
11
|
+
mockProvider = {
|
|
12
|
+
getSiteConfig: async () => {
|
|
13
|
+
getSiteConfigCallCount++;
|
|
14
|
+
return { title: 'Test Site' };
|
|
15
|
+
},
|
|
16
|
+
manifest: async () => {
|
|
17
|
+
manifestCallCount++;
|
|
18
|
+
return {
|
|
19
|
+
routes: {},
|
|
20
|
+
fragments: {},
|
|
21
|
+
siteConfigRef: 'site.json'
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
getPage: async () => ({}),
|
|
25
|
+
getFragment: async () => ({}),
|
|
26
|
+
listRoutes: async () => []
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
it('should cache siteConfig across multiple calls', async () => {
|
|
30
|
+
const result1 = await getCachedSiteConfig(mockProvider);
|
|
31
|
+
const result2 = await getCachedSiteConfig(mockProvider);
|
|
32
|
+
expect(getSiteConfigCallCount).toBe(1); // Only called once
|
|
33
|
+
expect(result1).toEqual(result2);
|
|
34
|
+
const stats = getCacheStats();
|
|
35
|
+
expect(stats.siteConfig.hits).toBe(1);
|
|
36
|
+
expect(stats.siteConfig.misses).toBe(1);
|
|
37
|
+
});
|
|
38
|
+
it('should cache manifest across multiple calls', async () => {
|
|
39
|
+
const result1 = await getCachedManifest(mockProvider);
|
|
40
|
+
const result2 = await getCachedManifest(mockProvider);
|
|
41
|
+
expect(manifestCallCount).toBe(1); // Only called once
|
|
42
|
+
expect(result1).toEqual(result2);
|
|
43
|
+
const stats = getCacheStats();
|
|
44
|
+
expect(stats.manifest.hits).toBe(1);
|
|
45
|
+
expect(stats.manifest.misses).toBe(1);
|
|
46
|
+
});
|
|
47
|
+
it('should clear cache for a provider', async () => {
|
|
48
|
+
await getCachedSiteConfig(mockProvider);
|
|
49
|
+
expect(getSiteConfigCallCount).toBe(1);
|
|
50
|
+
clearProviderCache(mockProvider);
|
|
51
|
+
await getCachedSiteConfig(mockProvider);
|
|
52
|
+
expect(getSiteConfigCallCount).toBe(2); // Called again after clear
|
|
53
|
+
const stats = getCacheStats();
|
|
54
|
+
expect(stats.siteConfig.misses).toBe(2);
|
|
55
|
+
});
|
|
56
|
+
it('should not cache rejected promises', async () => {
|
|
57
|
+
const errorProvider = {
|
|
58
|
+
getSiteConfig: async () => {
|
|
59
|
+
getSiteConfigCallCount++;
|
|
60
|
+
throw new Error('S3 timeout');
|
|
61
|
+
},
|
|
62
|
+
getPage: async () => ({}),
|
|
63
|
+
getFragment: async () => ({}),
|
|
64
|
+
listRoutes: async () => []
|
|
65
|
+
};
|
|
66
|
+
await expect(getCachedSiteConfig(errorProvider)).rejects.toThrow('S3 timeout');
|
|
67
|
+
expect(getSiteConfigCallCount).toBe(1);
|
|
68
|
+
// Second call should retry (not cached)
|
|
69
|
+
await expect(getCachedSiteConfig(errorProvider)).rejects.toThrow('S3 timeout');
|
|
70
|
+
expect(getSiteConfigCallCount).toBe(2);
|
|
71
|
+
});
|
|
72
|
+
it('should cache separately per provider instance', async () => {
|
|
73
|
+
const provider2 = {
|
|
74
|
+
getSiteConfig: async () => ({ title: 'Site 2' }),
|
|
75
|
+
getPage: async () => ({}),
|
|
76
|
+
getFragment: async () => ({}),
|
|
77
|
+
listRoutes: async () => []
|
|
78
|
+
};
|
|
79
|
+
const result1 = await getCachedSiteConfig(mockProvider);
|
|
80
|
+
const result2 = await getCachedSiteConfig(provider2);
|
|
81
|
+
expect(result1).toEqual({ title: 'Test Site' });
|
|
82
|
+
expect(result2).toEqual({ title: 'Site 2' });
|
|
83
|
+
const stats = getCacheStats();
|
|
84
|
+
expect(stats.siteConfig.misses).toBe(2); // Both are cache misses
|
|
85
|
+
});
|
|
86
|
+
});
|
package/dist/content/index.d.ts
CHANGED
package/dist/content/index.js
CHANGED
|
@@ -12,33 +12,17 @@ export interface FragmentEntry {
|
|
|
12
12
|
digest: string;
|
|
13
13
|
fragmentRefs?: string[];
|
|
14
14
|
}
|
|
15
|
-
export interface LayoutEntry {
|
|
16
|
-
name: string;
|
|
17
|
-
slots: LayoutSlot[];
|
|
18
|
-
defaults?: Record<string, FragmentRef[]>;
|
|
19
|
-
}
|
|
20
|
-
export interface LayoutSlot {
|
|
21
|
-
name: string;
|
|
22
|
-
required: boolean;
|
|
23
|
-
}
|
|
24
|
-
export interface FragmentRef {
|
|
25
|
-
kind: string;
|
|
26
|
-
fragmentId: string;
|
|
27
|
-
inline: boolean;
|
|
28
|
-
}
|
|
29
15
|
export interface CompiledManifest {
|
|
30
16
|
manifestVersion: string;
|
|
31
17
|
buildDigest: string;
|
|
32
18
|
siteConfigRef: string;
|
|
33
19
|
routes: Record<string, RouteEntry>;
|
|
34
20
|
fragments: Record<string, FragmentEntry>;
|
|
35
|
-
layouts?: Record<string, LayoutEntry>;
|
|
36
21
|
}
|
|
37
22
|
export interface ContentProvider {
|
|
38
23
|
getSiteConfig<T = unknown>(): Promise<T>;
|
|
39
24
|
getPage<T = unknown>(route: string): Promise<T>;
|
|
40
25
|
getFragment<T = unknown>(id: string): Promise<T>;
|
|
41
|
-
getLayout<T = unknown>(name: string): Promise<T>;
|
|
42
26
|
listRoutes(): Promise<RouteEntry[]>;
|
|
43
27
|
}
|
|
44
28
|
export declare class PageNotFoundError extends Error {
|
|
@@ -47,9 +31,6 @@ export declare class PageNotFoundError extends Error {
|
|
|
47
31
|
export declare class FragmentNotFoundError extends Error {
|
|
48
32
|
constructor(id: string, cause?: unknown);
|
|
49
33
|
}
|
|
50
|
-
export declare class LayoutNotFoundError extends Error {
|
|
51
|
-
constructor(name: string, cause?: unknown);
|
|
52
|
-
}
|
|
53
34
|
export interface RenderInputOptions {
|
|
54
35
|
domain?: string;
|
|
55
36
|
locale?: string;
|
package/dist/content/provider.js
CHANGED
|
@@ -16,13 +16,6 @@ export class FragmentNotFoundError extends Error {
|
|
|
16
16
|
this.cause = cause;
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
-
export class LayoutNotFoundError extends Error {
|
|
20
|
-
constructor(name, cause) {
|
|
21
|
-
super(`Layout not found: ${name}`);
|
|
22
|
-
this.name = 'LayoutNotFoundError';
|
|
23
|
-
this.cause = cause;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
19
|
import { getCachedSiteConfig } from './cache.js';
|
|
27
20
|
export async function loadRenderInput(provider, route = '/', options = {}) {
|
|
28
21
|
const normalized = normalizeRoute(route);
|
package/dist/content/types.d.ts
CHANGED
|
@@ -55,6 +55,7 @@ export interface KernelPage {
|
|
|
55
55
|
$type: 'page';
|
|
56
56
|
status: string;
|
|
57
57
|
layout: string;
|
|
58
|
+
renderMode: string;
|
|
58
59
|
metadata: KernelPageSeo;
|
|
59
60
|
regions: Record<string, KernelPageBlock[]>;
|
|
60
61
|
}
|
|
@@ -92,7 +93,6 @@ export interface RenderInput<TPage extends KernelPage = KernelPage, TSiteConfig
|
|
|
92
93
|
siteConfig: TSiteConfig;
|
|
93
94
|
page: TPage | null;
|
|
94
95
|
fragments?: Record<string, unknown>;
|
|
95
|
-
layouts?: Record<string, unknown>;
|
|
96
96
|
}
|
|
97
97
|
export interface RenderOutput {
|
|
98
98
|
route: string;
|
|
@@ -121,6 +121,7 @@ export function createContentSchemas(contracts = {}) {
|
|
|
121
121
|
$type: z.literal('page'),
|
|
122
122
|
status: z.string().min(1),
|
|
123
123
|
layout: z.string().min(1),
|
|
124
|
+
renderMode: z.string().min(1),
|
|
124
125
|
metadata: z.looseObject({
|
|
125
126
|
title: z.string().optional(),
|
|
126
127
|
description: z.string().optional(),
|
package/dist/define-component.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
export function defineComponent(definition) {
|
|
2
|
+
const renderModes = definition.renderModes ?? definition.render;
|
|
3
|
+
if (!renderModes) {
|
|
4
|
+
throw new Error(`component ${definition.name} must define renderModes`);
|
|
5
|
+
}
|
|
2
6
|
return {
|
|
3
7
|
...definition,
|
|
8
|
+
renderModes,
|
|
9
|
+
render: renderModes,
|
|
4
10
|
varyDimensions: definition.varyDimensions ?? definition.vary,
|
|
5
11
|
vary: definition.vary ?? definition.varyDimensions,
|
|
6
12
|
};
|
package/dist/define-layout.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import type { LayoutContract, LayoutDefinition, LayoutSlot } from './types/layout.js';
|
|
2
|
-
import type { FragmentRefNode } from './types/page.js';
|
|
3
2
|
export declare function defineLayout(definition: LayoutDefinition): LayoutContract;
|
|
4
3
|
export declare const defineLayoutContract: typeof defineLayout;
|
|
5
4
|
export declare function slot(name: string, required?: boolean): LayoutSlot;
|
|
6
|
-
export declare function fragmentDefault(fragmentId: string): FragmentRefNode;
|
package/dist/define-layout.js
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -2,7 +2,6 @@ export * from './block-ref.js';
|
|
|
2
2
|
export * from './define-component.js';
|
|
3
3
|
export * from './define-data-model.js';
|
|
4
4
|
export * from './define-layout.js';
|
|
5
|
-
export * from './registry.js';
|
|
6
5
|
export * from './types/index.js';
|
|
7
6
|
// Note: content/ is NOT re-exported here because it imports node:path,
|
|
8
7
|
// making it incompatible with client-side bundlers. Import server-only
|
|
@@ -5,7 +5,5 @@ type AssetRouteParams = {
|
|
|
5
5
|
releaseId?: string;
|
|
6
6
|
path?: string[];
|
|
7
7
|
};
|
|
8
|
-
export declare const runtime = "nodejs";
|
|
9
|
-
export declare const dynamic = "force-dynamic";
|
|
10
8
|
export declare function GET(_request: Request, { params }: AssetRouteContext): Promise<Response>;
|
|
11
9
|
export {};
|
package/dist/next/asset-route.js
CHANGED
|
@@ -1,19 +1,11 @@
|
|
|
1
|
-
import { FileContentProvider } from '../providers/file.js';
|
|
2
1
|
import { getReleaseAssetResponse } from './server.js';
|
|
3
|
-
const LOCAL_RELEASE_ID = 'local';
|
|
4
|
-
export const runtime = 'nodejs';
|
|
5
|
-
export const dynamic = 'force-dynamic';
|
|
6
2
|
export async function GET(_request, { params }) {
|
|
7
3
|
const resolvedParams = await params;
|
|
8
4
|
const releaseId = resolvedParams.releaseId || '';
|
|
9
|
-
const assetPath =
|
|
10
|
-
if (!releaseId || assetPath
|
|
5
|
+
const assetPath = (resolvedParams.path || []).join('/');
|
|
6
|
+
if (!releaseId || !assetPath) {
|
|
11
7
|
return new Response('asset not found', { status: 404 });
|
|
12
8
|
}
|
|
13
|
-
if (releaseId === LOCAL_RELEASE_ID) {
|
|
14
|
-
const provider = new FileContentProvider();
|
|
15
|
-
return provider.fetchRaw(assetPath);
|
|
16
|
-
}
|
|
17
9
|
try {
|
|
18
10
|
return await getReleaseAssetResponse(releaseId, assetPath);
|
|
19
11
|
}
|
package/dist/next/config.js
CHANGED
|
@@ -9,7 +9,7 @@ export function withGradialAci(nextConfig = {}, options = {}) {
|
|
|
9
9
|
const gradialRewrites = [
|
|
10
10
|
{
|
|
11
11
|
source: `${assetPrefix}/:path*`,
|
|
12
|
-
destination: `/
|
|
12
|
+
destination: `/api/aci-assets${assetPrefix}/:path*`,
|
|
13
13
|
},
|
|
14
14
|
];
|
|
15
15
|
if (!userRewrites)
|
|
@@ -1,11 +1,7 @@
|
|
|
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;
|
|
6
5
|
deploymentId?: string;
|
|
7
6
|
}
|
|
8
|
-
export declare function createGradialMiddleware(config
|
|
9
|
-
export declare const config: {
|
|
10
|
-
matcher: string[];
|
|
11
|
-
};
|
|
7
|
+
export declare function createGradialMiddleware(config: GradialMiddlewareConfig): (request: NextRequest) => Promise<NextResponse<unknown>>;
|
package/dist/next/middleware.js
CHANGED
|
@@ -3,7 +3,7 @@ import { getUncachedEdgeConfigValue } from './edge-config.js';
|
|
|
3
3
|
import { PREVIEW_TOKEN_COOKIE, PREVIEW_RELEASE_COOKIE, PREVIEW_HEADER, RELEASE_HEADER, extractPreviewToken, resolvePreviewContext, createPreviewCookies, previewSignKey, } from '../preview/core.js';
|
|
4
4
|
const ROUTE_HEADER = 'x-gradial-route';
|
|
5
5
|
const DISPATCH_HEADER = 'x-gradial-dispatch';
|
|
6
|
-
export function createGradialMiddleware(config
|
|
6
|
+
export function createGradialMiddleware(config) {
|
|
7
7
|
return async function gradialMiddleware(request) {
|
|
8
8
|
const url = request.nextUrl;
|
|
9
9
|
// ?leave_preview=1 → clear cookies, redirect to clean URL
|
|
@@ -38,11 +38,6 @@ export function createGradialMiddleware(config = {}) {
|
|
|
38
38
|
return response;
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
|
-
export const config = {
|
|
42
|
-
matcher: [
|
|
43
|
-
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
|
|
44
|
-
],
|
|
45
|
-
};
|
|
46
41
|
function gradialHeaders(headers) {
|
|
47
42
|
const next = new Headers(headers);
|
|
48
43
|
next.delete(RELEASE_HEADER);
|
|
@@ -64,7 +59,7 @@ async function resolvePreviewFromRequest(request, config) {
|
|
|
64
59
|
console.warn('[gradial/mw] GRADIAL_PREVIEW_SIGN_KEY not set — preview tokens will be ignored');
|
|
65
60
|
return null;
|
|
66
61
|
}
|
|
67
|
-
return await resolvePreviewContext({ token, siteId: config.siteId
|
|
62
|
+
return await resolvePreviewContext({ token, siteId: config.siteId, signKey });
|
|
68
63
|
}
|
|
69
64
|
function cookieMap(cookies) {
|
|
70
65
|
const map = new Map();
|
package/dist/next/page.d.ts
CHANGED
|
@@ -1,39 +1,38 @@
|
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
1
2
|
import type { Metadata } from 'next';
|
|
2
|
-
import { type KernelPage } from '../content/index.js';
|
|
3
|
-
|
|
3
|
+
import { type KernelPage, type KernelSiteConfig } from '../content/index.js';
|
|
4
|
+
export interface AciPageProps {
|
|
5
|
+
page: KernelPage;
|
|
6
|
+
siteConfig: KernelSiteConfig;
|
|
7
|
+
}
|
|
8
|
+
export interface CreateAciPageOptions {
|
|
9
|
+
/** The site's CmsPage component that renders chrome + content blocks. */
|
|
10
|
+
CmsPage: ComponentType<AciPageProps>;
|
|
11
|
+
}
|
|
4
12
|
type NextPageProps = {
|
|
5
13
|
params: Promise<{
|
|
6
14
|
slug?: string[];
|
|
7
15
|
}>;
|
|
8
16
|
};
|
|
9
|
-
export type SlotPropsFn = (slotName: string, page: KernelPage) => Record<string, unknown> | undefined;
|
|
10
|
-
export interface CreatePageOptions {
|
|
11
|
-
slotProps?: SlotPropsFn;
|
|
12
|
-
}
|
|
13
17
|
/**
|
|
14
18
|
* Creates the catch-all route handler for a Next.js ACI site.
|
|
15
19
|
*
|
|
16
20
|
* Usage in `src/app/[[...slug]]/page.tsx`:
|
|
17
21
|
*
|
|
18
22
|
* ```tsx
|
|
19
|
-
* import {
|
|
20
|
-
* import {
|
|
23
|
+
* import { createAciPage } from '@gradial/aci/next/page';
|
|
24
|
+
* import { CmsPage } from '@/components/CmsPage';
|
|
21
25
|
*
|
|
22
|
-
* const page =
|
|
26
|
+
* const page = createAciPage({ CmsPage });
|
|
23
27
|
* export const dynamic = page.dynamic;
|
|
24
28
|
* export const generateMetadata = page.generateMetadata;
|
|
25
29
|
* export default page.default;
|
|
26
30
|
* ```
|
|
27
31
|
*
|
|
28
32
|
* The SDK owns param normalization, content loading, metadata generation,
|
|
29
|
-
* notFound handling
|
|
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.
|
|
33
|
+
* and notFound handling. The site only provides its CmsPage component.
|
|
35
34
|
*/
|
|
36
|
-
export declare function
|
|
35
|
+
export declare function createAciPage({ CmsPage }: CreateAciPageOptions): {
|
|
37
36
|
dynamic: "force-dynamic";
|
|
38
37
|
generateMetadata: ({ params }: NextPageProps) => Promise<Metadata>;
|
|
39
38
|
default: ({ params }: NextPageProps) => Promise<import("react").JSX.Element>;
|
package/dist/next/page.js
CHANGED
|
@@ -1,33 +1,26 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { notFound } from 'next/navigation.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { routeMetadataForContent, resolveSlots, } from '../content/index.js';
|
|
6
|
-
import { renderBlocks } from './render.js';
|
|
3
|
+
import { PageNotFoundError, routeFromNextParams, getRenderInput, } from './server.js';
|
|
4
|
+
import { routeMetadataForContent, } from '../content/index.js';
|
|
7
5
|
/**
|
|
8
6
|
* Creates the catch-all route handler for a Next.js ACI site.
|
|
9
7
|
*
|
|
10
8
|
* Usage in `src/app/[[...slug]]/page.tsx`:
|
|
11
9
|
*
|
|
12
10
|
* ```tsx
|
|
13
|
-
* import {
|
|
14
|
-
* import {
|
|
11
|
+
* import { createAciPage } from '@gradial/aci/next/page';
|
|
12
|
+
* import { CmsPage } from '@/components/CmsPage';
|
|
15
13
|
*
|
|
16
|
-
* const page =
|
|
14
|
+
* const page = createAciPage({ CmsPage });
|
|
17
15
|
* export const dynamic = page.dynamic;
|
|
18
16
|
* export const generateMetadata = page.generateMetadata;
|
|
19
17
|
* export default page.default;
|
|
20
18
|
* ```
|
|
21
19
|
*
|
|
22
20
|
* The SDK owns param normalization, content loading, metadata generation,
|
|
23
|
-
* notFound handling
|
|
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.
|
|
21
|
+
* and notFound handling. The site only provides its CmsPage component.
|
|
29
22
|
*/
|
|
30
|
-
export function
|
|
23
|
+
export function createAciPage({ CmsPage }) {
|
|
31
24
|
const dynamic = 'force-dynamic';
|
|
32
25
|
async function generateMetadata({ params }) {
|
|
33
26
|
const route = await routeFromNextParams(params);
|
|
@@ -50,18 +43,7 @@ export function createPage(registry, options) {
|
|
|
50
43
|
const input = await getRenderInput(route);
|
|
51
44
|
if (!input.page || input.page.status !== 'published')
|
|
52
45
|
notFound();
|
|
53
|
-
|
|
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
|
-
}) }));
|
|
46
|
+
return _jsx(CmsPage, { page: input.page, siteConfig: input.siteConfig });
|
|
65
47
|
}
|
|
66
48
|
catch (error) {
|
|
67
49
|
if (error instanceof PageNotFoundError)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function PreviewBanner({ releaseId }) {
|
|
3
|
+
if (!releaseId)
|
|
4
|
+
return null;
|
|
5
|
+
const shortId = releaseId.length > 20
|
|
6
|
+
? `${releaseId.slice(0, 8)}\u2026${releaseId.slice(-6)}`
|
|
7
|
+
: releaseId;
|
|
8
|
+
return (_jsxs("div", { role: "alert", style: {
|
|
9
|
+
position: 'fixed',
|
|
10
|
+
top: 0,
|
|
11
|
+
left: 0,
|
|
12
|
+
right: 0,
|
|
13
|
+
zIndex: 9999,
|
|
14
|
+
display: 'flex',
|
|
15
|
+
alignItems: 'center',
|
|
16
|
+
justifyContent: 'center',
|
|
17
|
+
gap: '12px',
|
|
18
|
+
padding: '8px 16px',
|
|
19
|
+
backgroundColor: '#1a1a2e',
|
|
20
|
+
color: '#fff',
|
|
21
|
+
fontSize: '13px',
|
|
22
|
+
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
23
|
+
boxShadow: '0 2px 8px rgba(0,0,0,0.3)',
|
|
24
|
+
}, children: [_jsx("span", { "aria-hidden": "true", style: {
|
|
25
|
+
display: 'inline-flex',
|
|
26
|
+
alignItems: 'center',
|
|
27
|
+
justifyContent: 'center',
|
|
28
|
+
width: '18px',
|
|
29
|
+
height: '18px',
|
|
30
|
+
borderRadius: '50%',
|
|
31
|
+
backgroundColor: '#f59e0b',
|
|
32
|
+
color: '#1a1a2e',
|
|
33
|
+
fontWeight: 700,
|
|
34
|
+
fontSize: '11px',
|
|
35
|
+
flexShrink: 0,
|
|
36
|
+
}, children: "P" }), _jsx("span", { style: { fontWeight: 500 }, children: "Release Preview" }), _jsx("span", { style: {
|
|
37
|
+
fontFamily: 'monospace',
|
|
38
|
+
opacity: 0.7,
|
|
39
|
+
fontSize: '12px',
|
|
40
|
+
}, title: releaseId, children: shortId }), _jsx("a", { href: "?leave_preview=1", style: {
|
|
41
|
+
marginLeft: 'auto',
|
|
42
|
+
padding: '4px 12px',
|
|
43
|
+
borderRadius: '6px',
|
|
44
|
+
backgroundColor: '#dc2626',
|
|
45
|
+
color: '#fff',
|
|
46
|
+
textDecoration: 'none',
|
|
47
|
+
fontSize: '12px',
|
|
48
|
+
fontWeight: 600,
|
|
49
|
+
whiteSpace: 'nowrap',
|
|
50
|
+
}, children: "Exit Preview" })] }));
|
|
51
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export async function validatePreviewToken(token, signKey) {
|
|
2
|
+
try {
|
|
3
|
+
const parts = token.split('.');
|
|
4
|
+
if (parts.length !== 3)
|
|
5
|
+
return null;
|
|
6
|
+
const signingInput = `${parts[0]}.${parts[1]}`;
|
|
7
|
+
const signature = base64URLToBytes(parts[2]);
|
|
8
|
+
const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(signKey), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
9
|
+
const expected = new Uint8Array(await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(signingInput)));
|
|
10
|
+
if (!constantTimeEqual(signature, expected))
|
|
11
|
+
return null;
|
|
12
|
+
const payload = new TextDecoder().decode(base64URLToBytes(parts[1]));
|
|
13
|
+
return JSON.parse(payload);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function base64URLToBytes(value) {
|
|
20
|
+
const base64 = value.replace(/-/g, '+').replace(/_/g, '/');
|
|
21
|
+
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=');
|
|
22
|
+
const binary = atob(padded);
|
|
23
|
+
const bytes = new Uint8Array(binary.length);
|
|
24
|
+
for (let i = 0; i < binary.length; i++) {
|
|
25
|
+
bytes[i] = binary.charCodeAt(i);
|
|
26
|
+
}
|
|
27
|
+
return bytes;
|
|
28
|
+
}
|
|
29
|
+
function constantTimeEqual(left, right) {
|
|
30
|
+
if (left.length !== right.length)
|
|
31
|
+
return false;
|
|
32
|
+
let diff = 0;
|
|
33
|
+
for (let i = 0; i < left.length; i++) {
|
|
34
|
+
diff |= left[i] ^ right[i];
|
|
35
|
+
}
|
|
36
|
+
return diff === 0;
|
|
37
|
+
}
|
package/dist/next/server.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { FragmentNotFoundError,
|
|
1
|
+
import { FragmentNotFoundError, PageNotFoundError } from '../content/provider.js';
|
|
2
2
|
import type { RenderInput } from '../content/types.js';
|
|
3
|
-
export { FragmentNotFoundError, PageNotFoundError
|
|
3
|
+
export { FragmentNotFoundError, PageNotFoundError };
|
|
4
4
|
export interface GradialFetchConfig {
|
|
5
5
|
siteId?: string;
|
|
6
6
|
bucket?: string;
|
|
@@ -29,7 +29,6 @@ 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>;
|
|
33
32
|
export declare function getRoutes(config?: GradialFetchConfig): Promise<string[]>;
|
|
34
33
|
export declare function getRenderInput<TPage, TSite extends GradialSiteConfig = GradialSiteConfig>(route?: string, config?: GradialFetchConfig): Promise<GradialRenderInput<TPage, TSite>>;
|
|
35
34
|
export declare function getPageRuntimeRenderInput(): Promise<RenderInput | null>;
|