@gradial/aci 0.1.19 → 0.1.20-preview.1
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 +28 -8
- package/bin/aci +0 -0
- package/dist/astro/index.d.ts +4 -2
- package/dist/astro/index.js +9 -1
- package/dist/astro/preview.d.ts +5 -0
- package/dist/astro/preview.js +33 -0
- package/dist/content/index.d.ts +1 -0
- package/dist/content/index.js +1 -0
- package/dist/content/provider.d.ts +19 -0
- package/dist/content/provider.js +7 -0
- package/dist/content/resolve-slots.d.ts +9 -0
- package/dist/content/resolve-slots.js +24 -0
- package/dist/content/types.d.ts +1 -1
- package/dist/content/validation.js +0 -1
- package/dist/define-component.js +0 -6
- package/dist/define-data-model.d.ts +2 -0
- package/dist/define-data-model.js +3 -0
- package/dist/define-layout.d.ts +2 -0
- package/dist/define-layout.js +3 -0
- package/dist/dev/index.d.ts +12 -0
- package/dist/dev/index.js +84 -9
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/next/asset-route.d.ts +2 -0
- package/dist/next/asset-route.js +10 -2
- package/dist/next/config.d.ts +1 -1
- package/dist/next/config.js +14 -2
- package/dist/next/content-watch.d.ts +11 -0
- package/dist/next/content-watch.js +106 -0
- package/dist/next/index.d.ts +2 -2
- package/dist/next/index.js +2 -2
- package/dist/next/middleware.d.ts +6 -2
- package/dist/next/middleware.js +40 -51
- package/dist/next/page.d.ts +41 -0
- package/dist/next/page.js +77 -0
- package/dist/next/preview-mode.js +4 -8
- package/dist/next/render.d.ts +5 -0
- package/dist/next/render.js +15 -0
- package/dist/next/root-layout.d.ts +4 -0
- package/dist/next/root-layout.js +5 -0
- package/dist/next/server.d.ts +3 -2
- package/dist/next/server.js +12 -8
- package/dist/preview/core.d.ts +41 -0
- package/dist/preview/core.js +116 -0
- package/dist/providers/file.d.ts +2 -0
- package/dist/providers/file.js +61 -1
- package/dist/providers/s3.d.ts +1 -0
- package/dist/providers/s3.js +14 -1
- package/dist/registry.d.ts +15 -0
- package/dist/registry.js +10 -0
- package/dist/sveltekit/index.d.ts +4 -1
- package/dist/sveltekit/index.js +9 -1
- package/dist/sveltekit/preview.d.ts +5 -0
- package/dist/sveltekit/preview.js +33 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.js +15 -2
- package/dist/types/component.d.ts +0 -9
- package/dist/types/config.d.ts +1 -1
- package/dist/types/data.d.ts +19 -0
- package/dist/types/data.js +11 -0
- package/dist/types/image.d.ts +1 -1
- package/dist/types/image.js +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/page.d.ts +1 -2
- package/dist/types/render-mode.d.ts +0 -1
- package/package.json +40 -6
- package/src/cli/compile-registry.mjs +167 -19
- package/src/types/component.ts +0 -10
- package/src/types/config.ts +1 -1
- package/src/types/data.ts +47 -0
- package/src/types/image.ts +2 -2
- package/src/types/index.ts +1 -0
- package/src/types/page.ts +1 -2
- package/src/types/render-mode.ts +0 -8
- package/dist/next/preview-banner.d.ts +0 -4
- package/dist/next/preview-banner.js +0 -51
- package/dist/next/preview.d.ts +0 -7
- package/dist/next/preview.js +0 -37
- package/dist/render.d.ts +0 -14
- package/dist/render.js +0 -33
- package/src/cli/verify-renderer.mjs +0 -73
|
@@ -5,6 +5,8 @@ import { pathToFileURL } from 'node:url';
|
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import { GradialImageSchema } from '../types/image.ts';
|
|
7
7
|
|
|
8
|
+
const DATA_TYPE_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
9
|
+
|
|
8
10
|
// Shim React global so JSX-containing component files can be loaded.
|
|
9
11
|
// The compile-registry only extracts contract metadata (schemas, imageSlots)
|
|
10
12
|
// and never renders components, but importing .tsx files requires the JSX
|
|
@@ -33,6 +35,9 @@ async function compileRegistry(options) {
|
|
|
33
35
|
|
|
34
36
|
const components = await loadDefaultArray(options.components, 'component registry');
|
|
35
37
|
const layouts = await loadDefaultArray(options.layouts, 'layout registry');
|
|
38
|
+
const dataModels = options.data
|
|
39
|
+
? await loadDefaultArray(options.data, 'data model registry')
|
|
40
|
+
: [];
|
|
36
41
|
const outDir = path.resolve(options.output);
|
|
37
42
|
const schemaDir = path.join(outDir, 'schemas');
|
|
38
43
|
await mkdir(schemaDir, { recursive: true });
|
|
@@ -47,22 +52,11 @@ async function compileRegistry(options) {
|
|
|
47
52
|
const schemaPath = path.join(outDir, schemaFile);
|
|
48
53
|
await writeFile(schemaPath, JSON.stringify(schema, null, 2) + '\n');
|
|
49
54
|
schemaFiles.push(schemaPath);
|
|
50
|
-
const renderModes = component.renderModes ?? component.render;
|
|
51
55
|
const varyDimensions = component.varyDimensions ?? component.vary;
|
|
52
56
|
const imageSlots = normalizeImageSlots(component.imageSlots);
|
|
53
57
|
componentEntries.push({
|
|
54
58
|
name: component.name,
|
|
55
59
|
schemaFile,
|
|
56
|
-
render: {
|
|
57
|
-
static: Boolean(renderModes.canStatic ?? renderModes.static),
|
|
58
|
-
ssr: Boolean(renderModes.canSSR ?? renderModes.ssr),
|
|
59
|
-
clientIsland: Boolean(renderModes.canClientIsland ?? renderModes.clientIsland),
|
|
60
|
-
},
|
|
61
|
-
renderModes: {
|
|
62
|
-
canStatic: Boolean(renderModes.canStatic ?? renderModes.static),
|
|
63
|
-
canSSR: Boolean(renderModes.canSSR ?? renderModes.ssr),
|
|
64
|
-
canClientIsland: Boolean(renderModes.canClientIsland ?? renderModes.clientIsland),
|
|
65
|
-
},
|
|
66
60
|
...(component.defaultIslandMode ? { defaultIslandMode: component.defaultIslandMode } : {}),
|
|
67
61
|
...(varyDimensions?.length ? { vary: varyDimensions, varyDimensions } : {}),
|
|
68
62
|
...(Object.keys(imageSlots).length ? { imageSlots } : {}),
|
|
@@ -72,6 +66,24 @@ async function compileRegistry(options) {
|
|
|
72
66
|
const seenLayouts = new Set();
|
|
73
67
|
const layoutEntries = layouts.map((layout, index) => validateLayout(layout, index, seenLayouts));
|
|
74
68
|
|
|
69
|
+
const seenDataModels = new Set();
|
|
70
|
+
const dataModelEntries = [];
|
|
71
|
+
if (dataModels.length > 0) {
|
|
72
|
+
await mkdir(path.join(schemaDir, 'data'), { recursive: true });
|
|
73
|
+
}
|
|
74
|
+
for (const [index, dataModel] of dataModels.entries()) {
|
|
75
|
+
validateDataModel(dataModel, index, seenDataModels);
|
|
76
|
+
const schema = compileSchema(dataModel.schema, dataModel.type);
|
|
77
|
+
const schemaFile = `schemas/data/${dataModel.type}.schema.json`;
|
|
78
|
+
const schemaPath = path.join(outDir, schemaFile);
|
|
79
|
+
await writeFile(schemaPath, JSON.stringify(schema, null, 2) + '\n');
|
|
80
|
+
schemaFiles.push(schemaPath);
|
|
81
|
+
dataModelEntries.push({
|
|
82
|
+
type: dataModel.type,
|
|
83
|
+
schemaFile,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
75
87
|
// Post-compilation: detect nested block patterns in JSON schemas and
|
|
76
88
|
// validate that referenced component names exist in the registry.
|
|
77
89
|
const knownNames = new Set(componentEntries.map((c) => c.name));
|
|
@@ -90,19 +102,39 @@ async function compileRegistry(options) {
|
|
|
90
102
|
generatedAt: new Date().toISOString(),
|
|
91
103
|
components: componentEntries,
|
|
92
104
|
layouts: layoutEntries,
|
|
105
|
+
dataModels: dataModelEntries,
|
|
93
106
|
};
|
|
94
107
|
const registryPath = path.join(outDir, 'registry.json');
|
|
95
108
|
await writeFile(registryPath, JSON.stringify(registry, null, 2) + '\n');
|
|
109
|
+
|
|
110
|
+
const pageSchemaPath = await generatePageSchema(schemaDir, componentEntries, outDir);
|
|
111
|
+
schemaFiles.push(pageSchemaPath);
|
|
112
|
+
|
|
96
113
|
return { registry: registryPath, schemas: schemaFiles };
|
|
97
114
|
}
|
|
98
115
|
|
|
99
116
|
async function loadDefaultArray(modulePath, label) {
|
|
100
117
|
const mod = await import(pathToFileURL(path.resolve(modulePath)).href);
|
|
101
|
-
const value = mod.default ?? mod.registry ?? mod.components ?? mod.layouts;
|
|
118
|
+
const value = mod.default ?? mod.registry ?? mod.components ?? mod.layouts ?? mod.dataModels;
|
|
102
119
|
if (!Array.isArray(value)) throw new Error(`${label} must export an array`);
|
|
103
120
|
return value;
|
|
104
121
|
}
|
|
105
122
|
|
|
123
|
+
function validateDataModel(dataModel, index, seen) {
|
|
124
|
+
if (!dataModel || typeof dataModel !== 'object') {
|
|
125
|
+
throw new Error(`dataModels[${index}] must be an object`);
|
|
126
|
+
}
|
|
127
|
+
if (typeof dataModel.type !== 'string' || !DATA_TYPE_PATTERN.test(dataModel.type)) {
|
|
128
|
+
throw new Error(`dataModels[${index}].type must match ${DATA_TYPE_PATTERN}`);
|
|
129
|
+
}
|
|
130
|
+
if (seen.has(dataModel.type)) {
|
|
131
|
+
throw new Error(`duplicate data model type ${dataModel.type}`);
|
|
132
|
+
}
|
|
133
|
+
seen.add(dataModel.type);
|
|
134
|
+
if (!dataModel.schema) throw new Error(`dataModels[${index}].schema is required`);
|
|
135
|
+
validateZodSubset(dataModel.schema, `dataModels[${index}].schema`);
|
|
136
|
+
}
|
|
137
|
+
|
|
106
138
|
function validateComponent(component, index, seen) {
|
|
107
139
|
if (!component || typeof component !== 'object') throw new Error(`components[${index}] must be an object`);
|
|
108
140
|
if (!component.name) throw new Error(`components[${index}].name is required`);
|
|
@@ -111,12 +143,6 @@ function validateComponent(component, index, seen) {
|
|
|
111
143
|
if (!component.schema) throw new Error(`components[${index}].schema is required`);
|
|
112
144
|
validateZodSubset(component.schema, `components[${index}].schema`);
|
|
113
145
|
validateImageSlotSchemaMatch(component, index);
|
|
114
|
-
const renderModes = component.renderModes ?? component.render;
|
|
115
|
-
if (!renderModes) throw new Error(`components[${index}].renderModes is required`);
|
|
116
|
-
const enabled = Boolean(renderModes.canStatic ?? renderModes.static)
|
|
117
|
-
|| Boolean(renderModes.canSSR ?? renderModes.ssr)
|
|
118
|
-
|| Boolean(renderModes.canClientIsland ?? renderModes.clientIsland);
|
|
119
|
-
if (!enabled) throw new Error(`components[${index}].renderModes must enable at least one mode`);
|
|
120
146
|
}
|
|
121
147
|
|
|
122
148
|
function validateImageSlotSchemaMatch(component, index) {
|
|
@@ -310,7 +336,11 @@ function validateLayout(layout, index, seen) {
|
|
|
310
336
|
seenSlots.add(slot.name);
|
|
311
337
|
slots.push({ name: slot.name, required: Boolean(slot.required) });
|
|
312
338
|
}
|
|
313
|
-
|
|
339
|
+
const entry = { name: layout.name, slots };
|
|
340
|
+
if (layout.defaults && typeof layout.defaults === 'object') {
|
|
341
|
+
entry.defaults = layout.defaults;
|
|
342
|
+
}
|
|
343
|
+
return entry;
|
|
314
344
|
}
|
|
315
345
|
|
|
316
346
|
function compileSchema(schema, name) {
|
|
@@ -329,6 +359,124 @@ function compileSchema(schema, name) {
|
|
|
329
359
|
};
|
|
330
360
|
}
|
|
331
361
|
|
|
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
|
+
|
|
332
480
|
function validateZodSubset(schema, pathLabel, seen = new Set()) {
|
|
333
481
|
if (seen.has(schema)) return;
|
|
334
482
|
seen.add(schema);
|
package/src/types/component.ts
CHANGED
|
@@ -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[];
|
package/src/types/config.ts
CHANGED
|
@@ -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
|
@@ -20,7 +20,7 @@ export interface GradialImage {
|
|
|
20
20
|
versionId: string;
|
|
21
21
|
alt: string;
|
|
22
22
|
fallback: ImageSource;
|
|
23
|
-
sources: PictureSource[];
|
|
23
|
+
sources: PictureSource[] | null;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export interface ImageSlotContract {
|
|
@@ -65,7 +65,7 @@ export function renderImageHTML(image: GradialImage, attrs: ImageHTMLAttributes
|
|
|
65
65
|
height: image.fallback.height > 0 ? image.fallback.height : undefined,
|
|
66
66
|
});
|
|
67
67
|
const img = `<img${imgAttrs}>`;
|
|
68
|
-
if (!image.sources
|
|
68
|
+
if (!image.sources?.length) {
|
|
69
69
|
return img;
|
|
70
70
|
}
|
|
71
71
|
const sources = image.sources.map((source) => `<source${renderAttrs({
|
package/src/types/index.ts
CHANGED
package/src/types/page.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
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 {
|
package/src/types/render-mode.ts
CHANGED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
export function PreviewBanner({ releaseId }) {
|
|
3
|
-
if (!releaseId)
|
|
4
|
-
return null;
|
|
5
|
-
const shortId = releaseId.length > 20
|
|
6
|
-
? `${releaseId.slice(0, 8)}\u2026${releaseId.slice(-6)}`
|
|
7
|
-
: releaseId;
|
|
8
|
-
return (_jsxs("div", { role: "alert", style: {
|
|
9
|
-
position: 'fixed',
|
|
10
|
-
top: 0,
|
|
11
|
-
left: 0,
|
|
12
|
-
right: 0,
|
|
13
|
-
zIndex: 9999,
|
|
14
|
-
display: 'flex',
|
|
15
|
-
alignItems: 'center',
|
|
16
|
-
justifyContent: 'center',
|
|
17
|
-
gap: '12px',
|
|
18
|
-
padding: '8px 16px',
|
|
19
|
-
backgroundColor: '#1a1a2e',
|
|
20
|
-
color: '#fff',
|
|
21
|
-
fontSize: '13px',
|
|
22
|
-
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
23
|
-
boxShadow: '0 2px 8px rgba(0,0,0,0.3)',
|
|
24
|
-
}, children: [_jsx("span", { "aria-hidden": "true", style: {
|
|
25
|
-
display: 'inline-flex',
|
|
26
|
-
alignItems: 'center',
|
|
27
|
-
justifyContent: 'center',
|
|
28
|
-
width: '18px',
|
|
29
|
-
height: '18px',
|
|
30
|
-
borderRadius: '50%',
|
|
31
|
-
backgroundColor: '#f59e0b',
|
|
32
|
-
color: '#1a1a2e',
|
|
33
|
-
fontWeight: 700,
|
|
34
|
-
fontSize: '11px',
|
|
35
|
-
flexShrink: 0,
|
|
36
|
-
}, children: "P" }), _jsx("span", { style: { fontWeight: 500 }, children: "Release Preview" }), _jsx("span", { style: {
|
|
37
|
-
fontFamily: 'monospace',
|
|
38
|
-
opacity: 0.7,
|
|
39
|
-
fontSize: '12px',
|
|
40
|
-
}, title: releaseId, children: shortId }), _jsx("a", { href: "?leave_preview=1", style: {
|
|
41
|
-
marginLeft: 'auto',
|
|
42
|
-
padding: '4px 12px',
|
|
43
|
-
borderRadius: '6px',
|
|
44
|
-
backgroundColor: '#dc2626',
|
|
45
|
-
color: '#fff',
|
|
46
|
-
textDecoration: 'none',
|
|
47
|
-
fontSize: '12px',
|
|
48
|
-
fontWeight: 600,
|
|
49
|
-
whiteSpace: 'nowrap',
|
|
50
|
-
}, children: "Exit Preview" })] }));
|
|
51
|
-
}
|
package/dist/next/preview.d.ts
DELETED
package/dist/next/preview.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
export async function validatePreviewToken(token, signKey) {
|
|
2
|
-
try {
|
|
3
|
-
const parts = token.split('.');
|
|
4
|
-
if (parts.length !== 3)
|
|
5
|
-
return null;
|
|
6
|
-
const signingInput = `${parts[0]}.${parts[1]}`;
|
|
7
|
-
const signature = base64URLToBytes(parts[2]);
|
|
8
|
-
const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(signKey), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
9
|
-
const expected = new Uint8Array(await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(signingInput)));
|
|
10
|
-
if (!constantTimeEqual(signature, expected))
|
|
11
|
-
return null;
|
|
12
|
-
const payload = new TextDecoder().decode(base64URLToBytes(parts[1]));
|
|
13
|
-
return JSON.parse(payload);
|
|
14
|
-
}
|
|
15
|
-
catch {
|
|
16
|
-
return null;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
function base64URLToBytes(value) {
|
|
20
|
-
const base64 = value.replace(/-/g, '+').replace(/_/g, '/');
|
|
21
|
-
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=');
|
|
22
|
-
const binary = atob(padded);
|
|
23
|
-
const bytes = new Uint8Array(binary.length);
|
|
24
|
-
for (let i = 0; i < binary.length; i++) {
|
|
25
|
-
bytes[i] = binary.charCodeAt(i);
|
|
26
|
-
}
|
|
27
|
-
return bytes;
|
|
28
|
-
}
|
|
29
|
-
function constantTimeEqual(left, right) {
|
|
30
|
-
if (left.length !== right.length)
|
|
31
|
-
return false;
|
|
32
|
-
let diff = 0;
|
|
33
|
-
for (let i = 0; i < left.length; i++) {
|
|
34
|
-
diff |= left[i] ^ right[i];
|
|
35
|
-
}
|
|
36
|
-
return diff === 0;
|
|
37
|
-
}
|
package/dist/render.d.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { ComponentType, ReactNode } from 'react';
|
|
2
|
-
export interface BlockData {
|
|
3
|
-
id: string;
|
|
4
|
-
component: string;
|
|
5
|
-
props: Record<string, unknown>;
|
|
6
|
-
}
|
|
7
|
-
export type ComponentRegistry = Record<string, ComponentType<any>>;
|
|
8
|
-
export interface RenderBlocksOptions {
|
|
9
|
-
registry: ComponentRegistry;
|
|
10
|
-
compositionParams?: Record<string, unknown>;
|
|
11
|
-
slot?: string;
|
|
12
|
-
}
|
|
13
|
-
export declare function renderBlock(block: BlockData, registry: ComponentRegistry, compositionParams?: Record<string, unknown>): ReactNode;
|
|
14
|
-
export declare function renderBlocks(blocks: readonly BlockData[], opts: RenderBlocksOptions): ReactNode[];
|
package/dist/render.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { PreviewBanner } from './next/preview-banner.js';
|
|
3
|
-
function BlockError({ component, id }) {
|
|
4
|
-
return (_jsxs("div", { style: {
|
|
5
|
-
padding: '1rem 1.25rem',
|
|
6
|
-
margin: '1rem 0',
|
|
7
|
-
background: '#fef2f2',
|
|
8
|
-
border: '2px solid #dc2626',
|
|
9
|
-
borderRadius: '6px',
|
|
10
|
-
fontFamily: 'ui-monospace, monospace',
|
|
11
|
-
fontSize: '13px',
|
|
12
|
-
lineHeight: 1.5,
|
|
13
|
-
color: '#991b1b',
|
|
14
|
-
}, children: [_jsx("strong", { children: "Unknown block:" }), " ", _jsx("code", { children: component }), _jsx("br", {}), _jsxs("span", { style: { color: '#b91c1c', opacity: 0.7 }, children: ["id: ", id] })] }));
|
|
15
|
-
}
|
|
16
|
-
export function renderBlock(block, registry, compositionParams) {
|
|
17
|
-
const Component = registry[block.component];
|
|
18
|
-
if (!Component) {
|
|
19
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
20
|
-
return _jsx(BlockError, { component: block.component, id: block.id }, block.id);
|
|
21
|
-
}
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
return _jsx(Component, { ...block.props, ...compositionParams }, block.id);
|
|
25
|
-
}
|
|
26
|
-
export function renderBlocks(blocks, opts) {
|
|
27
|
-
const { registry, compositionParams, slot } = opts;
|
|
28
|
-
const rendered = blocks.map((block) => renderBlock(block, registry, compositionParams));
|
|
29
|
-
if (slot === 'main') {
|
|
30
|
-
return [_jsx(PreviewBanner, {}, "__gradial_preview"), ...rendered];
|
|
31
|
-
}
|
|
32
|
-
return rendered;
|
|
33
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
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
|
-
}
|