@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.
Files changed (57) hide show
  1. package/README.md +10 -8
  2. package/bin/aci +0 -0
  3. package/dist/astro/index.d.ts +0 -2
  4. package/dist/astro/index.js +0 -4
  5. package/dist/content/cache.test.d.ts +1 -0
  6. package/dist/content/cache.test.js +86 -0
  7. package/dist/content/index.d.ts +0 -1
  8. package/dist/content/index.js +0 -1
  9. package/dist/content/provider.d.ts +0 -19
  10. package/dist/content/provider.js +0 -7
  11. package/dist/content/types.d.ts +1 -1
  12. package/dist/content/validation.js +1 -0
  13. package/dist/define-component.js +6 -0
  14. package/dist/define-layout.d.ts +0 -2
  15. package/dist/define-layout.js +0 -3
  16. package/dist/index.d.ts +0 -1
  17. package/dist/index.js +0 -1
  18. package/dist/next/asset-route.d.ts +0 -2
  19. package/dist/next/asset-route.js +2 -10
  20. package/dist/next/config.js +1 -1
  21. package/dist/next/middleware.d.ts +2 -6
  22. package/dist/next/middleware.js +2 -7
  23. package/dist/next/page.d.ts +15 -16
  24. package/dist/next/page.js +9 -27
  25. package/dist/next/preview-banner.d.ts +4 -0
  26. package/dist/next/preview-banner.js +51 -0
  27. package/dist/next/preview.d.ts +7 -0
  28. package/dist/next/preview.js +37 -0
  29. package/dist/next/server.d.ts +2 -3
  30. package/dist/next/server.js +2 -12
  31. package/dist/providers/file.d.ts +0 -2
  32. package/dist/providers/file.js +1 -61
  33. package/dist/providers/s3.d.ts +0 -1
  34. package/dist/providers/s3.js +1 -14
  35. package/dist/sveltekit/index.d.ts +0 -2
  36. package/dist/sveltekit/index.js +0 -4
  37. package/dist/testing/index.d.ts +0 -3
  38. package/dist/testing/index.js +2 -15
  39. package/dist/types/component.d.ts +9 -0
  40. package/dist/types/config.d.ts +1 -0
  41. package/dist/types/page.d.ts +2 -1
  42. package/dist/types/render-mode.d.ts +1 -0
  43. package/package.json +7 -18
  44. package/src/cli/compile-registry.mjs +18 -127
  45. package/src/cli/verify-renderer.mjs +73 -0
  46. package/src/types/component.ts +10 -0
  47. package/src/types/config.ts +1 -0
  48. package/src/types/page.ts +2 -1
  49. package/src/types/render-mode.ts +8 -0
  50. package/dist/content/resolve-slots.d.ts +0 -9
  51. package/dist/content/resolve-slots.js +0 -24
  52. package/dist/next/render.d.ts +0 -5
  53. package/dist/next/render.js +0 -15
  54. package/dist/next/root-layout.d.ts +0 -4
  55. package/dist/next/root-layout.js +0 -5
  56. package/dist/registry.d.ts +0 -15
  57. package/dist/registry.js +0 -10
@@ -1,12 +1,12 @@
1
1
  import { headers } from 'next/headers.js';
2
- import { FragmentNotFoundError, LayoutNotFoundError, PageNotFoundError, } from '../content/provider.js';
2
+ import { FragmentNotFoundError, 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, LayoutNotFoundError };
9
+ export { FragmentNotFoundError, PageNotFoundError };
10
10
  export async function getPage(route = '', config = {}) {
11
11
  const runtimeInput = await getPageRuntimeRenderInput();
12
12
  if (runtimeInput)
@@ -31,16 +31,6 @@ 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
- }
44
34
  export async function getRoutes(config = {}) {
45
35
  const provider = await resolveProviderWithFallback(config);
46
36
  const routes = await provider.listRoutes();
@@ -7,8 +7,6 @@ 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>;
11
10
  manifest(): Promise<CompiledManifest>;
12
- fetchRaw(key: string): Promise<Response>;
13
11
  private readJSON;
14
12
  }
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { FragmentNotFoundError, LayoutNotFoundError, PageNotFoundError } from '../content/provider.js';
3
+ import { FragmentNotFoundError, PageNotFoundError } from '../content/provider.js';
4
4
  import { normalizeRoute } from '../content/routes.js';
