@gradial/aci 0.1.20-preview.2 → 0.1.20-preview.3

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 +8 -10
  2. package/bin/aci +0 -0
  3. package/dist/astro/index.d.ts +2 -0
  4. package/dist/astro/index.js +4 -0
  5. package/dist/content/index.d.ts +1 -0
  6. package/dist/content/index.js +1 -0
  7. package/dist/content/provider.d.ts +19 -0
  8. package/dist/content/provider.js +7 -0
  9. package/dist/content/resolve-slots.d.ts +9 -0
  10. package/dist/content/resolve-slots.js +24 -0
  11. package/dist/content/types.d.ts +1 -1
  12. package/dist/content/validation.js +0 -1
  13. package/dist/define-component.js +0 -6
  14. package/dist/define-layout.d.ts +2 -0
  15. package/dist/define-layout.js +3 -0
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +1 -0
  18. package/dist/next/asset-route.d.ts +2 -0
  19. package/dist/next/asset-route.js +10 -2
  20. package/dist/next/config.js +1 -1
  21. package/dist/next/middleware.d.ts +6 -2
  22. package/dist/next/middleware.js +7 -2
  23. package/dist/next/page.d.ts +16 -15
  24. package/dist/next/page.js +27 -9
  25. package/dist/next/render.d.ts +5 -0
  26. package/dist/next/render.js +15 -0
  27. package/dist/next/root-layout.d.ts +4 -0
  28. package/dist/next/root-layout.js +5 -0
  29. package/dist/next/server.d.ts +3 -2
  30. package/dist/next/server.js +12 -2
  31. package/dist/providers/file.d.ts +2 -0
  32. package/dist/providers/file.js +61 -1
  33. package/dist/providers/s3.d.ts +1 -0
  34. package/dist/providers/s3.js +14 -1
  35. package/dist/registry.d.ts +15 -0
  36. package/dist/registry.js +10 -0
  37. package/dist/sveltekit/index.d.ts +2 -0
  38. package/dist/sveltekit/index.js +4 -0
  39. package/dist/testing/index.d.ts +3 -0
  40. package/dist/testing/index.js +15 -2
  41. package/dist/types/component.d.ts +0 -9
  42. package/dist/types/config.d.ts +0 -1
  43. package/dist/types/page.d.ts +1 -2
  44. package/dist/types/render-mode.d.ts +0 -1
  45. package/package.json +19 -13
  46. package/src/cli/compile-registry.mjs +132 -12
  47. package/src/types/component.ts +0 -10
  48. package/src/types/config.ts +0 -1
  49. package/src/types/page.ts +1 -2
  50. package/src/types/render-mode.ts +0 -8
  51. package/dist/content/cache.test.d.ts +0 -1
  52. package/dist/content/cache.test.js +0 -86
  53. package/dist/next/preview-banner.d.ts +0 -4
  54. package/dist/next/preview-banner.js +0 -51
  55. package/dist/next/preview.d.ts +0 -7
  56. package/dist/next/preview.js +0 -37
  57. package/src/cli/verify-renderer.mjs +0 -73
@@ -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,5 +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 { createRegistry, registryLookup } from '../registry.js';
23
+ export type { Registry, RegistryEntry, AnyComponent } from '../registry.js';
22
24
  export { getPreviewMode, createPreviewBannerHTML, createPreviewCookies, type PreviewContext, type PreviewCookie, } from './preview.js';
23
25
  export declare function withGradialAci(options?: GradialContentWatchOptions): VitePluginLike;
@@ -32,6 +32,10 @@ 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
+ // ---------------------------------------------------------------------------
35
39
  // Preview support
36
40
  // ---------------------------------------------------------------------------
37
41
  export { getPreviewMode, createPreviewBannerHTML, createPreviewCookies, } from './preview.js';
@@ -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[];
@@ -11,7 +11,6 @@ export interface BareMetalConfig {
11
11
  componentRegistry: string;
12
12
  dataRegistry?: string;
13
13
  layoutRegistry: string;
14
- rendererEntry: string;
15
14
  capabilities: RenderCapabilities;
16
15
  routes: RouteConfig;
17
16
  dam?: DAMConfig;
@@ -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.20-preview.2",
3
+ "version": "0.1.20-preview.3",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -43,10 +43,6 @@
43
43
  "types": "./dist/content/validation.d.ts",
44
44
  "import": "./dist/content/validation.js"
45
45
  },
