@gradial/aci 0.1.10 → 0.1.12

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/bin/aci ADDED
Binary file
package/dist/index.d.ts CHANGED
@@ -2,3 +2,4 @@ export * from './block-ref.js';
2
2
  export * from './define-component.js';
3
3
  export * from './define-layout.js';
4
4
  export * from './types/index.js';
5
+ export * from './render.js';
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ export * from './block-ref.js';
2
2
  export * from './define-component.js';
3
3
  export * from './define-layout.js';
4
4
  export * from './types/index.js';
5
+ export * from './render.js';
5
6
  // Note: content/ is NOT re-exported here because it imports node:path,
6
7
  // making it incompatible with client-side bundlers. Import server-only
7
8
  // content utilities from '@gradial/aci/content' explicitly.
@@ -1,2 +1,3 @@
1
1
  export { DevRefresh, type DevRefreshProps } from './dev-refresh.js';
2
2
  export { withGradial, type WithGradialOptions } from './config.js';
3
+ export { PreviewBanner } from './preview-banner.js';
@@ -1,2 +1,3 @@
1
1
  export { DevRefresh } from './dev-refresh.js';
2
2
  export { withGradial } from './config.js';
3
+ export { PreviewBanner } from './preview-banner.js';
@@ -6,6 +6,14 @@ const ROUTE_HEADER = 'x-gradial-route';
6
6
  const DISPATCH_HEADER = 'x-gradial-dispatch';
