@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.
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 +40 -51
  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 -8
  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 -37
  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,116 @@
1
+ export const PREVIEW_HEADER = 'x-gradial-preview';
2
+ export const RELEASE_HEADER = 'x-gradial-release-id';
3
+ export const PREVIEW_TOKEN_COOKIE = 'aci_preview';
4
+ export const PREVIEW_RELEASE_COOKIE = 'aci_preview_release';
5
+ export async function validatePreviewToken(token, signKey) {
6
+ try {
7
+ const parts = token.split('.');
8
+ if (parts.length !== 3)
9
+ return null;
10
+ const signingInput = `${parts[0]}.${parts[1]}`;
11
+ const signature = base64URLToBytes(parts[2]);
12
+ const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(signKey), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
13
+ const expected = new Uint8Array(await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(signingInput)));
14
+ if (!constantTimeEqual(signature, expected))
15
+ return null;
16
+ const payload = new TextDecoder().decode(base64URLToBytes(parts[1]));
17
+ return JSON.parse(payload);
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ export function extractPreviewToken(source) {
24
+ const query = source.query?.get('preview_token');
25
+ if (query)
26
+ return query;
27
+ const cookies = source.cookies;
28
+ if (cookies) {
29
+ if (cookies instanceof Map) {
30
+ const cookie = cookies.get(PREVIEW_TOKEN_COOKIE);
31
+ if (cookie)
32
+ return cookie;
33
+ }
34
+ else {
35
+ const cookie = cookies[PREVIEW_TOKEN_COOKIE];
36
+ if (cookie)
37
+ return cookie;
38
+ }
39
+ }
40
+ const auth = source.authorization || '';
41
+ if (auth.startsWith('Bearer '))
42
+ return auth.slice('Bearer '.length);
43
+ return undefined;
44
+ }
45
+ export function previewSignKey() {
46
+ return process.env.GRADIAL_PREVIEW_SIGN_KEY || undefined;
47
+ }
48
+ export async function resolvePreviewContext(options) {
49
+ const token = options.token;
50
+ if (!token)
51
+ return { isPreview: false };
52
+ const signKey = options.signKey || previewSignKey();
53
+ if (!signKey) {
54
+ throw new Error('GRADIAL_PREVIEW_SIGN_KEY is not set; preview tokens cannot be validated.');
55
+ }
56
+ const claims = await validatePreviewToken(token, signKey);
57
+ if (!claims || claims.scope !== 'preview' || !claims.releaseId) {
58
+ return { isPreview: false };
59
+ }
60
+ if (claims.siteId && options.siteId && claims.siteId !== options.siteId) {
61
+ return { isPreview: false };
62
+ }
63
+ if (!claims.expiresAt || Date.parse(claims.expiresAt) <= Date.now()) {
64
+ return { isPreview: false };
65
+ }
66
+ return { isPreview: true, releaseId: claims.releaseId, token };
67
+ }
68
+ export function createPreviewCookies(token, releaseId) {
69
+ const common = {
70
+ sameSite: 'lax',
71
+ maxAge: 60 * 60,
72
+ path: '/',
73
+ };
74
+ return [
75
+ {
76
+ name: PREVIEW_TOKEN_COOKIE,
77
+ value: token,
78
+ options: { ...common, httpOnly: true },
79
+ },
80
+ {
81
+ name: PREVIEW_RELEASE_COOKIE,
82
+ value: releaseId,
83
+ options: { ...common, httpOnly: false },
84
+ },
85
+ ];
86
+ }
87
+ export function createPreviewBannerHTML(releaseId) {
88
+ const shortId = releaseId.length > 20
89
+ ? `${releaseId.slice(0, 8)}\u2026${releaseId.slice(-6)}`
90
+ : releaseId;
91
+ return `<div role="alert" style="position:fixed;top:0;left:0;right:0;z-index:9999;display:flex;align-items:center;justify-content:center;gap:12px;padding:8px 16px;background-color:#1a1a2e;color:#fff;font-size:13px;font-family:system-ui,-apple-system,sans-serif;box-shadow:0 2px 8px rgba(0,0,0,0.3);">
92
+ <span aria-hidden="true" style="display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;background-color:#f59e0b;color:#1a1a2e;font-weight:700;font-size:11px;flex-shrink:0;">P</span>
93
+ <span style="font-weight:500;">Release Preview</span>
94
+ <span style="font-family:monospace;opacity:0.7;font-size:12px;" title="${releaseId}">${shortId}</span>
95
+ <a href="?leave_preview=1" style="margin-left:auto;padding:4px 12px;border-radius:6px;background-color:#dc2626;color:#fff;text-decoration:none;font-size:12px;font-weight:600;white-space:nowrap;">Exit Preview</a>
96
+ </div>`;
97
+ }
98
+ function base64URLToBytes(value) {
99
+ const base64 = value.replace(/-/g, '+').replace(/_/g, '/');
100
+ const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=');
101
+ const binary = atob(padded);
102
+ const bytes = new Uint8Array(binary.length);
103
+ for (let i = 0; i < binary.length; i++) {
104
+ bytes[i] = binary.charCodeAt(i);
105
+ }
106
+ return bytes;
107
+ }
108
+ function constantTimeEqual(left, right) {
109
+ if (left.length !== right.length)
110
+ return false;
111
+ let diff = 0;
112
+ for (let i = 0; i < left.length; i++) {
113
+ diff |= left[i] ^ right[i];
114
+ }
115
+ return diff === 0;
116
+ }
@@ -7,6 +7,8 @@ export declare class FileContentProvider implements ContentProvider {
7
7
  getSiteConfig<T = unknown>(): Promise<T>;
8
8
  getPage<T = unknown>(route: string): Promise<T>;
9
9
  getFragment<T = unknown>(id: string): Promise<T>;
10
+ getLayout<T = unknown>(name: string): Promise<T>;
10
11
  manifest(): Promise<CompiledManifest>;
12
+ fetchRaw(key: string): Promise<Response>;
11
13
  private readJSON;
12
14
  }
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { FragmentNotFoundError, PageNotFoundError } from '../content/provider.js';
3
+ import { FragmentNotFoundError, LayoutNotFoundError, PageNotFoundError } from '../content/provider.js';
4
4
  import { normalizeRoute } from '../content/routes.js';
5
5
  export class FileContentProvider {
6
6
  root;
@@ -46,12 +46,72 @@ export class FileContentProvider {
46
46
  throw new FragmentNotFoundError(id, error);
47
47
  }
48
48
  }
49
+ async getLayout(name) {
50
+ const manifest = await this.manifest();
51
+ const layouts = manifest.layouts;
52
+ if (!layouts || !layouts[name]) {
53
+ throw new LayoutNotFoundError(name);
54
+ }
55
+ try {
56
+ return await this.readJSON(`layouts/${name}.json`);
57
+ }
58
+ catch (error) {
59
+ throw new LayoutNotFoundError(name, error);
60
+ }
61
+ }
49
62
  async manifest() {
50
63
  this.#manifest ??= this.readJSON('manifest.json');
51
64
  return await this.#manifest;
52
65
  }
66
+ async fetchRaw(key) {
67
+ const relativeKey = key.replace(/^\/+/, '');
68
+ const filePath = path.resolve(this.root, relativeKey);
69
+ const rel = path.relative(this.root, filePath);
70
+ if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) {
71
+ return new Response('not found', { status: 404 });
72
+ }
73
+ let body;
74
+ try {
75
+ body = await fs.readFile(filePath);
76
+ }
77
+ catch {
78
+ return new Response('not found', { status: 404 });
79
+ }
80
+ return new Response(new Uint8Array(body), {
81
+ status: 200,
82
+ headers: {
83
+ 'content-type': contentType(filePath),
84
+ 'cache-control': 'public, max-age=31536000, immutable',
85
+ },
86
+ });
87
+ }
53
88
  async readJSON(key) {
54
89
  const raw = await fs.readFile(path.join(this.root, key), 'utf8');
55
90
  return JSON.parse(raw);
56
91
  }