46
- "./content/cache": {
47
- "types": "./dist/content/cache.d.ts",
48
- "import": "./dist/content/cache.js"
49
- },
50
46
  "./content/tailwind-validator": {
51
47
  "types": "./dist/content/tailwind-validator.d.ts",
52
48
  "import": "./dist/content/tailwind-validator.js"
@@ -94,6 +90,16 @@
94
90
  "import": "./dist/next/page.js",
95
91
  "default": "./dist/next/page.js"
96
92
  },
93
+ "./next/root-layout": {
94
+ "types": "./dist/next/root-layout.d.ts",
95
+ "import": "./dist/next/root-layout.js",
96
+ "default": "./dist/next/root-layout.js"
97
+ },
98
+ "./next/render": {
99
+ "types": "./dist/next/render.d.ts",
100
+ "import": "./dist/next/render.js",
101
+ "default": "./dist/next/render.js"
102
+ },
97
103
  "./next/server": {
98
104
  "types": "./dist/next/server.d.ts",
99
105
  "import": "./dist/next/server.js",
@@ -127,19 +133,16 @@
127
133
  "scripts": {
128
134
  "build": "tsc -p tsconfig.json",
129
135
  "build:cli": "cd ../.. && ./scripts/build-cli.sh",
130
- "compile-registry": "tsx src/cli/compile-registry.mjs",
131
- "prepack": "npm run build"
132
- },
133
- "overrides": {
134
- "postcss": "^8.5.10"
136
+ "compile-registry": "tsx src/cli/compile-registry.mjs"
135
137
  },
136
138
  "dependencies": {
137
139
  "ws": "^8.18.0"
138
140
  },
139
141
  "peerDependencies": {
140
142
  "@vercel/edge-config": "^1.4.3",
141
- "next": "^15.5.0",
142
- "react": "^19.0.0",
143
+ "next": "^15.0.0",
144
+ "react": "^18.0.0 || ^19.0.0",
145
+ "react-dom": "^18.0.0 || ^19.0.0",
143
146
  "zod": "^4.0.0"
144
147
  },
145
148
  "peerDependenciesMeta": {
@@ -151,6 +154,9 @@
151
154
  },
152
155
  "react": {
153
156
  "optional": true
157
+ },
158
+ "react-dom": {
159
+ "optional": true
154
160
  }
155
161
  },
156
162
  "devDependencies": {
@@ -165,4 +171,4 @@
165
171
  "ws": "^8.18.0",
166
172
  "zod": "^4.0.0"
167
173
  }
168
- }
174
+ }
@@ -52,22 +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;
56
55
  const varyDimensions = component.varyDimensions ?? component.vary;
57
56
  const imageSlots = normalizeImageSlots(component.imageSlots);