5
5
  export class FileContentProvider {
6
6
  root;
@@ -46,72 +46,12 @@ 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
- }
62
49
  async manifest() {
63
50
  this.#manifest ??= this.readJSON('manifest.json');
64
51
  return await this.#manifest;
65
52
  }
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
- }
88
53
  async readJSON(key) {
89
54
  const raw = await fs.readFile(path.join(this.root, key), 'utf8');
90
55
  return JSON.parse(raw);
91
56
  }
92
57
  }
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,7 +17,6 @@ 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>;
21
20
  manifest(): Promise<CompiledManifest>;
22
21
  fetchRaw(key: string): Promise<Response>;
23
22
  private getJSON;
@@ -1,5 +1,5 @@
1
1
  import crypto from 'node:crypto';
2
- import { FragmentNotFoundError, LayoutNotFoundError, PageNotFoundError } from '../content/provider.js';
2
+ import { FragmentNotFoundError, 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,19 +45,6 @@ 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
- }
61
48
  async manifest() {
62
49
  this.#manifest ??= this.getJSON('manifest.json');
63
50
  return await this.#manifest;
@@ -19,7 +19,5 @@ export declare function gradialEntries(): Promise<string[]>;
19
19
  export declare function getPageRuntimeRenderInput(event: {
20
20
  request: Request;
21
21
  }): RenderInput | null;
22
- export { createRegistry, registryLookup } from '../registry.js';
23
- export type { Registry, RegistryEntry, AnyComponent } from '../registry.js';
24
22
  export { getPreviewMode, createPreviewBannerHTML, createPreviewCookies, type PreviewContext, type PreviewCookie, } from './preview.js';
25
23
  export declare function withGradialAci(options?: GradialContentWatchOptions): VitePluginLike;
@@ -32,10 +32,6 @@ 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
35
  // Preview support
40
36
  // ---------------------------------------------------------------------------
41
37
  export { getPreviewMode, createPreviewBannerHTML, createPreviewCookies, } from './preview.js';
@@ -4,18 +4,15 @@ export interface FixtureContentProviderOptions {
4
4
  siteConfig?: KernelSiteConfig;
5
5
  pages?: Record<string, KernelPage>;
6
6
  fragments?: Record<string, unknown>;
7
- layouts?: Record<string, unknown>;
8
7
  }
9
8
  export declare class FixtureContentProvider implements ContentProvider {
10
9
  private siteConfig;
11
10
  private pages;
12
11
  private fragments;
13
- private layouts;
14
12
  constructor(options?: FixtureContentProviderOptions);
15
13
  getSiteConfig<T = unknown>(): Promise<T>;
16
14
  getPage<T = unknown>(route: string): Promise<T>;
17
15
  getFragment<T = unknown>(id: string): Promise<T>;
18
- getLayout<T = unknown>(name: string): Promise<T>;
19
16
  listRoutes(): Promise<RouteEntry[]>;
20
17
  loadRenderInput(route?: string, options?: RenderInputOptions): Promise<RenderInput>;
21
18
  resolveRouteMetadata(route?: string): Promise<KernelRouteMetadata>;
@@ -1,15 +1,13 @@
1
- import { PageNotFoundError, FragmentNotFoundError, LayoutNotFoundError, loadRenderInput, routeMetadataForContent, } from '../content/provider.js';
1
+ import { PageNotFoundError, FragmentNotFoundError, 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;
8
7
  constructor(options = {}) {
9
8
  this.siteConfig = cloneFixture(options.siteConfig ?? defaultFixtureSiteConfig());
10
9
  this.pages = new Map();
11
10
  this.fragments = new Map();
12
- this.layouts = new Map();
13
11
  const inputPages = options.pages ?? { '/': defaultFixturePage() };
14
12
  for (const [route, page] of Object.entries(inputPages)) {
15
13
  this.pages.set(normalizeRoute(route), cloneFixture(page));
@@ -19,11 +17,6 @@ export class FixtureContentProvider {
19
17
  this.fragments.set(id, cloneFixture(fragment));
20
18
  }
21
19
  }
22
- if (options.layouts) {
23
- for (const [name, layout] of Object.entries(options.layouts)) {
24
- this.layouts.set(name, cloneFixture(layout));
25
- }
26
- }
27
20
  }
28
21
  async getSiteConfig() {
29
22
  return cloneFixture(this.siteConfig);
@@ -42,13 +35,6 @@ export class FixtureContentProvider {
42
35
  }
43
36
  return cloneFixture(fragment);
44
37
  }
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
- }
52
38
  async listRoutes() {
53
39
  return [...this.pages.entries()]
54
40
  .sort(([a], [b]) => a.localeCompare(b))
@@ -98,6 +84,7 @@ export function defaultFixturePage() {
98
84
  $type: 'page',
99
85
  status: 'published',
100
86
  layout: 'marketing',
87
+ renderMode: 'static',
101
88
  metadata: {
102
89
  title: 'Fixture Home',
103
90
  description: 'Fixture home page.',
@@ -1,6 +1,11 @@
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
+ }
4
9
  /**
5
10
  * A CMS-registered component — sync or async (server components).
6
11
  * Accepts content props derived from the Zod schema.
@@ -21,6 +26,8 @@ export interface ComponentDefinition<TSchema = unknown> {
21
26
  */
22
27
  component?: CmsComponentFn<any>;
23
28
  schema: TSchema;
29
+ renderModes?: ComponentRenderModes;
30
+ render?: ComponentRenderModes;
24
31
  defaultIslandMode?: IslandMode;
25
32
  varyDimensions?: VaryDimension[];
26
33
  vary?: VaryDimension[];
@@ -30,6 +37,8 @@ export interface ComponentContract<TSchema = unknown> {
30
37
  name: string;
31
38
  component?: CmsComponentFn<any>;
32
39
  schema: TSchema;
40
+ renderModes: ComponentRenderModes;
41
+ render: ComponentRenderModes;
33
42
  defaultIslandMode?: IslandMode;
34
43
  varyDimensions?: VaryDimension[];
35
44
  vary?: VaryDimension[];
@@ -11,6 +11,7 @@ export interface BareMetalConfig {
11
11
  componentRegistry: string;
12
12
  dataRegistry?: string;
13
13
  layoutRegistry: string;
14
+ rendererEntry: string;
14
15
  capabilities: RenderCapabilities;
15
16
  routes: RouteConfig;
16
17
  dam?: DAMConfig;
@@ -1,10 +1,11 @@
1
- import type { VaryDimension } from './render-mode.js';
1
+ import type { RenderMode, 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;
8
9
  }
9
10
  export interface PageMetadata {
10
11
  title: string;
@@ -1,2 +1,3 @@
1
+ export type RenderMode = 'static' | 'static-with-fragments' | 'prerender-on-demand' | 'ssr-page' | 'ssr-island' | 'client-island';
1
2
  export type IslandMode = 'ssr' | 'client';
2
3
  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.20-preview.1",
3
+ "version": "0.1.20-preview.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -94,16 +94,6 @@
94
94
  "import": "./dist/next/page.js",
95
95
  "default": "./dist/next/page.js"
96
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
- },
107
97
  "./next/server": {
108
98
  "types": "./dist/next/server.d.ts",
109
99
  "import": "./dist/next/server.js",
@@ -138,16 +128,18 @@
138
128
  "build": "tsc -p tsconfig.json",
139
129
  "build:cli": "cd ../.. && ./scripts/build-cli.sh",
140
130
  "compile-registry": "tsx src/cli/compile-registry.mjs",
141
- "prepack": "pnpm run build && pnpm run build:cli"
131
+ "prepack": "npm run build"
132
+ },
133
+ "overrides": {
134
+ "postcss": "^8.5.10"
142
135
  },
143
136
  "dependencies": {
144
137
  "ws": "^8.18.0"
145
138
  },
146
139
  "peerDependencies": {
147
140
  "@vercel/edge-config": "^1.4.3",
148
- "next": "^15.0.0",
149
- "react": "^18.0.0 || ^19.0.0",
150
- "react-dom": "^18.0.0 || ^19.0.0",
141
+ "next": "^15.5.0",
142
+ "react": "^19.0.0",
151
143
  "zod": "^4.0.0"
152
144
  },
153
145
  "peerDependenciesMeta": {
@@ -159,9 +151,6 @@
159
151
  },
160
152
  "react": {
161
153
  "optional": true
162
- },
163
- "react-dom": {
164
- "optional": true
165
154
  }
166
155
  },
167
156
  "devDependencies": {
@@ -52,11 +52,22 @@ async function compileRegistry(options) {
52
52
  const schemaPath = path.join(outDir, schemaFile);
53
53
  await writeFile(schemaPath, JSON.stringify(schema, null, 2) + '\n');
54
54
  schemaFiles.push(schemaPath);
55
+ const renderModes = component.renderModes ?? component.render;
55
56
  const varyDimensions = component.varyDimensions ?? component.vary;
56
57
  const imageSlots = normalizeImageSlots(component.imageSlots);
57
58
  componentEntries.push({
58
59
  name: component.name,
59
60
  schemaFile,
61
+ render: {
62
+ static: Boolean(renderModes.canStatic ?? renderModes.static),
63
+ ssr: Boolean(renderModes.canSSR ?? renderModes.ssr),
64
+ clientIsland: Boolean(renderModes.canClientIsland ?? renderModes.clientIsland),
65
+ },
66
+ renderModes: {
67
+ canStatic: Boolean(renderModes.canStatic ?? renderModes.static),
68
+ canSSR: Boolean(renderModes.canSSR ?? renderModes.ssr),
69
+ canClientIsland: Boolean(renderModes.canClientIsland ?? renderModes.clientIsland),
70
+ },
60
71
  ...(component.defaultIslandMode ? { defaultIslandMode: component.defaultIslandMode } : {}),
61
72
  ...(varyDimensions?.length ? { vary: varyDimensions, varyDimensions } : {}),
62
73
  ...(Object.keys(imageSlots).length ? { imageSlots } : {}),
@@ -106,10 +117,6 @@ async function compileRegistry(options) {
106
117
  };
107
118
  const registryPath = path.join(outDir, 'registry.json');
108
119
  await writeFile(registryPath, JSON.stringify(registry, null, 2) + '\n');
109
-
110
- const pageSchemaPath = await generatePageSchema(schemaDir, componentEntries, outDir);
111
- schemaFiles.push(pageSchemaPath);
112
-
113
120
  return { registry: registryPath, schemas: schemaFiles };
114
121
  }
115
122
 
@@ -143,6 +150,12 @@ function validateComponent(component, index, seen) {
143
150
  if (!component.schema) throw new Error(`components[${index}].schema is required`);
144
151
  validateZodSubset(component.schema, `components[${index}].schema`);
145
152
  validateImageSlotSchemaMatch(component, index);
153
+ const renderModes = component.renderModes ?? component.render;
154
+ if (!renderModes) throw new Error(`components[${index}].renderModes is required`);
155
+ const enabled = Boolean(renderModes.canStatic ?? renderModes.static)
156
+ || Boolean(renderModes.canSSR ?? renderModes.ssr)
157
+ || Boolean(renderModes.canClientIsland ?? renderModes.clientIsland);
158
+ if (!enabled) throw new Error(`components[${index}].renderModes must enable at least one mode`);
146
159
  }
147
160
 
148
161
  function validateImageSlotSchemaMatch(component, index) {
@@ -336,11 +349,7 @@ function validateLayout(layout, index, seen) {
336
349
  seenSlots.add(slot.name);
337
350
  slots.push({ name: slot.name, required: Boolean(slot.required) });
338
351
  }
339
- const entry = { name: layout.name, slots };
340
- if (layout.defaults && typeof layout.defaults === 'object') {
341
- entry.defaults = layout.defaults;
342
- }
343
- return entry;
352
+ return { name: layout.name, slots };
344
353
  }
345
354
 
346
355
  function compileSchema(schema, name) {
@@ -359,124 +368,6 @@ function compileSchema(schema, name) {
359
368
  };
360
369
  }
361
370
 
362
- async function generatePageSchema(schemaDir, componentEntries, outDir) {
363
- const { readFile } = await import('node:fs/promises');
364
- const defs = {
365
- baseBlock: {
366
- type: 'object',
367
- required: ['id', 'component', 'props'],
368
- additionalProperties: true,
369
- properties: {
370
- id: { type: 'string', minLength: 1 },
371
- component: { type: 'string' },
372
- props: { type: 'object' },
373
- },
374
- },
375
- damAssetRef: {
376
- type: 'object',
377
- required: ['$type', 'assetId'],
378
- additionalProperties: true,
379
- properties: {
380
- $type: { const: 'dam.assetRef' },
381
- assetId: { type: 'string', minLength: 1 },
382
- version: { type: 'string', minLength: 1 },
383
- alt: { type: 'string' },
384
- },
385
- },
386
- };
387
- const oneOf = [];
388
- for (const entry of componentEntries) {
389
- const schemaPath = path.join(outDir, entry.schemaFile);
390
- const raw = await readFile(schemaPath, 'utf-8');
391
- const componentSchema = JSON.parse(raw);
392
- const { $schema: _s, title: _t, ...propsSchema } = componentSchema;
393
- replaceGradialImagesWithDamAssetRef(propsSchema, defs.damAssetRef);
394
- const defName = `${entry.name}Block`;
395
- defs[defName] = {
396
- allOf: [
397
- { $ref: '#/$defs/baseBlock' },
398
- {
399
- properties: {
400
- component: { const: entry.name },
401
- props: propsSchema,
402
- },
403
- },
404
- ],
405
- };
406
- oneOf.push({ $ref: `#/$defs/${defName}` });
407
- }
408
- const pageSchema = {
409
- $schema: 'https://json-schema.org/draft/2020-12/schema',
410
- $id: 'https://baremetal.local/schemas/page.schema.json',
411
- title: 'bare-metal Page',
412
- type: 'object',
413
- required: ['$type', 'id', 'status', 'layout', 'metadata', 'regions'],
414
- additionalProperties: true,
415
- properties: {
416
- $type: { const: 'page' },
417
- id: { type: 'string', minLength: 1 },
418
- status: { type: 'string', minLength: 1 },
419
- layout: { type: 'string', minLength: 1 },
420
- metadata: {
421
- type: 'object',
422
- required: ['title'],
423
- additionalProperties: true,
424
- properties: {
425
- title: { type: 'string', minLength: 1 },
426
- description: { type: 'string' },
427
- canonical: { type: 'string' },
428
- },
429
- },
430
- regions: {
431
- type: 'object',
432
- additionalProperties: {
433
- type: 'array',
434
- items: { oneOf },
435
- },
436
- },
437
- },
438
- $defs: defs,
439
- };
440
- const pageSchemaPath = path.join(schemaDir, 'page.schema.json');
441
- await writeFile(pageSchemaPath, JSON.stringify(pageSchema, null, 2) + '\n');
442
- return pageSchemaPath;
443
- }
444
-
445
- function replaceGradialImagesWithDamAssetRef(schema, damAssetRef) {
446
- if (!schema || typeof schema !== 'object') return schema;
447
- if (schema.$ref) return;
448
- if (Array.isArray(schema.anyOf)) {
449
- for (const s of schema.anyOf) replaceGradialImagesWithDamAssetRef(s, damAssetRef);
450
- }
451
- if (Array.isArray(schema.allOf)) {
452
- for (const s of schema.allOf) replaceGradialImagesWithDamAssetRef(s, damAssetRef);
453
- }
454
- if (Array.isArray(schema.oneOf)) {
455
- for (const s of schema.oneOf) replaceGradialImagesWithDamAssetRef(s, damAssetRef);
456
- }
457
- if (schema.properties) {
458
- for (const [key, value] of Object.entries(schema.properties)) {
459
- if (isJsonGradialImage(value)) {
460
- schema.properties[key] = { $ref: '#/$defs/damAssetRef' };
461
- } else {
462
- replaceGradialImagesWithDamAssetRef(value, damAssetRef);
463
- }
464
- }
465
- }
466
- if (schema.items) {
467
- if (isJsonGradialImage(schema.items)) {
468
- schema.items = { $ref: '#/$defs/damAssetRef' };
469
- } else {
470
- replaceGradialImagesWithDamAssetRef(schema.items, damAssetRef);
471
- }
472
- }
473
- }
474
-
475
- function isJsonGradialImage(node) {
476
- return node?.type === 'object'
477
- && node?.properties?.$type?.const === 'gradial.image';
478
- }
479
-
480
371
  function validateZodSubset(schema, pathLabel, seen = new Set()) {
481
372
  if (seen.has(schema)) return;
482
373
  seen.add(schema);
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+
6
+ const args = parseArgs(process.argv.slice(2));
7
+
8
+ try {
9
+ if (!args.renderer) throw new Error('missing --renderer');
10
+ await verifyRenderer(path.resolve(args.renderer));
11
+ console.log(JSON.stringify({ success: true, errors: [] }, null, 2));
12
+ } catch (error) {
13
+ console.error(JSON.stringify({ success: false, errors: [String(error?.message ?? error)] }, null, 2));
14
+ process.exitCode = 1;
15
+ }
16
+
17
+ async function verifyRenderer(rendererPath) {
18
+ try {
19
+ const mod = await import(pathToFileURL(rendererPath).href);
20
+ const renderer = mod.default ?? mod.renderer;
21
+ if (!renderer || typeof renderer.renderPage !== 'function') {
22
+ throw new Error('renderer must export a GradialRenderer with renderPage');
23
+ }
24
+ return;
25
+ } catch (error) {
26
+ if (!shouldUseStaticFallback(error)) {
27
+ throw error;
28
+ }
29
+ }
30
+
31
+ const source = fs.readFileSync(rendererPath, 'utf8');
32
+ if (!staticallyExportsRenderPage(source)) {
33
+ throw new Error('renderer must export a GradialRenderer with renderPage');
34
+ }
35
+ }
36
+
37
+ function shouldUseStaticFallback(error) {
38
+ const message = String(error?.message ?? error);
39
+ return message.includes('Unknown file extension ".astro"');
40
+ }
41
+
42
+ function staticallyExportsRenderPage(source) {
43
+ if (!/\brenderPage\s*[:=]\s*(async\s*)?(\([^)]*\)|[A-Za-z_$][\w$]*)?\s*=>|\brenderPage\s*[:=]\s*(async\s*)?function\b|\basync\s+renderPage\s*\(|\brenderPage\s*\(/m.test(source)) {
44
+ return false;
45
+ }
46
+ if (/export\s+default\s+{[\s\S]*\brenderPage\b[\s\S]*}/m.test(source)) {
47
+ return true;
48
+ }
49
+ const defaultName = source.match(/export\s+default\s+([A-Za-z_$][\w$]*)\s*;?/m)?.[1];
50
+ const namedExport = source.match(/export\s*{\s*([A-Za-z_$][\w$]*)\s+as\s+renderer\s*}/m)?.[1] ?? source.match(/export\s*{\s*([A-Za-z_$][\w$]*)\s*}/m)?.[1];
51
+ const rendererName = defaultName ?? namedExport;
52
+ if (!rendererName) {
53
+ return false;
54
+ }
55
+ const declaration = new RegExp(`(?:const|let|var)\\s+${rendererName}\\b[\\s\\S]*?\\{[\\s\\S]*?\\brenderPage\\b`, 'm');
56
+ return declaration.test(source);
57
+ }
58
+
59
+ function parseArgs(argv) {
60
+ const out = {};
61
+ for (let i = 0; i < argv.length; i++) {
62
+ const arg = argv[i];
63
+ if (!arg.startsWith('--')) continue;
64
+ const body = arg.slice(2);
65
+ const equals = body.indexOf('=');
66
+ if (equals >= 0) {
67
+ out[body.slice(0, equals)] = body.slice(equals + 1);
68
+ continue;
69
+ }
70
+ out[body] = argv[++i];
71
+ }
72
+ return out;
73
+ }
@@ -2,6 +2,12 @@ import type { IslandMode, VaryDimension } from './render-mode.js';
2
2
  import type { ImageSlotContract } from './image.js';
3
3
  import type { z } from 'zod';
4
4
 
5
+ export interface ComponentRenderModes {
6
+ canStatic: boolean;
7
+ canSSR: boolean;
8
+ canClientIsland: boolean;
9
+ }
10
+
5
11
  /**
6
12
  * A CMS-registered component — sync or async (server components).
7
13
  * Accepts content props derived from the Zod schema.
@@ -26,6 +32,8 @@ export interface ComponentDefinition<TSchema = unknown> {
26
32
  */
27
33
  component?: CmsComponentFn<any>;
28
34
  schema: TSchema;
35
+ renderModes?: ComponentRenderModes;
36
+ render?: ComponentRenderModes;
29
37
  defaultIslandMode?: IslandMode;
30
38
  varyDimensions?: VaryDimension[];
31
39
  vary?: VaryDimension[];
@@ -36,6 +44,8 @@ export interface ComponentContract<TSchema = unknown> {
36
44
  name: string;
37
45
  component?: CmsComponentFn<any>;
38
46
  schema: TSchema;
47
+ renderModes: ComponentRenderModes;
48
+ render: ComponentRenderModes;
39
49
  defaultIslandMode?: IslandMode;
40
50
  varyDimensions?: VaryDimension[];
41
51
  vary?: VaryDimension[];
@@ -12,6 +12,7 @@ export interface BareMetalConfig {
12
12
  componentRegistry: string;
13
13
  dataRegistry?: string;
14
14
  layoutRegistry: string;
15
+ rendererEntry: string;
15
16
  capabilities: RenderCapabilities;
16
17
  routes: RouteConfig;
17
18
  dam?: DAMConfig;
package/src/types/page.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { VaryDimension } from './render-mode.js';
1
+ import type { RenderMode, VaryDimension } from './render-mode.js';
2
2
 
3
3
  export interface PageDocument {
4
4
  path: string;
@@ -6,6 +6,7 @@ export interface PageDocument {
6
6
  locale: string;
7
7
  metadata: PageMetadata;
8
8
  regions: Record<string, RegionNode>;
9
+ renderMode: RenderMode;
9
10
  }
10
11
 
11
12
  export interface PageMetadata {