@gradial/aci 0.1.23 → 0.1.24
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 +4 -4
- package/bin/aci-darwin-arm64 +0 -0
- package/dist/content/assets.js +1 -1
- package/dist/next/asset-route.d.ts +0 -1
- package/dist/next/asset-route.js +5 -6
- package/dist/next/config.js +25 -1
- package/dist/next/middleware.d.ts +2 -2
- package/dist/next/middleware.js +42 -25
- package/dist/next/page.d.ts +6 -0
- package/dist/next/page.js +34 -4
- package/dist/next/preview-mode.js +6 -7
- package/dist/next/server.js +4 -20
- package/dist/preview/core.d.ts +0 -2
- package/dist/preview/core.js +1 -3
- package/dist/react/Media.d.ts +1 -1
- package/dist/react/Media.js +1 -1
- package/dist/react/VideoPlayer.d.ts +307 -6
- package/dist/react/VideoPlayer.js +19 -5
- package/dist/react/index.d.ts +1 -1
- package/dist/types/image.d.ts +2 -2
- package/dist/types/media.d.ts +3 -0
- package/dist/types/video.d.ts +14 -8
- package/dist/types/video.js +3 -0
- package/package.json +2 -3
- package/src/cli/compile-registry.mjs +68 -43
- package/src/cli/verify-next-cdn-routes.mjs +101 -0
- package/src/types/component.ts +55 -0
- package/src/types/config.ts +37 -0
- package/src/types/data.ts +47 -0
- package/src/types/image.ts +4 -3
- package/src/types/index.ts +11 -0
- package/src/types/layout.ts +29 -0
- package/src/types/link.ts +100 -0
- package/src/types/media.ts +34 -0
- package/src/types/page.ts +47 -0
- package/src/types/render-mode.ts +10 -0
- package/src/types/renderer.ts +83 -0
- package/src/types/rich-text.ts +64 -0
- package/src/types/video.ts +76 -0
- package/templates/astro/template/package.json.tmpl +1 -1
- package/templates/nextjs/template/package.json.tmpl +1 -1
- package/templates/nextjs/template/src/app/gradial/assets/[...path]/route.ts +4 -0
- package/templates/nextjs/template/src/app/gradial/assets/[releaseId]/[...path]/route.ts +0 -1
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import { ImageSchema } from '../types/image.ts';
|
|
7
|
+
import { VideoSchema } from '../types/video.ts';
|
|
7
8
|
import { getBlockRefMeta } from '../types/block-ref.ts';
|
|
8
9
|
|
|
9
10
|
const DATA_TYPE_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
@@ -147,15 +148,17 @@ function validateComponent(component, index, seen) {
|
|
|
147
148
|
|
|
148
149
|
function validateImageSlotSchemaMatch(component, index) {
|
|
149
150
|
const imageSlots = normalizeImageSlots(component.imageSlots);
|
|
150
|
-
const
|
|
151
|
+
const mediaFields = collectMediaFields(component.props);
|
|
152
|
+
const imageFields = new Set(mediaFields.filter(([, k]) => k === 'image').map(([p]) => p));
|
|
153
|
+
const allMediaFields = new Set(mediaFields.map(([p]) => p));
|
|
151
154
|
for (const field of imageFields) {
|
|
152
155
|
if (!Object.prototype.hasOwnProperty.call(imageSlots, field)) {
|
|
153
156
|
throw new Error(`components[${index}].imageSlots.${field} is required because props field ${field} uses an image schema`);
|
|
154
157
|
}
|
|
155
158
|
}
|
|
156
159
|
for (const field of Object.keys(imageSlots)) {
|
|
157
|
-
if (!
|
|
158
|
-
throw new Error(`components[${index}].props.${field} must use an image schema because imageSlots.${field} is declared`);
|
|
160
|
+
if (!allMediaFields.has(field)) {
|
|
161
|
+
throw new Error(`components[${index}].props.${field} must use an image or asset schema because imageSlots.${field} is declared`);
|
|
159
162
|
}
|
|
160
163
|
}
|
|
161
164
|
}
|
|
@@ -272,48 +275,35 @@ function normalizeSlotOutput(slotKey, output, outputIndex) {
|
|
|
272
275
|
};
|
|
273
276
|
}
|
|
274
277
|
|
|
275
|
-
|
|
276
|
-
|
|
278
|
+
/**
|
|
279
|
+
* Walks a Zod schema and returns [dotPath, kind] pairs for every field
|
|
280
|
+
* that contains an image or asset schema.
|
|
281
|
+
* kind = 'image' for ImageSchema-only fields, 'asset' for unions (image | video).
|
|
282
|
+
*/
|
|
283
|
+
function collectMediaFields(schema, prefix = '') {
|
|
284
|
+
if (!schema) return [];
|
|
277
285
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
return out;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function collectImageFieldsFromSchema(schema, prefix, out) {
|
|
284
|
-
if (!schema) {
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (prefix && isImageSchema(schema)) {
|
|
289
|
-
out.add(prefix);
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
286
|
+
if (prefix && isImageSchema(schema)) return [[prefix, 'image']];
|
|
287
|
+
if (prefix && isVideoSchema(schema)) return [];
|
|
292
288
|
|
|
293
289
|
const itemSchema = arrayItemSchema(schema);
|
|
294
|
-
if (itemSchema)
|
|
295
|
-
collectImageFieldsFromSchema(itemSchema, prefix, out);
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
290
|
+
if (itemSchema) return collectMediaFields(itemSchema, prefix);
|
|
298
291
|
|
|
299
292
|
const unionOptions = unionOptionSchemas(schema);
|
|
300
293
|
if (unionOptions) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
return;
|
|
294
|
+
const hasImage = unionOptions.some((opt) => isImageSchema(opt));
|
|
295
|
+
const hasVideo = unionOptions.some((opt) => isVideoSchema(opt));
|
|
296
|
+
if (prefix && hasImage && hasVideo) return [[prefix, 'asset']];
|
|
297
|
+
if (prefix && hasVideo) return [];
|
|
298
|
+
return unionOptions.flatMap((opt) => collectMediaFields(opt, prefix));
|
|
305
299
|
}
|
|
306
300
|
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
if (!shape || typeof shape !== 'object') {
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
301
|
+
const shape = getSchemaShape(unwrapSchema(schema));
|
|
302
|
+
if (!shape) return [];
|
|
312
303
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
304
|
+
return Object.entries(shape).flatMap(([field, fieldSchema]) =>
|
|
305
|
+
collectMediaFields(fieldSchema, prefix ? `${prefix}.${field}` : field),
|
|
306
|
+
);
|
|
317
307
|
}
|
|
318
308
|
|
|
319
309
|
function unionOptionSchemas(schema) {
|
|
@@ -369,6 +359,21 @@ function isImageSchema(schema) {
|
|
|
369
359
|
}
|
|
370
360
|
}
|
|
371
361
|
|
|
362
|
+
function isVideoSchema(schema) {
|
|
363
|
+
const unwrapped = unwrapSchema(schema);
|
|
364
|
+
if (unwrapped === VideoSchema) return true;
|
|
365
|
+
try {
|
|
366
|
+
const compiled = z.toJSONSchema(unwrapped, {
|
|
367
|
+
target: 'draft-2020-12',
|
|
368
|
+
unrepresentable: 'throw',
|
|
369
|
+
});
|
|
370
|
+
return compiled?.type === 'object'
|
|
371
|
+
&& compiled?.properties?.$type?.const === 'gradial.video';
|
|
372
|
+
} catch {
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
372
377
|
function unwrapSchema(schema) {
|
|
373
378
|
let current = schema;
|
|
374
379
|
const seen = new Set();
|
|
@@ -451,6 +456,25 @@ async function generatePageSchema(schemaDir, componentEntries, outDir) {
|
|
|
451
456
|
alt: { type: 'string' },
|
|
452
457
|
},
|
|
453
458
|
},
|
|
459
|
+
staticAssetRef: {
|
|
460
|
+
type: 'object',
|
|
461
|
+
required: ['$type', 'src'],
|
|
462
|
+
additionalProperties: true,
|
|
463
|
+
properties: {
|
|
464
|
+
$type: { const: 'static.assetRef' },
|
|
465
|
+
src: { type: 'string', minLength: 1 },
|
|
466
|
+
alt: { type: 'string' },
|
|
467
|
+
mediaType: { type: 'string' },
|
|
468
|
+
width: { type: 'number' },
|
|
469
|
+
height: { type: 'number' },
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
assetRef: {
|
|
473
|
+
oneOf: [
|
|
474
|
+
{ $ref: '#/$defs/damAssetRef' },
|
|
475
|
+
{ $ref: '#/$defs/staticAssetRef' },
|
|
476
|
+
],
|
|
477
|
+
},
|
|
454
478
|
};
|
|
455
479
|
const oneOf = [];
|
|
456
480
|
for (const entry of componentEntries) {
|
|
@@ -524,25 +548,26 @@ function replaceImagesWithDamAssetRef(schema, damAssetRef) {
|
|
|
524
548
|
}
|
|
525
549
|
if (schema.properties) {
|
|
526
550
|
for (const [key, value] of Object.entries(schema.properties)) {
|
|
527
|
-
if (
|
|
528
|
-
schema.properties[key] = { $ref: '#/$defs/
|
|
551
|
+
if (isJsonAssetRef(value)) {
|
|
552
|
+
schema.properties[key] = { $ref: '#/$defs/assetRef' };
|
|
529
553
|
} else {
|
|
530
554
|
replaceImagesWithDamAssetRef(value, damAssetRef);
|
|
531
555
|
}
|
|
532
556
|
}
|
|
533
557
|
}
|
|
534
558
|
if (schema.items) {
|
|
535
|
-
if (
|
|
536
|
-
schema.items = { $ref: '#/$defs/
|
|
559
|
+
if (isJsonAssetRef(schema.items)) {
|
|
560
|
+
schema.items = { $ref: '#/$defs/assetRef' };
|
|
537
561
|
} else {
|
|
538
562
|
replaceImagesWithDamAssetRef(schema.items, damAssetRef);
|
|
539
563
|
}
|
|
540
564
|
}
|
|
541
565
|
}
|
|
542
566
|
|
|
543
|
-
function
|
|
544
|
-
|
|
545
|
-
|
|
567
|
+
function isJsonAssetRef(node) {
|
|
568
|
+
if (node?.type !== 'object' || !node?.properties?.$type) return false;
|
|
569
|
+
const constVal = node.properties.$type.const;
|
|
570
|
+
return constVal === 'gradial.image' || constVal === 'gradial.video';
|
|
546
571
|
}
|
|
547
572
|
|
|
548
573
|
function validateZodSubset(schema, pathLabel, seen = new Set()) {
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
const projectRoot = process.argv[2] || '.';
|
|
7
|
+
|
|
8
|
+
const checks = [];
|
|
9
|
+
const missing = [];
|
|
10
|
+
|
|
11
|
+
function checkFile(label, relativePath, validate, message) {
|
|
12
|
+
const fullPath = path.join(projectRoot, relativePath);
|
|
13
|
+
if (!fs.existsSync(fullPath)) {
|
|
14
|
+
missing.push(`✗ ${label}: ${relativePath} not found`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
18
|
+
if (!validate(content)) {
|
|
19
|
+
checks.push(`✗ ${label}: ${message}`);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
checks.push(`✓ ${label}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function checkMissing(label, relativePath, message) {
|
|
26
|
+
const fullPath = path.join(projectRoot, relativePath);
|
|
27
|
+
if (fs.existsSync(fullPath)) {
|
|
28
|
+
checks.push(`✗ ${label}: ${relativePath} should be removed. ${message}`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
checks.push(`✓ ${label}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function checkOptionalFile(label, relativePath, validate, message) {
|
|
35
|
+
const fullPath = path.join(projectRoot, relativePath);
|
|
36
|
+
if (!fs.existsSync(fullPath)) {
|
|
37
|
+
checks.push(`- ${label}: ${relativePath} not found (optional)`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
41
|
+
if (!validate(content)) {
|
|
42
|
+
checks.push(`✗ ${label}: ${message}`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
checks.push(`✓ ${label}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
checkFile(
|
|
49
|
+
'middleware is configured',
|
|
50
|
+
'src/middleware.ts',
|
|
51
|
+
(content) => (content.includes('@gradial/aci/next/middleware') || content.includes('createGradialMiddleware'))
|
|
52
|
+
&& !content.includes('|__r'),
|
|
53
|
+
'Expected src/middleware.ts to use @gradial/aci/next/middleware and not exclude __r; middleware blocks direct /__r requests.',
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
checkFile(
|
|
57
|
+
'catch-all page route exists',
|
|
58
|
+
'src/app/[[...slug]]/page.tsx',
|
|
59
|
+
(content) => content.includes('createPage') || content.includes('getRenderInput'),
|
|
60
|
+
'Expected catch-all page to use createPage() from the SDK.',
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
checkFile(
|
|
64
|
+
'public asset route exists',
|
|
65
|
+
'src/app/gradial/assets/[...path]/route.ts',
|
|
66
|
+
(content) => content.includes('@gradial/aci/next/asset-route'),
|
|
67
|
+
'Expected public asset route to re-export @gradial/aci/next/asset-route.',
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
checkMissing(
|
|
71
|
+
'legacy release-scoped asset route is removed',
|
|
72
|
+
'src/app/gradial/assets/[releaseId]/[...path]/route.ts',
|
|
73
|
+
'Use src/app/gradial/assets/[...path]/route.ts instead. Assets are now public, not release-scoped.',
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
checkMissing(
|
|
77
|
+
'no separate __r route folder',
|
|
78
|
+
'src/app/__r/[releaseId]/routes/[[...slug]]/page.tsx',
|
|
79
|
+
'The root catch-all handles release-scoped rewrites. Do not create a separate __r route folder.',
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
checkFile(
|
|
83
|
+
'next.config uses withAci',
|
|
84
|
+
'next.config.ts',
|
|
85
|
+
(content) => content.includes('withAci'),
|
|
86
|
+
'Expected next.config.ts to use withAci() from @gradial/aci/next.',
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
for (const line of checks) {
|
|
90
|
+
console.log(line);
|
|
91
|
+
}
|
|
92
|
+
for (const line of missing) {
|
|
93
|
+
console.log(line);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const failed = [...checks.filter((line) => line.startsWith('✗')), ...missing];
|
|
97
|
+
if (failed.length > 0) {
|
|
98
|
+
console.error(`\n${failed.length} check(s) failed`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
console.log('\nAll route checks passed');
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ImageSlotContract } from './image.js';
|
|
2
|
+
import type { BlockRefMeta } from './block-ref.js';
|
|
3
|
+
import type { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
type InferContentProps<TSchema> = TSchema extends z.ZodType<infer T> ? T : Record<string, unknown>;
|
|
6
|
+
|
|
7
|
+
export interface ComponentContractValidator<TProps = Record<string, unknown>> {
|
|
8
|
+
(context: ComponentValidationContext<TProps>): void | Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ComponentValidationContext<TProps = Record<string, unknown>> {
|
|
12
|
+
component: string;
|
|
13
|
+
props: TProps;
|
|
14
|
+
blockId?: string;
|
|
15
|
+
path: readonly string[];
|
|
16
|
+
report(issue: ComponentValidationIssue): void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ComponentValidationIssue {
|
|
20
|
+
path?: readonly string[];
|
|
21
|
+
code?: string;
|
|
22
|
+
message: string;
|
|
23
|
+
severity?: 'error' | 'warning';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ComponentDefinition<TId extends string = string, TSchema = unknown> {
|
|
27
|
+
id: TId;
|
|
28
|
+
displayName?: string;
|
|
29
|
+
props: TSchema;
|
|
30
|
+
imageSlots?: Record<string, ImageSlotContract>;
|
|
31
|
+
validate?: readonly ComponentContractValidator<InferContentProps<TSchema>>[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ComponentContract<TId extends string = string, TSchema = unknown> {
|
|
35
|
+
readonly id: TId;
|
|
36
|
+
readonly displayName?: string;
|
|
37
|
+
props: TSchema;
|
|
38
|
+
blockSlots?: Record<string, BlockRefMeta>;
|
|
39
|
+
imageSlots?: Record<string, ImageSlotContract>;
|
|
40
|
+
validate: readonly ComponentContractValidator<InferContentProps<TSchema>>[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type InferComponentProps<TContract> = TContract extends ComponentContract<string, infer TSchema>
|
|
44
|
+
? InferContentProps<TSchema>
|
|
45
|
+
: Record<string, unknown>;
|
|
46
|
+
|
|
47
|
+
export type ComponentContractID<TContract> = TContract extends ComponentContract<infer TId, unknown>
|
|
48
|
+
? TId
|
|
49
|
+
: never;
|
|
50
|
+
|
|
51
|
+
export type ComponentRegistry<TContracts extends Record<string, ComponentContract>> = {
|
|
52
|
+
readonly [K in keyof TContracts]: TContracts[K] & { readonly id: K & string };
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type { InferContentProps };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { RenderCapabilities } from './renderer.js';
|
|
2
|
+
|
|
3
|
+
export interface Config {
|
|
4
|
+
version: '1';
|
|
5
|
+
siteId: string;
|
|
6
|
+
framework: 'astro' | 'next' | 'sveltekit' | 'custom';
|
|
7
|
+
source: {
|
|
8
|
+
root: string;
|
|
9
|
+
outDir?: string;
|
|
10
|
+
publicDir?: string;
|
|
11
|
+
};
|
|
12
|
+
componentRegistry: string;
|
|
13
|
+
dataRegistry?: string;
|
|
14
|
+
layoutRegistry: string;
|
|
15
|
+
capabilities: RenderCapabilities;
|
|
16
|
+
routes: RouteConfig;
|
|
17
|
+
dam?: DAMConfig;
|
|
18
|
+
externalDependencies?: ExternalDependency[];
|
|
19
|
+
rendererProtocol: 'http' | 'stdio-json';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface RouteConfig {
|
|
23
|
+
cmsManaged: string;
|
|
24
|
+
frameworkOwned?: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ExternalDependency {
|
|
28
|
+
name: string;
|
|
29
|
+
host: string;
|
|
30
|
+
kind?: 'api' | 'client-script';
|
|
31
|
+
description?: string;
|
|
32
|
+
cacheTTL?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface DAMConfig {
|
|
36
|
+
autoApproveUploads?: boolean;
|
|
37
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
type InferDataProps<TSchema> = TSchema extends z.ZodType<infer T>
|
|
4
|
+
? T
|
|
5
|
+
: Record<string, unknown>;
|
|
6
|
+
|
|
7
|
+
const dataRefPattern = /^\$ref:data\/[A-Za-z0-9][A-Za-z0-9_./-]*$/;
|
|
8
|
+
|
|
9
|
+
export interface DataModelDefinition<TSchema = unknown, TType extends string = string> {
|
|
10
|
+
type: TType;
|
|
11
|
+
schema: TSchema;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface DataModelContract<TSchema = unknown, TType extends string = string> {
|
|
15
|
+
type: TType;
|
|
16
|
+
schema: TSchema;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DataEntry<TProps = Record<string, unknown>, TType extends string = string> {
|
|
20
|
+
$type: TType;
|
|
21
|
+
props: TProps;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type InferDataModelProps<TContract> = TContract extends DataModelContract<infer TSchema, string>
|
|
25
|
+
? InferDataProps<TSchema>
|
|
26
|
+
: Record<string, unknown>;
|
|
27
|
+
|
|
28
|
+
export type InferDataModelType<TContract> = TContract extends DataModelContract<unknown, infer TType>
|
|
29
|
+
? TType
|
|
30
|
+
: string;
|
|
31
|
+
|
|
32
|
+
export type DataModelRef<TContract extends DataModelContract> = DataEntry<
|
|
33
|
+
InferDataModelProps<TContract>,
|
|
34
|
+
InferDataModelType<TContract>
|
|
35
|
+
>;
|
|
36
|
+
|
|
37
|
+
export function dataModelRef<TContract extends DataModelContract<z.ZodTypeAny>>(
|
|
38
|
+
contract: TContract,
|
|
39
|
+
): z.ZodType<DataModelRef<TContract>> {
|
|
40
|
+
return z.union([
|
|
41
|
+
z.string().regex(dataRefPattern, `expected a $ref:data/... pointer for ${contract.type}`),
|
|
42
|
+
z.object({
|
|
43
|
+
$type: z.literal(contract.type),
|
|
44
|
+
props: contract.schema,
|
|
45
|
+
}),
|
|
46
|
+
]).describe(`data model reference: ${contract.type}`) as unknown as z.ZodType<DataModelRef<TContract>>;
|
|
47
|
+
}
|
package/src/types/image.ts
CHANGED
|
@@ -22,15 +22,16 @@ export interface ImageSource {
|
|
|
22
22
|
// Image — DAM-backed image asset with compiler-resolved derivatives
|
|
23
23
|
//
|
|
24
24
|
// Content authors write an asset reference: { "$type": "dam.assetRef", ... }
|
|
25
|
-
//
|
|
25
|
+
// or for static files: { "$type": "static.assetRef", "src": "/path" }
|
|
26
|
+
// The compiler resolves both to this Image value.
|
|
26
27
|
// ---------------------------------------------------------------------------
|
|
27
28
|
|
|
28
29
|
export interface Image {
|
|
29
30
|
/** Discriminator — always 'gradial.image'. */
|
|
30
31
|
$type: 'gradial.image';
|
|
31
|
-
/** DAM asset identifier. */
|
|
32
|
+
/** DAM asset identifier (or "static:<path>" for non-DAM assets). */
|
|
32
33
|
assetId: string;
|
|
33
|
-
/** DAM version identifier. */
|
|
34
|
+
/** DAM version identifier (or "static" for non-DAM assets). */
|
|
34
35
|
versionId: string;
|
|
35
36
|
/** Accessible description of the image content. */
|
|
36
37
|
alt: string;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from './config.js';
|
|
2
|
+
export * from './component.js';
|
|
3
|
+
export * from './data.js';
|
|
4
|
+
export * from './layout.js';
|
|
5
|
+
export * from './page.js';
|
|
6
|
+
export * from './rich-text.js';
|
|
7
|
+
export * from './renderer.js';
|
|
8
|
+
export * from './render-mode.js';
|
|
9
|
+
export * from './image.js';
|
|
10
|
+
export * from './video.js';
|
|
11
|
+
export * from './media.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { FragmentRefNode } from './page.js';
|
|
2
|
+
|
|
3
|
+
export interface LayoutSlot {
|
|
4
|
+
name: string;
|
|
5
|
+
required: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Default content for non-required layout slots, keyed by slot name. */
|
|
9
|
+
export type LayoutDefaults = Record<string, FragmentRefNode[]>;
|
|
10
|
+
|
|
11
|
+
export interface LayoutDefinition {
|
|
12
|
+
name: string;
|
|
13
|
+
slots: LayoutSlot[];
|
|
14
|
+
/** Default fragment references for slots not provided by the page. */
|
|
15
|
+
defaults?: LayoutDefaults;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface LayoutContract {
|
|
19
|
+
name: string;
|
|
20
|
+
slots: LayoutSlot[];
|
|
21
|
+
defaults?: LayoutDefaults;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A compiled layout file as stored on disk. */
|
|
25
|
+
export interface CompiledLayout {
|
|
26
|
+
name: string;
|
|
27
|
+
slots: LayoutSlot[];
|
|
28
|
+
defaults: LayoutDefaults;
|
|
29
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Link — internal or external hyperlink target
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
export type LinkTarget = '_self' | '_blank' | '_parent' | '_top';
|
|
8
|
+
|
|
9
|
+
export interface Link {
|
|
10
|
+
href: string;
|
|
11
|
+
target?: LinkTarget;
|
|
12
|
+
title?: string;
|
|
13
|
+
rel?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Zod schema
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export const LinkSchema = z.object({
|
|
21
|
+
href: z.string().min(1),
|
|
22
|
+
target: z.enum(['_self', '_blank', '_parent', '_top']).optional(),
|
|
23
|
+
title: z.string().optional(),
|
|
24
|
+
rel: z.string().optional(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Link resolver — turns a reference string into a Link object
|
|
29
|
+
//
|
|
30
|
+
// Supported reference schemes:
|
|
31
|
+
// aci:page:{slug} → resolved by the consumer (e.g. Next.js/Astro router)
|
|
32
|
+
// aci:external:{url} → external URL
|
|
33
|
+
// {url} → literal URL
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
export interface LinkResolverContext {
|
|
37
|
+
/** Resolved path for an aci:page:{slug} reference, if found. */
|
|
38
|
+
resolvePageSlug?: (slug: string) => string | undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function resolveLink(
|
|
42
|
+
reference: string | Link,
|
|
43
|
+
ctx: LinkResolverContext = {},
|
|
44
|
+
): Link {
|
|
45
|
+
if (typeof reference !== 'string') return reference;
|
|
46
|
+
|
|
47
|
+
if (reference.startsWith('aci:external:')) {
|
|
48
|
+
return { href: reference.slice('aci:external:'.length), target: '_blank' };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (reference.startsWith('aci:page:')) {
|
|
52
|
+
const slug = reference.slice('aci:page:'.length);
|
|
53
|
+
const path = ctx.resolvePageSlug?.(slug);
|
|
54
|
+
return { href: path ?? `/${slug}` };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { href: reference };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolve a Link object to its final href string.
|
|
62
|
+
* An optional resolver can perform async or domain-specific resolution.
|
|
63
|
+
*/
|
|
64
|
+
export function resolveHref(
|
|
65
|
+
link: Link,
|
|
66
|
+
resolver?: (link: Link) => string | Promise<string>,
|
|
67
|
+
): string | Promise<string> {
|
|
68
|
+
if (resolver) {
|
|
69
|
+
return resolver(link);
|
|
70
|
+
}
|
|
71
|
+
return link.href;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Shared helpers — consumed by React, Astro, and Svelte components
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Throw if the href uses an unsafe protocol (javascript:, data:, vbscript:).
|
|
80
|
+
*/
|
|
81
|
+
export function assertSafeHref(href: string): void {
|
|
82
|
+
const protocol = href.split(':')[0].toLowerCase();
|
|
83
|
+
if (protocol === 'javascript' || protocol === 'data' || protocol === 'vbscript') {
|
|
84
|
+
throw new Error(`unsafe Link protocol: ${protocol}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Normalize the rel attribute for a Link.
|
|
90
|
+
* Adds noopener noreferrer for target="_blank" automatically.
|
|
91
|
+
*/
|
|
92
|
+
export function normalizeLinkRel(link: Link): string | undefined {
|
|
93
|
+
if (link.target !== '_blank') {
|
|
94
|
+
return link.rel;
|
|
95
|
+
}
|
|
96
|
+
const rel = new Set((link.rel ?? '').split(/\s+/).filter(Boolean));
|
|
97
|
+
rel.add('noopener');
|
|
98
|
+
rel.add('noreferrer');
|
|
99
|
+
return [...rel].join(' ');
|
|
100
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ImageSchema, type Image } from './image.js';
|
|
3
|
+
import { VideoSchema, type Video } from './video.js';
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Asset — union of all compiler-resolved asset types
|
|
7
|
+
//
|
|
8
|
+
// Content authors write either:
|
|
9
|
+
// { "$type": "dam.assetRef", "assetId": "..." } — resolved via DAM
|
|
10
|
+
// { "$type": "static.assetRef", "src": "/path" } — resolved as-is
|
|
11
|
+
//
|
|
12
|
+
// The compiler normalizes both into Image or Video values.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
/** Discriminated union of all compiler-resolved asset types. */
|
|
16
|
+
export type Asset = Image | Video;
|
|
17
|
+
|
|
18
|
+
/** Zod schema for a resolved asset value. */
|
|
19
|
+
export const AssetSchema = z.union([ImageSchema, VideoSchema]);
|
|
20
|
+
|
|
21
|
+
/** Runtime guard: value is a resolved asset. */
|
|
22
|
+
export function isAsset(value: unknown): value is Asset {
|
|
23
|
+
return ImageSchema.safeParse(value).success || VideoSchema.safeParse(value).success;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Runtime guard: value is a resolved image asset. */
|
|
27
|
+
export function isImage(value: unknown): value is Image {
|
|
28
|
+
return ImageSchema.safeParse(value).success;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Runtime guard: value is a resolved video asset. */
|
|
32
|
+
export function isVideo(value: unknown): value is Video {
|
|
33
|
+
return VideoSchema.safeParse(value).success;
|
|
34
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { VaryDimension } from './render-mode.js';
|
|
2
|
+
|
|
3
|
+
export interface PageDocument {
|
|
4
|
+
path: string;
|
|
5
|
+
layout: string;
|
|
6
|
+
locale: string;
|
|
7
|
+
metadata: PageMetadata;
|
|
8
|
+
regions: Record<string, RegionNode>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface PageMetadata {
|
|
12
|
+
title: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
openGraph?: Record<string, string>;
|
|
15
|
+
canonicalUrl?: string;
|
|
16
|
+
alternateLocales?: { locale: string; path: string }[];
|
|
17
|
+
customHead?: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface RegionNode {
|
|
21
|
+
kind: 'region';
|
|
22
|
+
name: string;
|
|
23
|
+
children: Array<BlockNode | FragmentRefNode>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface BlockNode {
|
|
27
|
+
kind: 'block';
|
|
28
|
+
id: string;
|
|
29
|
+
component: string;
|
|
30
|
+
props: Record<string, unknown>;
|
|
31
|
+
renderHints: BlockRenderHints;
|
|
32
|
+
contentRef?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface BlockRenderHints {
|
|
36
|
+
canStatic: boolean;
|
|
37
|
+
needsSSR: boolean;
|
|
38
|
+
isIsland: boolean;
|
|
39
|
+
islandMode?: 'ssr' | 'client';
|
|
40
|
+
varyDimensions?: VaryDimension[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface FragmentRefNode {
|
|
44
|
+
kind: 'fragment-ref';
|
|
45
|
+
fragmentId: string;
|
|
46
|
+
inline: boolean;
|
|
47
|
+
}
|