57
+ const renderModes = component.renderModes ?? component.render;
58
58
  componentEntries.push({
59
59
  name: component.name,
60
60
  schemaFile,
61
- render: {
61
+ render: renderModes ? {
62
62
  static: Boolean(renderModes.canStatic ?? renderModes.static),
63
63
  ssr: Boolean(renderModes.canSSR ?? renderModes.ssr),
64
64
  clientIsland: Boolean(renderModes.canClientIsland ?? renderModes.clientIsland),
65
- },
66
- renderModes: {
65
+ } : { static: true, ssr: true, clientIsland: false },
66
+ renderModes: renderModes ? {
67
67
  canStatic: Boolean(renderModes.canStatic ?? renderModes.static),
68
68
  canSSR: Boolean(renderModes.canSSR ?? renderModes.ssr),
69
69
  canClientIsland: Boolean(renderModes.canClientIsland ?? renderModes.clientIsland),
70
- },
70
+ } : { canStatic: true, canSSR: true, canClientIsland: false },
71
71
  ...(component.defaultIslandMode ? { defaultIslandMode: component.defaultIslandMode } : {}),
72
72
  ...(varyDimensions?.length ? { vary: varyDimensions, varyDimensions } : {}),
73
73
  ...(Object.keys(imageSlots).length ? { imageSlots } : {}),
@@ -117,6 +117,10 @@ async function compileRegistry(options) {
117
117
  };
118
118
  const registryPath = path.join(outDir, 'registry.json');
119
119
  await writeFile(registryPath, JSON.stringify(registry, null, 2) + '\n');
120
+
121
+ const pageSchemaPath = await generatePageSchema(schemaDir, componentEntries, outDir);
122
+ schemaFiles.push(pageSchemaPath);
123
+
120
124
  return { registry: registryPath, schemas: schemaFiles };
121
125
  }
122
126
 
@@ -150,12 +154,6 @@ function validateComponent(component, index, seen) {
150
154
  if (!component.schema) throw new Error(`components[${index}].schema is required`);
151
155
  validateZodSubset(component.schema, `components[${index}].schema`);
152
156
  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`);
159
157
  }
160
158
 
161
159
  function validateImageSlotSchemaMatch(component, index) {
@@ -349,7 +347,11 @@ function validateLayout(layout, index, seen) {
349
347
  seenSlots.add(slot.name);
350
348
  slots.push({ name: slot.name, required: Boolean(slot.required) });
351
349
  }
352
- return { name: layout.name, slots };
350
+ const entry = { name: layout.name, slots };
351
+ if (layout.defaults && typeof layout.defaults === 'object') {
352
+ entry.defaults = layout.defaults;
353
+ }
354
+ return entry;
353
355
  }
354
356
 
355
357
  function compileSchema(schema, name) {
@@ -368,6 +370,124 @@ function compileSchema(schema, name) {
368
370
  };
369
371
  }
370
372
 
373
+ async function generatePageSchema(schemaDir, componentEntries, outDir) {
374
+ const { readFile } = await import('node:fs/promises');
375
+ const defs = {
376
+ baseBlock: {
377
+ type: 'object',
378
+ required: ['id', 'component', 'props'],
379
+ additionalProperties: true,
380
+ properties: {
381
+ id: { type: 'string', minLength: 1 },
382
+ component: { type: 'string' },
383
+ props: { type: 'object' },
384
+ },
385
+ },
386
+ damAssetRef: {
387
+ type: 'object',
388
+ required: ['$type', 'assetId'],
389
+ additionalProperties: true,
390
+ properties: {
391
+ $type: { const: 'dam.assetRef' },
392
+ assetId: { type: 'string', minLength: 1 },
393
+ version: { type: 'string', minLength: 1 },
394
+ alt: { type: 'string' },
395
+ },
396
+ },
397
+ };
398
+ const oneOf = [];
399
+ for (const entry of componentEntries) {
400
+ const schemaPath = path.join(outDir, entry.schemaFile);
401
+ const raw = await readFile(schemaPath, 'utf-8');
402
+ const componentSchema = JSON.parse(raw);
403
+ const { $schema: _s, title: _t, ...propsSchema } = componentSchema;
404
+ replaceGradialImagesWithDamAssetRef(propsSchema, defs.damAssetRef);
405
+ const defName = `${entry.name}Block`;
406
+ defs[defName] = {
407
+ allOf: [
408
+ { $ref: '#/$defs/baseBlock' },
409
+ {
410
+ properties: {
411
+ component: { const: entry.name },
412
+ props: propsSchema,
413
+ },
414
+ },
415
+ ],
416
+ };
417
+ oneOf.push({ $ref: `#/$defs/${defName}` });
418
+ }
419
+ const pageSchema = {
420
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
421
+ $id: 'https://baremetal.local/schemas/page.schema.json',
422
+ title: 'bare-metal Page',
423
+ type: 'object',
424
+ required: ['$type', 'id', 'status', 'layout', 'metadata', 'regions'],
425
+ additionalProperties: true,
426
+ properties: {
427
+ $type: { const: 'page' },
428
+ id: { type: 'string', minLength: 1 },
429
+ status: { type: 'string', minLength: 1 },
430
+ layout: { type: 'string', minLength: 1 },
431
+ metadata: {
432
+ type: 'object',
433
+ required: ['title'],
434
+ additionalProperties: true,
435
+ properties: {
436
+ title: { type: 'string', minLength: 1 },
437
+ description: { type: 'string' },
438
+ canonical: { type: 'string' },
439
+ },
440
+ },
441
+ regions: {
442
+ type: 'object',
443
+ additionalProperties: {
444
+ type: 'array',
445
+ items: { oneOf },
446
+ },
447
+ },
448
+ },
449
+ $defs: defs,
450
+ };
451
+ const pageSchemaPath = path.join(schemaDir, 'page.schema.json');
452
+ await writeFile(pageSchemaPath, JSON.stringify(pageSchema, null, 2) + '\n');
453
+ return pageSchemaPath;
454
+ }
455
+
456
+ function replaceGradialImagesWithDamAssetRef(schema, damAssetRef) {
457
+ if (!schema || typeof schema !== 'object') return schema;
458
+ if (schema.$ref) return;
459
+ if (Array.isArray(schema.anyOf)) {
460
+ for (const s of schema.anyOf) replaceGradialImagesWithDamAssetRef(s, damAssetRef);
461
+ }
462
+ if (Array.isArray(schema.allOf)) {
463
+ for (const s of schema.allOf) replaceGradialImagesWithDamAssetRef(s, damAssetRef);
464
+ }
465
+ if (Array.isArray(schema.oneOf)) {
466
+ for (const s of schema.oneOf) replaceGradialImagesWithDamAssetRef(s, damAssetRef);
467
+ }
468
+ if (schema.properties) {
469
+ for (const [key, value] of Object.entries(schema.properties)) {
470
+ if (isJsonGradialImage(value)) {
471
+ schema.properties[key] = { $ref: '#/$defs/damAssetRef' };
472
+ } else {
473
+ replaceGradialImagesWithDamAssetRef(value, damAssetRef);
474
+ }
475
+ }
476
+ }
477
+ if (schema.items) {
478
+ if (isJsonGradialImage(schema.items)) {
479
+ schema.items = { $ref: '#/$defs/damAssetRef' };
480
+ } else {
481
+ replaceGradialImagesWithDamAssetRef(schema.items, damAssetRef);
482
+ }
483
+ }
484
+ }
485
+
486
+ function isJsonGradialImage(node) {
487
+ return node?.type === 'object'
488
+ && node?.properties?.$type?.const === 'gradial.image';
489
+ }
490
+
371
491
  function validateZodSubset(schema, pathLabel, seen = new Set()) {
372
492
  if (seen.has(schema)) return;
373
493
  seen.add(schema);
@@ -2,12 +2,6 @@ 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
-
11
5
  /**
12
6
  * A CMS-registered component — sync or async (server components).
13
7
  * Accepts content props derived from the Zod schema.
@@ -32,8 +26,6 @@ export interface ComponentDefinition<TSchema = unknown> {
32
26
  */
33
27
  component?: CmsComponentFn<any>;
34
28
  schema: TSchema;
35
- renderModes?: ComponentRenderModes;
36
- render?: ComponentRenderModes;
37
29
  defaultIslandMode?: IslandMode;
38
30
  varyDimensions?: VaryDimension[];
39
31
  vary?: VaryDimension[];
@@ -44,8 +36,6 @@ export interface ComponentContract<TSchema = unknown> {
44
36
  name: string;
45
37
  component?: CmsComponentFn<any>;
46
38
  schema: TSchema;
47
- renderModes: ComponentRenderModes;
48
- render: ComponentRenderModes;
49
39
  defaultIslandMode?: IslandMode;
50
40
  varyDimensions?: VaryDimension[];
51
41
  vary?: VaryDimension[];
@@ -12,7 +12,6 @@ export interface BareMetalConfig {
12
12
  componentRegistry: string;
13
13
  dataRegistry?: string;
14
14
  layoutRegistry: string;
15
- rendererEntry: string;
16
15
  capabilities: RenderCapabilities;
17
16
  routes: RouteConfig;
18
17
  dam?: DAMConfig;
package/src/types/page.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { RenderMode, VaryDimension } from './render-mode.js';
1
+ import type { VaryDimension } from './render-mode.js';
2
2
 
3
3
  export interface PageDocument {
4
4
  path: string;
@@ -6,7 +6,6 @@ export interface PageDocument {
6
6
  locale: string;
7
7
  metadata: PageMetadata;
8
8
  regions: Record<string, RegionNode>;
9
- renderMode: RenderMode;
10
9
  }
11
10
 
12
11
  export interface PageMetadata {
@@ -1,11 +1,3 @@
1
- export type RenderMode =
2
- | 'static'
3
- | 'static-with-fragments'
4
- | 'prerender-on-demand'
5
- | 'ssr-page'
6
- | 'ssr-island'
7
- | 'client-island';
8
-
9
1
  export type IslandMode = 'ssr' | 'client';
10
2
 
11
3
  export type VaryDimension =
@@ -1 +0,0 @@
1
- export {};