7
7
  export function createGradialMiddleware(config) {
8
8
  return async function gradialMiddleware(request) {
9
+ const url = request.nextUrl;
10
+ if (url.searchParams.get('leave_preview') === '1') {
11
+ const cleanUrl = new URL(url.pathname, url.origin);
12
+ const response = NextResponse.redirect(cleanUrl);
13
+ response.cookies.delete('aci_preview');
14
+ response.cookies.delete('aci_preview_release');
15
+ return response;
16
+ }
9
17
  const requestHeaders = gradialHeaders(request.headers);
10
18
  const preview = await releaseFromPreviewToken(request, config);
11
19
  if (preview.invalid) {
@@ -16,13 +24,28 @@ export function createGradialMiddleware(config) {
16
24
  return NextResponse.next({ request: { headers: requestHeaders } });
17
25
  }
18
26
  requestHeaders.set(RELEASE_HEADER, releaseId);
19
- requestHeaders.set(ROUTE_HEADER, request.nextUrl.pathname);
27
+ requestHeaders.set(ROUTE_HEADER, url.pathname);
20
28
  requestHeaders.set(DISPATCH_HEADER, 'ssr-page');
21
- return NextResponse.next({
22
- request: {
23
- headers: requestHeaders,
24
- },
25
- });
29
+ if (preview.releaseId) {
30
+ requestHeaders.set('x-gradial-preview', '1');
31
+ }
32
+ const response = NextResponse.next({ request: { headers: requestHeaders } });
33
+ const queryToken = url.searchParams.get('preview_token');
34
+ if (queryToken && preview.releaseId) {
35
+ response.cookies.set('aci_preview', queryToken, {
36
+ httpOnly: true,
37
+ sameSite: 'lax',
38
+ maxAge: 60 * 60,
39
+ path: '/',
40
+ });
41
+ response.cookies.set('aci_preview_release', preview.releaseId, {
42
+ httpOnly: false,
43
+ sameSite: 'lax',
44
+ maxAge: 60 * 60,
45
+ path: '/',
46
+ });
47
+ }
48
+ return response;
26
49
  };
27
50
  }
28
51
  function gradialHeaders(headers) {
@@ -106,9 +129,11 @@ async function releaseFromPreviewToken(request, config) {
106
129
  const token = previewTokenFromRequest(request);
107
130
  if (!token)
108
131
  return { releaseId: '', invalid: false };
109
- const signKey = config.previewSignKey || process.env.ACI_PREVIEW_SIGN_KEY || '';
110
- if (!signKey)
111
- return { releaseId: '', invalid: true };
132
+ const signKey = config.previewSignKey || process.env.ACI_PREVIEW_SIGN_KEY || process.env.GRADIAL_PREVIEW_SIGN_KEY || '';
133
+ if (!signKey) {
134
+ console.warn('[gradial] preview sign key not set (ACI_PREVIEW_SIGN_KEY or GRADIAL_PREVIEW_SIGN_KEY) — preview token ignored');
135
+ return { releaseId: '', invalid: false };
136
+ }
112
137
  const claims = await validatePreviewToken(token, signKey);
113
138
  if (!claims || claims.scope !== 'preview' || !claims.releaseId)
114
139
  return { releaseId: '', invalid: true };
@@ -0,0 +1 @@
1
+ export declare function PreviewBanner(): Promise<import("react").JSX.Element | null>;
@@ -0,0 +1,53 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { isPreviewMode } from './server.js';
3
+ export async function PreviewBanner() {
4
+ const releaseId = await isPreviewMode();
5
+ if (!releaseId)
6
+ return null;
7
+ const shortId = releaseId.length > 20
8
+ ? `${releaseId.slice(0, 8)}\u2026${releaseId.slice(-6)}`
9
+ : releaseId;
10
+ return (_jsxs("div", { role: "alert", style: {
11
+ position: 'fixed',
12
+ top: 0,
13
+ left: 0,
14
+ right: 0,
15
+ zIndex: 9999,
16
+ display: 'flex',
17
+ alignItems: 'center',
18
+ justifyContent: 'center',
19
+ gap: '12px',
20
+ padding: '8px 16px',
21
+ backgroundColor: '#1a1a2e',
22
+ color: '#fff',
23
+ fontSize: '13px',
24
+ fontFamily: 'system-ui, -apple-system, sans-serif',
25
+ boxShadow: '0 2px 8px rgba(0,0,0,0.3)',
26
+ }, children: [_jsx("span", { "aria-hidden": "true", style: {
27
+ display: 'inline-flex',
28
+ alignItems: 'center',
29
+ justifyContent: 'center',
30
+ width: '18px',
31
+ height: '18px',
32
+ borderRadius: '50%',
33
+ backgroundColor: '#f59e0b',
34
+ color: '#1a1a2e',
35
+ fontWeight: 700,
36
+ fontSize: '11px',
37
+ flexShrink: 0,
38
+ }, children: "P" }), _jsx("span", { style: { fontWeight: 500 }, children: "Release Preview" }), _jsx("span", { style: {
39
+ fontFamily: 'monospace',
40
+ opacity: 0.7,
41
+ fontSize: '12px',
42
+ }, title: releaseId, children: shortId }), _jsx("a", { href: "?leave_preview=1", style: {
43
+ marginLeft: 'auto',
44
+ padding: '4px 12px',
45
+ borderRadius: '6px',
46
+ backgroundColor: '#dc2626',
47
+ color: '#fff',
48
+ textDecoration: 'none',
49
+ fontSize: '12px',
50
+ fontWeight: 600,
51
+ whiteSpace: 'nowrap',
52
+ }, children: "Exit Preview" })] }));
53
+ }
@@ -32,6 +32,7 @@ export declare function getFragment<T = unknown>(name: string, config?: GradialF
32
32
  export declare function getRoutes(config?: GradialFetchConfig): Promise<string[]>;
33
33
  export declare function getRenderInput<TPage, TSite extends GradialSiteConfig = GradialSiteConfig>(route?: string, config?: GradialFetchConfig): Promise<GradialRenderInput<TPage, TSite>>;
34
34
  export declare function getPageRuntimeRenderInput(): Promise<RenderInput | null>;
35
+ export declare function isPreviewMode(): Promise<string>;
35
36
  export declare function routeFromNextParams(params?: Promise<{
36
37
  slug?: string[];
37
38
  }> | {
@@ -5,6 +5,7 @@ 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
+ const PREVIEW_HEADER = 'x-gradial-preview';
8
9
  export { FragmentNotFoundError, PageNotFoundError };
9
10
  export async function getPage(route = '', config = {}) {
10
11
  const runtimeInput = await getPageRuntimeRenderInput();
@@ -70,6 +71,16 @@ export async function getPageRuntimeRenderInput() {
70
71
  return null;
71
72
  }
72
73
  }
74
+ export async function isPreviewMode() {
75
+ try {
76
+ const current = await headers();
77
+ if (current.get(PREVIEW_HEADER) === '1') {
78
+ return current.get(RELEASE_HEADER) || '';
79
+ }
80
+ }
81
+ catch { }
82
+ return '';
83
+ }
73
84
  export async function routeFromNextParams(params) {
74
85
  if (!params)
75
86
  return '/';
@@ -0,0 +1,14 @@
1
+ import type { ComponentType, ReactNode } from 'react';
2
+ export interface BlockData {
3
+ id: string;
4
+ component: string;
5
+ props: Record<string, unknown>;
6
+ }
7
+ export type ComponentRegistry = Record<string, ComponentType<any>>;
8
+ export interface RenderBlocksOptions {
9
+ registry: ComponentRegistry;
10
+ compositionParams?: Record<string, unknown>;
11
+ slot?: string;
12
+ }
13
+ export declare function renderBlock(block: BlockData, registry: ComponentRegistry, compositionParams?: Record<string, unknown>): ReactNode;
14
+ export declare function renderBlocks(blocks: readonly BlockData[], opts: RenderBlocksOptions): ReactNode[];
package/dist/render.js ADDED
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PreviewBanner } from './next/preview-banner.js';
3
+ function BlockError({ component, id }) {
4
+ return (_jsxs("div", { style: {
5
+ padding: '1rem 1.25rem',
6
+ margin: '1rem 0',
7
+ background: '#fef2f2',
8
+ border: '2px solid #dc2626',
9
+ borderRadius: '6px',
10
+ fontFamily: 'ui-monospace, monospace',
11
+ fontSize: '13px',
12
+ lineHeight: 1.5,
13
+ color: '#991b1b',
14
+ }, children: [_jsx("strong", { children: "Unknown block:" }), " ", _jsx("code", { children: component }), _jsx("br", {}), _jsxs("span", { style: { color: '#b91c1c', opacity: 0.7 }, children: ["id: ", id] })] }));
15
+ }
16
+ export function renderBlock(block, registry, compositionParams) {
17
+ const Component = registry[block.component];
18
+ if (!Component) {
19
+ if (process.env.NODE_ENV !== 'production') {
20
+ return _jsx(BlockError, { component: block.component, id: block.id }, block.id);
21
+ }
22
+ return null;
23
+ }
24
+ return _jsx(Component, { ...block.props, ...compositionParams }, block.id);
25
+ }
26
+ export function renderBlocks(blocks, opts) {
27
+ const { registry, compositionParams, slot } = opts;
28
+ const rendered = blocks.map((block) => renderBlock(block, registry, compositionParams));
29
+ if (slot === 'main') {
30
+ return [_jsx(PreviewBanner, {}, "__gradial_preview"), ...rendered];
31
+ }
32
+ return rendered;
33
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradial/aci",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",