57
92
  }
93
+ function contentType(filePath) {
94
+ switch (path.extname(filePath).toLowerCase()) {
95
+ case '.webp':
96
+ return 'image/webp';
97
+ case '.svg':
98
+ return 'image/svg+xml';
99
+ case '.jpg':
100
+ case '.jpeg':
101
+ return 'image/jpeg';
102
+ case '.png':
103
+ return 'image/png';
104
+ case '.avif':
105
+ return 'image/avif';
106
+ case '.gif':
107
+ return 'image/gif';
108
+ case '.css':
109
+ return 'text/css';
110
+ case '.js':
111
+ return 'application/javascript';
112
+ case '.json':
113
+ return 'application/json';
114
+ default:
115
+ return 'application/octet-stream';
116
+ }
117
+ }
@@ -17,6 +17,7 @@ export declare class S3ContentProvider implements ContentProvider {
17
17
  getSiteConfig<T = unknown>(): Promise<T>;
18
18
  getPage<T = unknown>(route: string): Promise<T>;
19
19
  getFragment<T = unknown>(id: string): Promise<T>;
20
+ getLayout<T = unknown>(name: string): Promise<T>;
20
21
  manifest(): Promise<CompiledManifest>;
21
22
  fetchRaw(key: string): Promise<Response>;
22
23
  private getJSON;
@@ -1,5 +1,5 @@
1
1
  import crypto from 'node:crypto';
2
- import { FragmentNotFoundError, PageNotFoundError } from '../content/provider.js';
2
+ import { FragmentNotFoundError, LayoutNotFoundError, PageNotFoundError } from '../content/provider.js';
3
3
  import { normalizeRoute } from '../content/routes.js';
4
4
  // ---------------------------------------------------------------------------
5
5
  // S3 content provider — reads compiled content files from S3
@@ -45,6 +45,19 @@ export class S3ContentProvider {
45
45
  throw new FragmentNotFoundError(id, error);
46
46
  }
47
47
  }
