@gradial/aci 0.1.20-preview.1 → 0.1.20-preview.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -8
- package/bin/aci +0 -0
- package/dist/astro/index.d.ts +0 -2
- package/dist/astro/index.js +0 -4
- package/dist/content/cache.test.d.ts +1 -0
- package/dist/content/cache.test.js +86 -0
- package/dist/content/index.d.ts +0 -1
- package/dist/content/index.js +0 -1
- package/dist/content/provider.d.ts +0 -19
- package/dist/content/provider.js +0 -7
- package/dist/content/types.d.ts +1 -1
- package/dist/content/validation.js +1 -0
- package/dist/define-component.js +6 -0
- package/dist/define-layout.d.ts +0 -2
- package/dist/define-layout.js +0 -3
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/next/asset-route.d.ts +0 -2
- package/dist/next/asset-route.js +2 -10
- package/dist/next/config.js +1 -1
- package/dist/next/middleware.d.ts +2 -6
- package/dist/next/middleware.js +2 -7
- package/dist/next/page.d.ts +15 -16
- package/dist/next/page.js +9 -27
- package/dist/next/preview-banner.d.ts +4 -0
- package/dist/next/preview-banner.js +51 -0
- package/dist/next/preview.d.ts +7 -0
- package/dist/next/preview.js +37 -0
- package/dist/next/server.d.ts +2 -3
- package/dist/next/server.js +2 -12
- package/dist/providers/file.d.ts +0 -2
- package/dist/providers/file.js +1 -61
- package/dist/providers/s3.d.ts +0 -1
- package/dist/providers/s3.js +1 -14
- package/dist/sveltekit/index.d.ts +0 -2
- package/dist/sveltekit/index.js +0 -4
- package/dist/testing/index.d.ts +0 -3
- package/dist/testing/index.js +2 -15
- package/dist/types/component.d.ts +9 -0
- package/dist/types/config.d.ts +1 -0
- package/dist/types/page.d.ts +2 -1
- package/dist/types/render-mode.d.ts +1 -0
- package/package.json +7 -18
- package/src/cli/compile-registry.mjs +18 -127
- package/src/cli/verify-renderer.mjs +73 -0
- package/src/types/component.ts +10 -0
- package/src/types/config.ts +1 -0
- package/src/types/page.ts +2 -1
- package/src/types/render-mode.ts +8 -0
- package/dist/content/resolve-slots.d.ts +0 -9
- package/dist/content/resolve-slots.js +0 -24
- package/dist/next/render.d.ts +0 -5
- package/dist/next/render.js +0 -15
- package/dist/next/root-layout.d.ts +0 -4
- package/dist/next/root-layout.js +0 -5
- package/dist/registry.d.ts +0 -15
- package/dist/registry.js +0 -10
package/dist/next/server.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { headers } from 'next/headers.js';
|
|
2
|
-
import { FragmentNotFoundError,
|
|
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
|
|
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();
|
package/dist/providers/file.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/providers/file.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { FragmentNotFoundError,
|
|
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
|
-
}
|
package/dist/providers/s3.d.ts
CHANGED
|
@@ -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;
|
package/dist/providers/s3.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
|
-
import { FragmentNotFoundError,
|
|
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;
|
package/dist/sveltekit/index.js
CHANGED
|
@@ -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';
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -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>;
|
package/dist/testing/index.js
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
import { PageNotFoundError, FragmentNotFoundError,
|
|
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[];
|
package/dist/types/config.d.ts
CHANGED
package/dist/types/page.d.ts
CHANGED
|
@@ -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.
|
|
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": "
|
|
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.
|
|
149
|
-
"react": "^
|
|
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
|
-
|
|
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
|
+
}
|
package/src/types/component.ts
CHANGED
|
@@ -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[];
|
package/src/types/config.ts
CHANGED
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 {
|