@gradial/aci 0.1.0 → 0.1.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 +47 -2
- package/bin/aci +0 -0
- package/bin/aci.js +157 -0
- package/dist/assets/index.d.ts +3 -0
- package/dist/assets/index.js +3 -0
- package/dist/astro/index.d.ts +24 -2
- package/dist/astro/index.js +42 -4
- package/dist/block-ref.d.ts +34 -0
- package/dist/block-ref.js +34 -0
- package/dist/content/index.d.ts +0 -3
- package/dist/content/index.js +0 -3
- package/dist/content/provider.d.ts +32 -8
- package/dist/content/provider.js +26 -16
- package/dist/content/routes.d.ts +6 -12
- package/dist/content/routes.js +9 -55
- package/dist/content/validation.js +1 -1
- package/dist/define-component.d.ts +1 -0
- package/dist/define-component.js +1 -0
- package/dist/define-layout.d.ts +1 -0
- package/dist/define-layout.js +6 -1
- package/dist/dev/browser.d.ts +1 -1
- package/dist/dev/browser.js +1 -1
- package/dist/dev/index.d.ts +9 -3
- package/dist/dev/index.js +74 -8
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/next/asset-route.d.ts +9 -0
- package/dist/next/asset-route.js +15 -0
- package/dist/next/config.d.ts +6 -0
- package/dist/next/config.js +25 -0
- package/dist/next/dev-refresh.js +4 -4
- package/dist/next/edge-config.d.ts +1 -0
- package/dist/next/edge-config.js +92 -0
- package/dist/next/index.d.ts +2 -0
- package/dist/next/index.js +2 -0
- package/dist/next/middleware.js +4 -6
- package/dist/next/server.d.ts +9 -24
- package/dist/next/server.js +100 -152
- package/dist/providers/file.d.ts +11 -17
- package/dist/providers/file.js +44 -78
- package/dist/providers/s3.d.ts +26 -0
- package/dist/providers/s3.js +174 -0
- package/dist/react/GradialMedia.d.ts +24 -0
- package/dist/react/GradialMedia.js +31 -0
- package/dist/react/GradialPicture.d.ts +14 -0
- package/dist/react/GradialPicture.js +30 -0
- package/dist/react/GradialVideoPlayer.d.ts +13 -0
- package/dist/react/GradialVideoPlayer.js +28 -0
- package/dist/react/index.d.ts +3 -0
- package/dist/react/index.js +3 -0
- package/dist/sveltekit/index.d.ts +18 -2
- package/dist/sveltekit/index.js +40 -4
- package/dist/testing/index.d.ts +14 -12
- package/dist/testing/index.js +41 -28
- package/dist/types/component.d.ts +24 -2
- package/dist/types/config.d.ts +4 -0
- package/dist/types/image.d.ts +51 -0
- package/dist/types/image.js +58 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.js +3 -0
- package/dist/types/layout.d.ts +12 -0
- package/dist/types/media.d.ts +69 -0
- package/dist/types/media.js +86 -0
- package/dist/types/video.d.ts +70 -0
- package/dist/types/video.js +22 -0
- package/package.json +30 -2
- package/src/cli/compile-registry.mjs +303 -0
- package/src/cli/validate-content.mjs +489 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { GradialImageSchema, renderImageHTML } from './image.js';
|
|
3
|
+
import { GradialVideoSchema } from './video.js';
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Zod schema — discriminated on $type
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
export const GradialAssetSchema = z.discriminatedUnion('$type', [
|
|
8
|
+
GradialImageSchema,
|
|
9
|
+
GradialVideoSchema,
|
|
10
|
+
]);
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Type guards
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
export function isGradialImage(value) {
|
|
15
|
+
return (typeof value === 'object' &&
|
|
16
|
+
value !== null &&
|
|
17
|
+
value.$type === 'gradial.image');
|
|
18
|
+
}
|
|
19
|
+
export function isGradialVideo(value) {
|
|
20
|
+
return (typeof value === 'object' &&
|
|
21
|
+
value !== null &&
|
|
22
|
+
value.$type === 'gradial.video');
|
|
23
|
+
}
|
|
24
|
+
export function isGradialAsset(value) {
|
|
25
|
+
return isGradialImage(value) || isGradialVideo(value);
|
|
26
|
+
}
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// HTML string renderers (framework-agnostic)
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
/**
|
|
31
|
+
* Renders a GradialVideo to an HTML string.
|
|
32
|
+
* Uses poster.fallback.src for the poster attribute.
|
|
33
|
+
* Includes <source> elements for format variants.
|
|
34
|
+
*/
|
|
35
|
+
export function renderVideoHTML(video, attrs = {}) {
|
|
36
|
+
const posterSrc = video.poster?.fallback.src;
|
|
37
|
+
const attrPairs = [
|
|
38
|
+
...Object.entries(attrs),
|
|
39
|
+
['src', video.sources?.length ? undefined : video.src],
|
|
40
|
+
['poster', posterSrc],
|
|
41
|
+
['width', video.width > 0 ? video.width : undefined],
|
|
42
|
+
['height', video.height > 0 ? video.height : undefined],
|
|
43
|
+
];
|
|
44
|
+
const attrString = renderAttrs(attrPairs);
|
|
45
|
+
const sources = (video.sources ?? [])
|
|
46
|
+
.map((s) => `<source${renderAttrs([['src', s.src], ['type', s.type]])}>`)
|
|
47
|
+
.join('');
|
|
48
|
+
// If no format variants, src is on the <video> element itself
|
|
49
|
+
if (!sources) {
|
|
50
|
+
return `<video${attrString}></video>`;
|
|
51
|
+
}
|
|
52
|
+
return `<video${attrString}>${sources}</video>`;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Renders any GradialAsset to an HTML string.
|
|
56
|
+
* Delegates to renderImageHTML or renderVideoHTML based on $type.
|
|
57
|
+
*/
|
|
58
|
+
export function renderAssetHTML(asset, attrs = {}) {
|
|
59
|
+
if (asset.$type === 'gradial.image') {
|
|
60
|
+
return renderImageHTML(asset, attrs);
|
|
61
|
+
}
|
|
62
|
+
return renderVideoHTML(asset, attrs);
|
|
63
|
+
}
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Internal helpers
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
function renderAttrs(pairs) {
|
|
68
|
+
const rendered = pairs
|
|
69
|
+
.filter(([, value]) => value !== undefined && value !== null && value !== false)
|
|
70
|
+
.map(([name, value]) => value === true
|
|
71
|
+
? escapeName(name)
|
|
72
|
+
: `${escapeName(name)}="${escapeAttr(String(value))}"`);
|
|
73
|
+
if (!rendered.length)
|
|
74
|
+
return '';
|
|
75
|
+
return ' ' + rendered.join(' ');
|
|
76
|
+
}
|
|
77
|
+
function escapeName(name) {
|
|
78
|
+
return name.replace(/[^A-Za-z0-9_:-]/g, '');
|
|
79
|
+
}
|
|
80
|
+
function escapeAttr(value) {
|
|
81
|
+
return value
|
|
82
|
+
.replace(/&/g, '&')
|
|
83
|
+
.replace(/"/g, '"')
|
|
84
|
+
.replace(/</g, '<')
|
|
85
|
+
.replace(/>/g, '>');
|
|
86
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type GradialImage } from './image.js';
|
|
3
|
+
export interface VideoSource {
|
|
4
|
+
src: string;
|
|
5
|
+
type: string;
|
|
6
|
+
}
|
|
7
|
+
export interface GradialVideo {
|
|
8
|
+
/** Discriminator — always 'gradial.video'. */
|
|
9
|
+
$type: 'gradial.video';
|
|
10
|
+
/** DAM asset identifier. */
|
|
11
|
+
assetId: string;
|
|
12
|
+
/** DAM version identifier. */
|
|
13
|
+
versionId: string;
|
|
14
|
+
/** Accessible description of the video content. */
|
|
15
|
+
alt: string;
|
|
16
|
+
/** Primary video URL (compiler-resolved). */
|
|
17
|
+
src: string;
|
|
18
|
+
/** Primary video MIME type (e.g. 'video/mp4'). */
|
|
19
|
+
type: string;
|
|
20
|
+
/** Intrinsic width in pixels. */
|
|
21
|
+
width: number;
|
|
22
|
+
/** Intrinsic height in pixels. */
|
|
23
|
+
height: number;
|
|
24
|
+
/**
|
|
25
|
+
* Poster frame — a full GradialImage with responsive srcset.
|
|
26
|
+
* The compiler extracts or generates this from the video asset.
|
|
27
|
+
*/
|
|
28
|
+
poster?: GradialImage;
|
|
29
|
+
/** Format variants (e.g. mp4 + webm). Maps to <video><source> elements. */
|
|
30
|
+
sources?: VideoSource[];
|
|
31
|
+
/** Duration in seconds (if known). */
|
|
32
|
+
duration?: number;
|
|
33
|
+
}
|
|
34
|
+
export declare const VideoSourceSchema: z.ZodObject<{
|
|
35
|
+
src: z.ZodString;
|
|
36
|
+
type: z.ZodString;
|
|
37
|
+
}, z.core.$strip>;
|
|
38
|
+
export declare const GradialVideoSchema: z.ZodObject<{
|
|
39
|
+
$type: z.ZodLiteral<"gradial.video">;
|
|
40
|
+
assetId: z.ZodString;
|
|
41
|
+
versionId: z.ZodString;
|
|
42
|
+
alt: z.ZodString;
|
|
43
|
+
src: z.ZodString;
|
|
44
|
+
type: z.ZodString;
|
|
45
|
+
width: z.ZodNumber;
|
|
46
|
+
height: z.ZodNumber;
|
|
47
|
+
poster: z.ZodOptional<z.ZodObject<{
|
|
48
|
+
$type: z.ZodLiteral<"gradial.image">;
|
|
49
|
+
assetId: z.ZodString;
|
|
50
|
+
versionId: z.ZodString;
|
|
51
|
+
alt: z.ZodString;
|
|
52
|
+
fallback: z.ZodObject<{
|
|
53
|
+
src: z.ZodString;
|
|
54
|
+
width: z.ZodNumber;
|
|
55
|
+
height: z.ZodNumber;
|
|
56
|
+
type: z.ZodString;
|
|
57
|
+
}, z.core.$strip>;
|
|
58
|
+
sources: z.ZodDefault<z.ZodNullable<z.ZodArray<z.ZodObject<{
|
|
59
|
+
media: z.ZodOptional<z.ZodString>;
|
|
60
|
+
type: z.ZodString;
|
|
61
|
+
srcset: z.ZodString;
|
|
62
|
+
sizes: z.ZodOptional<z.ZodString>;
|
|
63
|
+
}, z.core.$strip>>>>;
|
|
64
|
+
}, z.core.$strip>>;
|
|
65
|
+
sources: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
66
|
+
src: z.ZodString;
|
|
67
|
+
type: z.ZodString;
|
|
68
|
+
}, z.core.$strip>>>;
|
|
69
|
+
duration: z.ZodOptional<z.ZodNumber>;
|
|
70
|
+
}, z.core.$strip>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { GradialImageSchema } from './image.js';
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Zod schema
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
export const VideoSourceSchema = z.object({
|
|
7
|
+
src: z.string().min(1),
|
|
8
|
+
type: z.string().min(1),
|
|
9
|
+
});
|
|
10
|
+
export const GradialVideoSchema = z.object({
|
|
11
|
+
$type: z.literal('gradial.video'),
|
|
12
|
+
assetId: z.string().min(1),
|
|
13
|
+
versionId: z.string().min(1),
|
|
14
|
+
alt: z.string(),
|
|
15
|
+
src: z.string().min(1),
|
|
16
|
+
type: z.string().min(1),
|
|
17
|
+
width: z.number().int().nonnegative(),
|
|
18
|
+
height: z.number().int().nonnegative(),
|
|
19
|
+
poster: GradialImageSchema.optional(),
|
|
20
|
+
sources: z.array(VideoSourceSchema).optional(),
|
|
21
|
+
duration: z.number().nonnegative().optional(),
|
|
22
|
+
});
|
package/package.json
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gradial/aci",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
7
|
"files": [
|
|
8
8
|
"dist",
|
|
9
|
+
"bin/aci.js",
|
|
10
|
+
"bin/aci",
|
|
9
11
|
"src/cli/*.mjs",
|
|
10
12
|
"README.md"
|
|
11
13
|
],
|
|
14
|
+
"bin": {
|
|
15
|
+
"aci": "./bin/aci.js"
|
|
16
|
+
},
|
|
12
17
|
"publishConfig": {
|
|
13
|
-
"access": "
|
|
18
|
+
"access": "public"
|
|
14
19
|
},
|
|
15
20
|
"exports": {
|
|
16
21
|
".": {
|
|
@@ -41,14 +46,31 @@
|
|
|
41
46
|
"types": "./dist/providers/file.d.ts",
|
|
42
47
|
"import": "./dist/providers/file.js"
|
|
43
48
|
},
|
|
49
|
+
"./providers/s3": {
|
|
50
|
+
"types": "./dist/providers/s3.d.ts",
|
|
51
|
+
"import": "./dist/providers/s3.js"
|
|
52
|
+
},
|
|
44
53
|
"./dev": {
|
|
45
54
|
"types": "./dist/dev/index.d.ts",
|
|
46
55
|
"import": "./dist/dev/index.js"
|
|
47
56
|
},
|
|
57
|
+
"./assets": {
|
|
58
|
+
"types": "./dist/assets/index.d.ts",
|
|
59
|
+
"import": "./dist/assets/index.js"
|
|
60
|
+
},
|
|
61
|
+
"./react": {
|
|
62
|
+
"types": "./dist/react/index.d.ts",
|
|
63
|
+
"import": "./dist/react/index.js"
|
|
64
|
+
},
|
|
48
65
|
"./astro": {
|
|
49
66
|
"types": "./dist/astro/index.d.ts",
|
|
50
67
|
"import": "./dist/astro/index.js"
|
|
51
68
|
},
|
|
69
|
+
"./next": {
|
|
70
|
+
"types": "./dist/next/index.d.ts",
|
|
71
|
+
"import": "./dist/next/index.js",
|
|
72
|
+
"default": "./dist/next/index.js"
|
|
73
|
+
},
|
|
52
74
|
"./next/server": {
|
|
53
75
|
"types": "./dist/next/server.d.ts",
|
|
54
76
|
"import": "./dist/next/server.js",
|
|
@@ -63,6 +85,10 @@
|
|
|
63
85
|
"types": "./dist/next/dev-refresh.d.ts",
|
|
64
86
|
"import": "./dist/next/dev-refresh.js"
|
|
65
87
|
},
|
|
88
|
+
"./next/asset-route": {
|
|
89
|
+
"types": "./dist/next/asset-route.d.ts",
|
|
90
|
+
"import": "./dist/next/asset-route.js"
|
|
91
|
+
},
|
|
66
92
|
"./sveltekit": {
|
|
67
93
|
"types": "./dist/sveltekit/index.d.ts",
|
|
68
94
|
"import": "./dist/sveltekit/index.js"
|
|
@@ -72,10 +98,12 @@
|
|
|
72
98
|
"import": "./dist/testing/index.js"
|
|
73
99
|
},
|
|
74
100
|
"./cli/compile-registry": "./src/cli/compile-registry.mjs",
|
|
101
|
+
"./cli/validate-content": "./src/cli/validate-content.mjs",
|
|
75
102
|
"./cli/verify-renderer": "./src/cli/verify-renderer.mjs"
|
|
76
103
|
},
|
|
77
104
|
"scripts": {
|
|
78
105
|
"build": "tsc -p tsconfig.json",
|
|
106
|
+
"build:cli": "cd ../.. && ./scripts/build-cli.sh",
|
|
79
107
|
"compile-registry": "tsx src/cli/compile-registry.mjs",
|
|
80
108
|
"prepack": "npm run build"
|
|
81
109
|
},
|
|
@@ -3,6 +3,17 @@ import { mkdir, writeFile } from 'node:fs/promises';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
5
|
import { z } from 'zod';
|
|
6
|
+
import { GradialImageSchema } from '../types/image.ts';
|
|
7
|
+
|
|
8
|
+
// Shim React global so JSX-containing component files can be loaded.
|
|
9
|
+
// The compile-registry only extracts contract metadata (schemas, imageSlots)
|
|
10
|
+
// and never renders components, but importing .tsx files requires the JSX
|
|
11
|
+
// runtime to be resolvable. Next.js uses jsx: "preserve" which leaves React
|
|
12
|
+
// references in the transpiled output.
|
|
13
|
+
try {
|
|
14
|
+
const React = await import('react');
|
|
15
|
+
if (!globalThis.React) globalThis.React = React;
|
|
16
|
+
} catch { /* react not available — component files without JSX will still work */ }
|
|
6
17
|
|
|
7
18
|
const args = parseArgs(process.argv.slice(2));
|
|
8
19
|
|
|
@@ -38,6 +49,7 @@ async function compileRegistry(options) {
|
|
|
38
49
|
schemaFiles.push(schemaPath);
|
|
39
50
|
const renderModes = component.renderModes ?? component.render;
|
|
40
51
|
const varyDimensions = component.varyDimensions ?? component.vary;
|
|
52
|
+
const imageSlots = normalizeImageSlots(component.imageSlots);
|
|
41
53
|
componentEntries.push({
|
|
42
54
|
name: component.name,
|
|
43
55
|
schemaFile,
|
|
@@ -53,12 +65,26 @@ async function compileRegistry(options) {
|
|
|
53
65
|
},
|
|
54
66
|
...(component.defaultIslandMode ? { defaultIslandMode: component.defaultIslandMode } : {}),
|
|
55
67
|
...(varyDimensions?.length ? { vary: varyDimensions, varyDimensions } : {}),
|
|
68
|
+
...(Object.keys(imageSlots).length ? { imageSlots } : {}),
|
|
56
69
|
});
|
|
57
70
|
}
|
|
58
71
|
|
|
59
72
|
const seenLayouts = new Set();
|
|
60
73
|
const layoutEntries = layouts.map((layout, index) => validateLayout(layout, index, seenLayouts));
|
|
61
74
|
|
|
75
|
+
// Post-compilation: detect nested block patterns in JSON schemas and
|
|
76
|
+
// validate that referenced component names exist in the registry.
|
|
77
|
+
const knownNames = new Set(componentEntries.map((c) => c.name));
|
|
78
|
+
for (const entry of componentEntries) {
|
|
79
|
+
const schemaPath = path.join(outDir, entry.schemaFile);
|
|
80
|
+
const schemaRaw = await import('node:fs/promises').then((fs) => fs.readFile(schemaPath, 'utf-8'));
|
|
81
|
+
const schema = JSON.parse(schemaRaw);
|
|
82
|
+
const nestedBlocks = collectNestedBlockRefs(schema, knownNames, entry.name);
|
|
83
|
+
if (nestedBlocks.length > 0) {
|
|
84
|
+
entry.nestedBlocks = nestedBlocks;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
62
88
|
const registry = {
|
|
63
89
|
manifestVersion: '1',
|
|
64
90
|
generatedAt: new Date().toISOString(),
|
|
@@ -84,6 +110,7 @@ function validateComponent(component, index, seen) {
|
|
|
84
110
|
seen.add(component.name);
|
|
85
111
|
if (!component.schema) throw new Error(`components[${index}].schema is required`);
|
|
86
112
|
validateZodSubset(component.schema, `components[${index}].schema`);
|
|
113
|
+
validateImageSlotSchemaMatch(component, index);
|
|
87
114
|
const renderModes = component.renderModes ?? component.render;
|
|
88
115
|
if (!renderModes) throw new Error(`components[${index}].renderModes is required`);
|
|
89
116
|
const enabled = Boolean(renderModes.canStatic ?? renderModes.static)
|
|
@@ -92,6 +119,183 @@ function validateComponent(component, index, seen) {
|
|
|
92
119
|
if (!enabled) throw new Error(`components[${index}].renderModes must enable at least one mode`);
|
|
93
120
|
}
|
|
94
121
|
|
|
122
|
+
function validateImageSlotSchemaMatch(component, index) {
|
|
123
|
+
const imageSlots = normalizeImageSlots(component.imageSlots);
|
|
124
|
+
const imageFields = collectGradialImageFields(component.schema);
|
|
125
|
+
for (const field of imageFields) {
|
|
126
|
+
if (!Object.prototype.hasOwnProperty.call(imageSlots, field)) {
|
|
127
|
+
throw new Error(`components[${index}].imageSlots.${field} is required because schema field ${field} uses GradialImageSchema`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (const field of Object.keys(imageSlots)) {
|
|
131
|
+
if (!imageFields.has(field)) {
|
|
132
|
+
throw new Error(`components[${index}].schema.${field} must use GradialImageSchema because imageSlots.${field} is declared`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function normalizeImageSlots(imageSlots) {
|
|
138
|
+
if (imageSlots == null) return {};
|
|
139
|
+
if (typeof imageSlots !== 'object' || Array.isArray(imageSlots)) {
|
|
140
|
+
throw new Error('imageSlots must be an object');
|
|
141
|
+
}
|
|
142
|
+
const out = {};
|
|
143
|
+
for (const [key, slot] of Object.entries(imageSlots)) {
|
|
144
|
+
if (!key) throw new Error('imageSlots keys must be non-empty');
|
|
145
|
+
if (!slot || typeof slot !== 'object' || Array.isArray(slot)) {
|
|
146
|
+
throw new Error(`imageSlots.${key} must be an object`);
|
|
147
|
+
}
|
|
148
|
+
if (!Array.isArray(slot.outputs) || !slot.outputs.length) {
|
|
149
|
+
throw new Error(`imageSlots.${key}.outputs must be a non-empty array`);
|
|
150
|
+
}
|
|
151
|
+
if (!Array.isArray(slot.formats) || !slot.formats.length) {
|
|
152
|
+
throw new Error(`imageSlots.${key}.formats must be a non-empty array`);
|
|
153
|
+
}
|
|
154
|
+
if (typeof slot.sizes !== 'string' || !slot.sizes.trim()) {
|
|
155
|
+
throw new Error(`imageSlots.${key}.sizes must be a non-empty string`);
|
|
156
|
+
}
|
|
157
|
+
out[key] = {
|
|
158
|
+
outputs: slot.outputs.map((output, outputIndex) => normalizeSlotOutput(key, output, outputIndex)),
|
|
159
|
+
formats: slot.formats.map((format, formatIndex) => {
|
|
160
|
+
if (typeof format !== 'string' || !format.trim()) {
|
|
161
|
+
throw new Error(`imageSlots.${key}.formats[${formatIndex}] must be a non-empty string`);
|
|
162
|
+
}
|
|
163
|
+
return format;
|
|
164
|
+
}),
|
|
165
|
+
sizes: slot.sizes,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function normalizeSlotOutput(slotKey, output, outputIndex) {
|
|
172
|
+
if (!output || typeof output !== 'object' || Array.isArray(output)) {
|
|
173
|
+
throw new Error(`imageSlots.${slotKey}.outputs[${outputIndex}] must be an object`);
|
|
174
|
+
}
|
|
175
|
+
if (typeof output.aspectRatio !== 'string' || !output.aspectRatio.trim()) {
|
|
176
|
+
throw new Error(`imageSlots.${slotKey}.outputs[${outputIndex}].aspectRatio must be a non-empty string`);
|
|
177
|
+
}
|
|
178
|
+
if (!Array.isArray(output.widths) || !output.widths.length) {
|
|
179
|
+
throw new Error(`imageSlots.${slotKey}.outputs[${outputIndex}].widths must be a non-empty array`);
|
|
180
|
+
}
|
|
181
|
+
const widths = output.widths.map((width, widthIndex) => {
|
|
182
|
+
if (!Number.isInteger(width) || width <= 0) {
|
|
183
|
+
throw new Error(`imageSlots.${slotKey}.outputs[${outputIndex}].widths[${widthIndex}] must be a positive integer`);
|
|
184
|
+
}
|
|
185
|
+
return width;
|
|
186
|
+
});
|
|
187
|
+
return {
|
|
188
|
+
aspectRatio: output.aspectRatio,
|
|
189
|
+
widths,
|
|
190
|
+
...(typeof output.media === 'string' && output.media.trim() ? { media: output.media } : {}),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function collectGradialImageFields(schema) {
|
|
195
|
+
const out = new Set();
|
|
196
|
+
|
|
197
|
+
collectGradialImageFieldsFromSchema(schema, '', out);
|
|
198
|
+
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function collectGradialImageFieldsFromSchema(schema, prefix, out) {
|
|
203
|
+
if (!schema) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (prefix && isGradialImageSchema(schema)) {
|
|
208
|
+
out.add(prefix);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const itemSchema = arrayItemSchema(schema);
|
|
213
|
+
if (itemSchema) {
|
|
214
|
+
collectGradialImageFieldsFromSchema(itemSchema, prefix, out);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const unionOptions = unionOptionSchemas(schema);
|
|
219
|
+
if (unionOptions) {
|
|
220
|
+
for (const option of unionOptions) {
|
|
221
|
+
collectGradialImageFieldsFromSchema(option, prefix, out);
|
|
222
|
+
}
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const def = unwrapSchema(schema)?._def;
|
|
227
|
+
const shape = typeof def?.shape === 'function' ? def.shape() : def?.shape;
|
|
228
|
+
if (!shape || typeof shape !== 'object') {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
for (const [field, fieldSchema] of Object.entries(shape)) {
|
|
233
|
+
const nextPrefix = prefix ? `${prefix}.${field}` : field;
|
|
234
|
+
collectGradialImageFieldsFromSchema(fieldSchema, nextPrefix, out);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function unionOptionSchemas(schema) {
|
|
239
|
+
const def = unwrapSchema(schema)?._def;
|
|
240
|
+
if (!def) return null;
|
|
241
|
+
if (Array.isArray(def.options)) return def.options;
|
|
242
|
+
if (def.options instanceof Map) return Array.from(def.options.values());
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function arrayItemSchema(schema) {
|
|
247
|
+
let current = schema;
|
|
248
|
+
const seen = new Set();
|
|
249
|
+
while (current && !seen.has(current)) {
|
|
250
|
+
seen.add(current);
|
|
251
|
+
const def = current?._def;
|
|
252
|
+
if (!def) {
|
|
253
|
+
// Zod 4 mini: check for ~standard array type
|
|
254
|
+
if (current?.type === 'array' && current?.element) return current.element;
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
if (def.typeName === 'ZodArray') return def.type;
|
|
258
|
+
// Zod 4: array element might be in def.element
|
|
259
|
+
if (def.type === 'array' && def.element) return def.element;
|
|
260
|
+
current = def.innerType ?? def.schema;
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function isGradialImageSchema(schema) {
|
|
266
|
+
const unwrapped = unwrapSchema(schema);
|
|
267
|
+
if (unwrapped === GradialImageSchema) return true;
|
|
268
|
+
try {
|
|
269
|
+
const compiled = z.toJSONSchema(unwrapped, {
|
|
270
|
+
target: 'draft-2020-12',
|
|
271
|
+
unrepresentable: 'throw',
|
|
272
|
+
});
|
|
273
|
+
const sources = compiled?.properties?.sources;
|
|
274
|
+
const sourcesIsArray = sources?.type === 'array'
|
|
275
|
+
|| (Array.isArray(sources?.anyOf) && sources.anyOf.some((s) => s?.type === 'array'));
|
|
276
|
+
return compiled?.type === 'object'
|
|
277
|
+
&& compiled?.properties?.$type?.const === 'gradial.image'
|
|
278
|
+
&& compiled?.properties?.fallback?.type === 'object'
|
|
279
|
+
&& sourcesIsArray;
|
|
280
|
+
} catch {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function unwrapSchema(schema) {
|
|
286
|
+
let current = schema;
|
|
287
|
+
const seen = new Set();
|
|
288
|
+
while (current && !seen.has(current)) {
|
|
289
|
+
seen.add(current);
|
|
290
|
+
if (current === GradialImageSchema) return current;
|
|
291
|
+
const def = current._def;
|
|
292
|
+
const next = def?.innerType ?? def?.schema;
|
|
293
|
+
if (!next) break;
|
|
294
|
+
current = next;
|
|
295
|
+
}
|
|
296
|
+
return current;
|
|
297
|
+
}
|
|
298
|
+
|
|
95
299
|
function validateLayout(layout, index, seen) {
|
|
96
300
|
if (!layout || typeof layout !== 'object') throw new Error(`layouts[${index}] must be an object`);
|
|
97
301
|
if (!layout.name) throw new Error(`layouts[${index}].name is required`);
|
|
@@ -182,6 +386,105 @@ function childSchemas(def) {
|
|
|
182
386
|
return children;
|
|
183
387
|
}
|
|
184
388
|
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
// Nested block detection — walks generated JSON schemas to find blockRef
|
|
391
|
+
// patterns (objects with `component` enum + `props`) and validates that
|
|
392
|
+
// referenced component names exist in the registry.
|
|
393
|
+
// ---------------------------------------------------------------------------
|
|
394
|
+
|
|
395
|
+
function collectNestedBlockRefs(schema, knownNames, componentName) {
|
|
396
|
+
const results = [];
|
|
397
|
+
_walkSchemaForBlockRefs(schema, schema, '', results, knownNames, componentName);
|
|
398
|
+
return results;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function _walkSchemaForBlockRefs(node, rootSchema, currentPath, results, knownNames, componentName) {
|
|
402
|
+
if (!node || typeof node !== 'object') return;
|
|
403
|
+
|
|
404
|
+
// Resolve $ref
|
|
405
|
+
if (node.$ref) {
|
|
406
|
+
const resolved = resolveJsonSchemaRef(node.$ref, rootSchema);
|
|
407
|
+
if (resolved) _walkSchemaForBlockRefs(resolved, rootSchema, currentPath, results, knownNames, componentName);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Check if this object matches the blockRef signature:
|
|
412
|
+
// { type: "object", properties: { id, component, props } }
|
|
413
|
+
if (isBlockRefSchema(node)) {
|
|
414
|
+
const allow = extractBlockRefAllow(node, rootSchema);
|
|
415
|
+
// Validate allowed names exist
|
|
416
|
+
for (const name of allow) {
|
|
417
|
+
if (!knownNames.has(name)) {
|
|
418
|
+
throw new Error(
|
|
419
|
+
`${componentName}: nested block allowlist references unknown component "${name}" at ${currentPath || '(root)'}`,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
results.push({
|
|
424
|
+
path: currentPath || '(root)',
|
|
425
|
+
...(allow.length > 0 ? { allow } : {}),
|
|
426
|
+
});
|
|
427
|
+
return; // Don't recurse into the block schema itself
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Recurse into object properties
|
|
431
|
+
if (node.properties) {
|
|
432
|
+
for (const [key, propSchema] of Object.entries(node.properties)) {
|
|
433
|
+
const nextPath = currentPath ? `${currentPath}.${key}` : key;
|
|
434
|
+
_walkSchemaForBlockRefs(propSchema, rootSchema, nextPath, results, knownNames, componentName);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Recurse into array items
|
|
439
|
+
if (node.items) {
|
|
440
|
+
_walkSchemaForBlockRefs(node.items, rootSchema, `${currentPath}[]`, results, knownNames, componentName);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Recurse into anyOf / allOf / oneOf
|
|
444
|
+
for (const keyword of ['anyOf', 'allOf', 'oneOf']) {
|
|
445
|
+
if (Array.isArray(node[keyword])) {
|
|
446
|
+
for (const sub of node[keyword]) {
|
|
447
|
+
_walkSchemaForBlockRefs(sub, rootSchema, currentPath, results, knownNames, componentName);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function isBlockRefSchema(node) {
|
|
454
|
+
if (node.type !== 'object' || !node.properties) return false;
|
|
455
|
+
const props = node.properties;
|
|
456
|
+
// Must have component and props fields
|
|
457
|
+
if (!props.component || !props.props) return false;
|
|
458
|
+
// component must be a string or enum
|
|
459
|
+
const comp = props.component;
|
|
460
|
+
if (comp.type !== 'string' && !Array.isArray(comp.enum)) return false;
|
|
461
|
+
// props must be an object (record)
|
|
462
|
+
if (props.props.type !== 'object') return false;
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function extractBlockRefAllow(node, rootSchema) {
|
|
467
|
+
let comp = node.properties.component;
|
|
468
|
+
// Resolve $ref on the component field
|
|
469
|
+
if (comp.$ref) {
|
|
470
|
+
comp = resolveJsonSchemaRef(comp.$ref, rootSchema) ?? comp;
|
|
471
|
+
}
|
|
472
|
+
if (Array.isArray(comp.enum)) {
|
|
473
|
+
return comp.enum.filter((v) => typeof v === 'string');
|
|
474
|
+
}
|
|
475
|
+
return [];
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function resolveJsonSchemaRef(ref, rootSchema) {
|
|
479
|
+
if (!ref || !ref.startsWith('#/')) return null;
|
|
480
|
+
const parts = ref.slice(2).split('/');
|
|
481
|
+
let current = rootSchema;
|
|
482
|
+
for (const part of parts) {
|
|
483
|
+
current = current?.[part];
|
|
484
|
+
}
|
|
485
|
+
return current ?? null;
|
|
486
|
+
}
|
|
487
|
+
|
|
185
488
|
function parseArgs(argv) {
|
|
186
489
|
const out = {};
|
|
187
490
|
for (let i = 0; i < argv.length; i++) {
|