48
+ async getLayout(name) {
49
+ const manifest = await this.manifest();
50
+ const layouts = manifest.layouts;
51
+ if (!layouts || !layouts[name]) {
52
+ throw new LayoutNotFoundError(name);
53
+ }
54
+ try {
55
+ return await this.getJSON(`layouts/${name}.json`);
56
+ }
57
+ catch (error) {
58
+ throw new LayoutNotFoundError(name, error);
59
+ }
60
+ }
48
61
  async manifest() {
49
62
  this.#manifest ??= this.getJSON('manifest.json');
50
63
  return await this.#manifest;
@@ -0,0 +1,15 @@
1
+ import type { ComponentContract, InferComponentProps } from './types/component.js';
2
+ export type AnyComponent = (abstract new (...args: any[]) => any) | ((props: any) => any) | Record<string, any>;
3
+ export interface RegistryEntry {
4
+ name: string;
5
+ component: AnyComponent;
6
+ contract: ComponentContract;
7
+ }
8
+ export type Registry = readonly RegistryEntry[];
9
+ type ValidatedEntry<T> = T extends readonly [infer C extends ComponentContract, infer Comp] ? Comp extends (props: InferComponentProps<C>) => any ? T : Comp extends (props: InferComponentProps<C> & infer _) => any ? T : Comp extends new (props: InferComponentProps<C>, ...args: any[]) => any ? T : Comp extends Record<string, any> ? T : never : never;
10
+ type ValidatedEntries<T extends readonly (readonly [ComponentContract, AnyComponent])[]> = {
11
+ [K in keyof T]: ValidatedEntry<T[K]>;
12
+ };
13
+ export declare function createRegistry<const T extends readonly (readonly [ComponentContract, AnyComponent])[]>(entries: ValidatedEntries<T> & T): Registry;
14
+ export declare function registryLookup(registry: Registry, name: string): AnyComponent | undefined;
15
+ export {};
@@ -0,0 +1,10 @@
1
+ export function createRegistry(entries) {
2
+ return entries.map(([contract, component]) => ({
3
+ name: contract.name,
4
+ component,
5
+ contract,
6
+ }));
7
+ }
8
+ export function registryLookup(registry, name) {
9
+ return registry.find((r) => r.name === name)?.component;
10
+ }
@@ -19,4 +19,7 @@ export declare function gradialEntries(): Promise<string[]>;
19
19
  export declare function getPageRuntimeRenderInput(event: {
20
20
  request: Request;
21
21
  }): RenderInput | null;
22
- export declare function gradialSvelteKit(options?: GradialContentWatchOptions): VitePluginLike;
22
+ export { createRegistry, registryLookup } from '../registry.js';
23
+ export type { Registry, RegistryEntry, AnyComponent } from '../registry.js';
24
+ export { getPreviewMode, createPreviewBannerHTML, createPreviewCookies, type PreviewContext, type PreviewCookie, } from './preview.js';
25
+ export declare function withGradialAci(options?: GradialContentWatchOptions): VitePluginLike;
@@ -32,9 +32,17 @@ export function getPageRuntimeRenderInput(event) {
32
32
  return getPendingRenderInputFromHeaders(event.request.headers);
33
33
  }
34
34
  // ---------------------------------------------------------------------------
35
+ // Registry helpers
36
+ // ---------------------------------------------------------------------------
37
+ export { createRegistry, registryLookup } from '../registry.js';
38
+ // ---------------------------------------------------------------------------
39
+ // Preview support
40
+ // ---------------------------------------------------------------------------
41
+ export { getPreviewMode, createPreviewBannerHTML, createPreviewCookies, } from './preview.js';
42
+ // ---------------------------------------------------------------------------
35
43
  // SvelteKit Vite plugin
36
44
  // ---------------------------------------------------------------------------
37
- export function gradialSvelteKit(options = {}) {
45
+ export function withGradialAci(options = {}) {
38
46
  const watchPlugin = gradialContentWatchPlugin(options);
39
47
  const damAssetPlugin = gradialDamAssetPlugin(options);
40
48
  return {
@@ -0,0 +1,5 @@
1
+ import { type PreviewContext, type PreviewCookie } from '../preview/core.js';
2
+ export declare function getPreviewMode(request: Request, siteId: string): Promise<PreviewContext>;
3
+ export declare function createPreviewBannerHTML(releaseId: string): string;
4
+ export declare function createPreviewCookies(token: string, releaseId: string): PreviewCookie[];
5
+ export type { PreviewContext, PreviewCookie };
@@ -0,0 +1,33 @@
1
+ import { extractPreviewToken, resolvePreviewContext, createPreviewBannerHTML as coreCreatePreviewBannerHTML, createPreviewCookies as coreCreatePreviewCookies, previewSignKey, } from '../preview/core.js';
2
+ export async function getPreviewMode(request, siteId) {
3
+ const url = new URL(request.url);
4
+ const token = extractPreviewToken({
5
+ query: url.searchParams,
6
+ cookies: parseCookieHeader(request.headers.get('cookie') || ''),
7
+ authorization: request.headers.get('authorization') || undefined,
8
+ });
9
+ if (!token)
10
+ return { isPreview: false };
11
+ return resolvePreviewContext({ token, siteId, signKey: previewSignKey() });
12
+ }
13
+ export function createPreviewBannerHTML(releaseId) {
14
+ return coreCreatePreviewBannerHTML(releaseId);
15
+ }
16
+ export function createPreviewCookies(token, releaseId) {
17
+ return coreCreatePreviewCookies(token, releaseId);
18
+ }
19
+ function parseCookieHeader(header) {
20
+ const cookies = new Map();
21
+ for (const part of header.split(';')) {
22
+ const trimmed = part.trim();
23
+ if (!trimmed)
24
+ continue;
25
+ const idx = trimmed.indexOf('=');
26
+ if (idx < 0)
27
+ continue;
28
+ const name = trimmed.slice(0, idx).trim();
29
+ const value = trimmed.slice(idx + 1).trim();
30
+ cookies.set(name, decodeURIComponent(value));
31
+ }
32
+ return cookies;
33
+ }
@@ -4,15 +4,18 @@ export interface FixtureContentProviderOptions {
4
4
  siteConfig?: KernelSiteConfig;
5
5
  pages?: Record<string, KernelPage>;
6
6
  fragments?: Record<string, unknown>;
7
+ layouts?: Record<string, unknown>;
7
8
  }
8
9
  export declare class FixtureContentProvider implements ContentProvider {
9
10
  private siteConfig;
10
11
  private pages;
11
12
  private fragments;
13
+ private layouts;
12
14
  constructor(options?: FixtureContentProviderOptions);
13
15
  getSiteConfig<T = unknown>(): Promise<T>;
14
16
  getPage<T = unknown>(route: string): Promise<T>;
15
17
  getFragment<T = unknown>(id: string): Promise<T>;
18
+ getLayout<T = unknown>(name: string): Promise<T>;
16
19
  listRoutes(): Promise<RouteEntry[]>;
17
20
  loadRenderInput(route?: string, options?: RenderInputOptions): Promise<RenderInput>;
18
21
  resolveRouteMetadata(route?: string): Promise<KernelRouteMetadata>;
@@ -1,13 +1,15 @@
1
- import { PageNotFoundError, FragmentNotFoundError, loadRenderInput, routeMetadataForContent, } from '../content/provider.js';
1
+ import { PageNotFoundError, FragmentNotFoundError, LayoutNotFoundError, loadRenderInput, routeMetadataForContent, } from '../content/provider.js';
2
2
  import { normalizeRoute } from '../content/routes.js';
3
3
  export class FixtureContentProvider {
4
4
  siteConfig;
5
5
  pages;
6
6
  fragments;
7
+ layouts;
7
8
  constructor(options = {}) {
8
9
  this.siteConfig = cloneFixture(options.siteConfig ?? defaultFixtureSiteConfig());
9
10
  this.pages = new Map();
10
11
  this.fragments = new Map();
12
+ this.layouts = new Map();
11
13
  const inputPages = options.pages ?? { '/': defaultFixturePage() };
12
14
  for (const [route, page] of Object.entries(inputPages)) {
13
15
  this.pages.set(normalizeRoute(route), cloneFixture(page));
@@ -17,6 +19,11 @@ export class FixtureContentProvider {
17
19
  this.fragments.set(id, cloneFixture(fragment));
18
20
  }
19
21
  }
22
+ if (options.layouts) {
23
+ for (const [name, layout] of Object.entries(options.layouts)) {
24
+ this.layouts.set(name, cloneFixture(layout));
25
+ }
26
+ }
20
27
  }
21
28
  async getSiteConfig() {
22
29
  return cloneFixture(this.siteConfig);
@@ -35,6 +42,13 @@ export class FixtureContentProvider {
35
42
  }
36
43
  return cloneFixture(fragment);
37
44
  }
45
+ async getLayout(name) {
46
+ const layout = this.layouts.get(name);
47
+ if (layout === undefined) {
48
+ throw new LayoutNotFoundError(name);
49
+ }
50
+ return cloneFixture(layout);
51
+ }
38
52
  async listRoutes() {
39
53
  return [...this.pages.entries()]
40
54
  .sort(([a], [b]) => a.localeCompare(b))
@@ -84,7 +98,6 @@ export function defaultFixturePage() {
84
98
  $type: 'page',
85
99
  status: 'published',
86
100
  layout: 'marketing',
87
- renderMode: 'static',
88
101
  metadata: {
89
102
  title: 'Fixture Home',
90
103
  description: 'Fixture home page.',
@@ -1,11 +1,6 @@
1
1
  import type { IslandMode, VaryDimension } from './render-mode.js';
2
2
  import type { ImageSlotContract } from './image.js';
3
3
  import type { z } from 'zod';
4
- export interface ComponentRenderModes {
5
- canStatic: boolean;
6
- canSSR: boolean;
7
- canClientIsland: boolean;
8
- }
9
4
  /**
10
5
  * A CMS-registered component — sync or async (server components).
11
6
  * Accepts content props derived from the Zod schema.
@@ -26,8 +21,6 @@ export interface ComponentDefinition<TSchema = unknown> {
26
21
  */
27
22
  component?: CmsComponentFn<any>;
28
23
  schema: TSchema;
29
- renderModes?: ComponentRenderModes;
30
- render?: ComponentRenderModes;
31
24
  defaultIslandMode?: IslandMode;
32
25
  varyDimensions?: VaryDimension[];
33
26
  vary?: VaryDimension[];
@@ -37,8 +30,6 @@ export interface ComponentContract<TSchema = unknown> {
37
30
  name: string;
38
31
  component?: CmsComponentFn<any>;
39
32
  schema: TSchema;
40
- renderModes: ComponentRenderModes;
41
- render: ComponentRenderModes;
42
33
  defaultIslandMode?: IslandMode;
43
34
  varyDimensions?: VaryDimension[];
44
35
  vary?: VaryDimension[];
@@ -9,8 +9,8 @@ export interface BareMetalConfig {
9
9
  publicDir?: string;
10
10
  };
11
11
  componentRegistry: string;
12
+ dataRegistry?: string;
12
13
  layoutRegistry: string;
13
- rendererEntry: string;
14
14
  capabilities: RenderCapabilities;
15
15
  routes: RouteConfig;
16
16
  dam?: DAMConfig;
@@ -0,0 +1,19 @@
1
+ import { z } from 'zod';
2
+ type InferDataProps<TSchema> = TSchema extends z.ZodType<infer T> ? T : Record<string, unknown>;
3
+ export interface DataModelDefinition<TSchema = unknown, TType extends string = string> {
4
+ type: TType;
5
+ schema: TSchema;
6
+ }
7
+ export interface DataModelContract<TSchema = unknown, TType extends string = string> {
8
+ type: TType;
9
+ schema: TSchema;
10
+ }
11
+ export interface DataEntry<TProps = Record<string, unknown>, TType extends string = string> {
12
+ $type: TType;
13
+ props: TProps;
14
+ }
15
+ export type InferDataModelProps<TContract> = TContract extends DataModelContract<infer TSchema, string> ? InferDataProps<TSchema> : Record<string, unknown>;
16
+ export type InferDataModelType<TContract> = TContract extends DataModelContract<unknown, infer TType> ? TType : string;
17
+ export type DataModelRef<TContract extends DataModelContract> = DataEntry<InferDataModelProps<TContract>, InferDataModelType<TContract>>;
18
+ export declare function dataModelRef<TContract extends DataModelContract<z.ZodTypeAny>>(contract: TContract): z.ZodType<DataModelRef<TContract>>;
19
+ export {};
@@ -0,0 +1,11 @@
1
+ import { z } from 'zod';
2
+ const dataRefPattern = /^\$ref:data\/[A-Za-z0-9][A-Za-z0-9_./-]*$/;
3
+ export function dataModelRef(contract) {
4
+ return z.union([
5
+ z.string().regex(dataRefPattern, `expected a $ref:data/... pointer for ${contract.type}`),
6
+ z.object({
7
+ $type: z.literal(contract.type),
8
+ props: contract.schema,
9
+ }),
10
+ ]).describe(`data model reference: ${contract.type}`);
11
+ }
@@ -17,7 +17,7 @@ export interface GradialImage {
17
17
  versionId: string;
18
18
  alt: string;
19
19
  fallback: ImageSource;
20
- sources: PictureSource[];
20
+ sources: PictureSource[] | null;
21
21
  }
22
22
  export interface ImageSlotContract {
23
23
  outputs: SlotOutput[];
@@ -26,7 +26,7 @@ export function renderImageHTML(image, attrs = {}) {
26
26
  height: image.fallback.height > 0 ? image.fallback.height : undefined,
27
27
  });
28
28
  const img = `<img${imgAttrs}>`;
29
- if (!image.sources.length) {
29
+ if (!image.sources?.length) {
30
30
  return img;
31
31
  }
32
32
  const sources = image.sources.map((source) => `<source${renderAttrs({
@@ -1,5 +1,6 @@
1
1
  export * from './config.js';
2
2
  export * from './component.js';
3
+ export * from './data.js';
3
4
  export * from './layout.js';
4
5
  export * from './page.js';
5
6
  export * from './renderer.js';
@@ -1,5 +1,6 @@
1
1
  export * from './config.js';
2
2
  export * from './component.js';
3
+ export * from './data.js';
3
4
  export * from './layout.js';
4
5
  export * from './page.js';
5
6
  export * from './renderer.js';
@@ -1,11 +1,10 @@
1
- import type { RenderMode, VaryDimension } from './render-mode.js';
1
+ import type { VaryDimension } from './render-mode.js';
2
2
  export interface PageDocument {
3
3
  path: string;
4
4
  layout: string;
5
5
  locale: string;
6
6
  metadata: PageMetadata;
7
7
  regions: Record<string, RegionNode>;
8
- renderMode: RenderMode;
9
8
  }
10
9
  export interface PageMetadata {
11
10
  title: string;
@@ -1,3 +1,2 @@
1
- export type RenderMode = 'static' | 'static-with-fragments' | 'prerender-on-demand' | 'ssr-page' | 'ssr-island' | 'client-island';
2
1
  export type IslandMode = 'ssr' | 'client';
3
2
  export type VaryDimension = 'geo:country' | 'geo:region' | 'geo:city' | `cookie:${string}` | `header:${string}` | 'locale' | `query:${string}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradial/aci",
3
- "version": "0.1.19",
3
+ "version": "0.1.20-preview.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -35,10 +35,18 @@
35
35
  "types": "./dist/content/contract.d.ts",
36
36
  "import": "./dist/content/contract.js"
37
37
  },
38
+ "./content/cache": {
39
+ "types": "./dist/content/cache.d.ts",
40
+ "import": "./dist/content/cache.js"
41
+ },
38
42
  "./content/validation": {
39
43
  "types": "./dist/content/validation.d.ts",
40
44
  "import": "./dist/content/validation.js"
41
45
  },
46
+ "./content/cache": {
47
+ "types": "./dist/content/cache.d.ts",
48
+ "import": "./dist/content/cache.js"
49
+ },
42
50
  "./content/tailwind-validator": {
43
51
  "types": "./dist/content/tailwind-validator.d.ts",
44
52
  "import": "./dist/content/tailwind-validator.js"
@@ -71,11 +79,31 @@
71
79
  "types": "./dist/astro/index.d.ts",
72
80
  "import": "./dist/astro/index.js"
73
81
  },
82
+ "./preview": {
83
+ "types": "./dist/preview/core.d.ts",
84
+ "import": "./dist/preview/core.js",
85
+ "default": "./dist/preview/core.js"
86
+ },
74
87
  "./next": {
75
88
  "types": "./dist/next/index.d.ts",
76
89
  "import": "./dist/next/index.js",
77
90
  "default": "./dist/next/index.js"
78
91
  },
92
+ "./next/page": {
93
+ "types": "./dist/next/page.d.ts",
94
+ "import": "./dist/next/page.js",
95
+ "default": "./dist/next/page.js"
96
+ },
97
+ "./next/root-layout": {
98
+ "types": "./dist/next/root-layout.d.ts",
99
+ "import": "./dist/next/root-layout.js",
100
+ "default": "./dist/next/root-layout.js"
101
+ },
102
+ "./next/render": {
103
+ "types": "./dist/next/render.d.ts",
104
+ "import": "./dist/next/render.js",
105
+ "default": "./dist/next/render.js"
106
+ },
79
107
  "./next/server": {
80
108
  "types": "./dist/next/server.d.ts",
81
109
  "import": "./dist/next/server.js",
@@ -110,15 +138,16 @@
110
138
  "build": "tsc -p tsconfig.json",
111
139
  "build:cli": "cd ../.. && ./scripts/build-cli.sh",
112
140
  "compile-registry": "tsx src/cli/compile-registry.mjs",
113
- "prepack": "npm run build"
141
+ "prepack": "pnpm run build && pnpm run build:cli"
114
142
  },
115
- "overrides": {
116
- "postcss": "^8.5.10"
143
+ "dependencies": {
144
+ "ws": "^8.18.0"
117
145
  },
118
146
  "peerDependencies": {
119
147
  "@vercel/edge-config": "^1.4.3",
120
- "next": "^15.5.0",
121
- "react": "^19.0.0",
148
+ "next": "^15.0.0",
149
+ "react": "^18.0.0 || ^19.0.0",
150
+ "react-dom": "^18.0.0 || ^19.0.0",
122
151
  "zod": "^4.0.0"
123
152
  },
124
153
  "peerDependenciesMeta": {
@@ -130,16 +159,21 @@
130
159
  },
131
160
  "react": {
132
161
  "optional": true
162
+ },
163
+ "react-dom": {
164
+ "optional": true
133
165
  }
134
166
  },
135
167
  "devDependencies": {
136
168
  "@types/node": "^24.10.1",
137
169
  "@types/react": "^19.0.0",
170
+ "@types/ws": "^8.18.0",
138
171
  "@vercel/edge-config": "^1.4.3",
139
172
  "next": "^15.5.6",
140
173
  "react": "^19.0.0",
141
174
  "tsx": "^4.20.0",
142
175
  "typescript": "^5.9.3",
176
+ "ws": "^8.18.0",
143
177
  "zod": "^4.0.0"
144
178
  }
145
179
  }