@gradial/aci 0.1.41 → 0.1.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/CHANGELOG.md +15 -11
  2. package/README.md +4 -4
  3. package/dist/astro/index.d.ts +1 -1
  4. package/dist/astro/index.js +1 -1
  5. package/dist/astro/page.d.ts +6 -0
  6. package/dist/astro/page.js +3 -2
  7. package/dist/audience-data-models.d.ts +286 -0
  8. package/dist/audience-data-models.js +46 -0
  9. package/dist/cli/compile-registry.d.mts +6 -0
  10. package/dist/cli/compile-registry.mjs +774 -0
  11. package/dist/cli/materialize-template.d.mts +19 -0
  12. package/dist/cli/materialize-template.mjs +128 -0
  13. package/dist/cli/validate-content.d.mts +32 -0
  14. package/dist/cli/validate-content.mjs +464 -0
  15. package/dist/cli/verify-next-cdn-routes.d.mts +2 -0
  16. package/dist/cli/verify-next-cdn-routes.mjs +47 -0
  17. package/dist/collect/aci.js +1 -0
  18. package/dist/collect/aci.js.sha256 +1 -0
  19. package/dist/collect/assignments.d.ts +65 -0
  20. package/dist/collect/assignments.js +138 -0
  21. package/dist/collect/batcher.d.ts +23 -0
  22. package/dist/collect/batcher.js +28 -6
  23. package/dist/collect/consent/envelope.d.ts +9 -3
  24. package/dist/collect/consent/envelope.js +58 -40
  25. package/dist/collect/declarative.d.ts +1 -1
  26. package/dist/collect/declarative.js +7 -1
  27. package/dist/collect/index.js +39 -6
  28. package/dist/collect/linkclicks.js +4 -2
  29. package/dist/collect/pageviews.d.ts +15 -3
  30. package/dist/collect/pageviews.js +28 -13
  31. package/dist/collect/transport.d.ts +4 -0
  32. package/dist/collect/transport.js +37 -2
  33. package/dist/consent/portable-envelope.d.ts +9 -0
  34. package/dist/consent/portable-envelope.js +110 -0
  35. package/dist/consent-banner/index.d.ts +33 -0
  36. package/dist/consent-banner/index.js +100 -0
  37. package/dist/content/assets.d.ts +1 -0
  38. package/dist/content/assets.js +1 -0
  39. package/dist/content/personalization.d.ts +35 -0
  40. package/dist/content/personalization.js +57 -0
  41. package/dist/content/resolve-slots.js +1 -0
  42. package/dist/content/types.d.ts +9 -1
  43. package/dist/edge/personalization-core.d.ts +11 -0
  44. package/dist/edge/personalization-core.js +265 -0
  45. package/dist/index.d.ts +1 -0
  46. package/dist/index.js +1 -0
  47. package/dist/next/assignments-script.d.ts +14 -0
  48. package/dist/next/assignments-script.js +17 -0
  49. package/dist/next/collect-route.d.ts +19 -0
  50. package/dist/next/collect-route.js +422 -0
  51. package/dist/next/envelope-digest.d.ts +21 -0
  52. package/dist/next/envelope-digest.js +58 -0
  53. package/dist/next/index.d.ts +1 -0
  54. package/dist/next/index.js +1 -0
  55. package/dist/next/middleware.d.ts +31 -3
  56. package/dist/next/middleware.js +254 -30
  57. package/dist/next/page.js +2 -1
  58. package/dist/next/personalization-host.d.ts +127 -0
  59. package/dist/next/personalization-host.js +257 -0
  60. package/dist/next/server.d.ts +1 -0
  61. package/dist/next/server.js +9 -6
  62. package/dist/next/vercel-collect-sdk-route.d.ts +27 -0
  63. package/dist/next/vercel-collect-sdk-route.js +205 -0
  64. package/dist/next/vercel-personalization.d.ts +4 -0
  65. package/dist/next/vercel-personalization.js +4 -0
  66. package/dist/next/vercel-s3-personalization.d.ts +79 -0
  67. package/dist/next/vercel-s3-personalization.js +574 -0
  68. package/dist/providers/s3.d.ts +20 -5
  69. package/dist/providers/s3.js +93 -23
  70. package/dist/react/VideoPlayer.d.ts +6 -6
  71. package/dist/react/index.d.ts +6 -6
  72. package/dist/testing/conformance/checks.js +75 -4
  73. package/dist/types/block-ref.d.ts +2 -0
  74. package/dist/types/block-ref.js +1 -0
  75. package/dist/types/page.d.ts +1 -1
  76. package/package.json +44 -15
  77. package/src/astro/ConsentBanner.astro +62 -0
  78. package/src/cli/compile-registry.mjs +24 -10
  79. package/src/cli/validate-content.mjs +52 -13
  80. package/src/cli/verify-next-cdn-routes.mjs +5 -4
  81. package/src/consent-banner/styles.css +110 -0
  82. package/src/types/block-ref.test.ts +45 -0
  83. package/src/types/block-ref.ts +2 -0
  84. package/src/types/page.ts +1 -1
  85. package/templates/astro/starter-content/src/cms/contracts/components/index.ts +2 -0
  86. package/templates/astro/starter-content/src/cms/contracts/components/personalizationChoice.contract.ts +17 -0
  87. package/templates/astro/starter-content/src/cms/registry.ts +3 -0
  88. package/templates/astro/starter-content/src/components/sections/PersonalizationChoice.astro +41 -0
  89. package/templates/astro/starter-content/src/fixtures/atomicBlocks.ts +7 -6
  90. package/templates/astro/starter-content/src/fixtures/composition.ts +3 -2
  91. package/templates/astro/starter-content/src/fixtures/sections.ts +2 -1
  92. package/templates/astro/starter-content/src/render/RenderPage.astro +3 -2
  93. package/templates/astro/template/src/layouts/SiteLayout.astro +3 -1
  94. package/templates/astro/template/src/pages/[...slug].astro +1 -0
  95. package/templates/common/.agents/skills/authoring/SKILL.md +11 -3
  96. package/templates/common/.agents/skills/authoring/references/block-slots.md +3 -0
  97. package/templates/common/.content/audiences/preference-blue/_index.json +11 -0
  98. package/templates/common/.content/audiences/preference-red/_index.json +11 -0
  99. package/templates/common/.content/overlays/audience/preference-blue/pages/home/_index.json +13 -0
  100. package/templates/common/.content/overlays/audience/preference-red/pages/home/_index.json +13 -0
  101. package/templates/common/.content/pages/home/_index.json +124 -4
  102. package/templates/common/.content/segments/preference-blue/_index.json +20 -0
  103. package/templates/common/.content/segments/preference-red/_index.json +20 -0
  104. package/templates/common/contracts/components/index.ts +21 -1
  105. package/templates/common/contracts/components/personalizationChoice.contract.ts +17 -0
  106. package/templates/common/starter-content/.content/pages/about/_index.json +3 -0
  107. package/templates/common/starter-content/.content/pages/home/_index.json +9 -0
  108. package/templates/nextjs/starter-content/src/cms/contracts/components/index.ts +3 -1
  109. package/templates/nextjs/starter-content/src/cms/contracts/components/personalizationChoice.contract.ts +17 -0
  110. package/templates/nextjs/starter-content/src/cms/registry.ts +3 -0
  111. package/templates/nextjs/starter-content/src/components/sections/PersonalizationChoice.tsx +53 -0
  112. package/templates/nextjs/starter-content/src/middleware.ts +41 -0
  113. package/templates/nextjs/template/src/app/collect/route.ts +18 -0
  114. package/templates/nextjs/template/src/middleware.ts +36 -2
  115. package/templates/nextjs/template/src/middleware.ts.tmpl +31 -1
  116. package/templates/astro/template/.aci.yaml +0 -28
  117. package/templates/common/.content/.gradial-dam/blobs/sha256/7f/bf/7fbfc7fe230ff12688c496f660119e41d9725c4504d82e310c15665c73c09292 +0 -0
  118. package/templates/common/.content/.gradial-dam/blobs/sha256/a0/f5/a0f506689de9b15a7055f70103a3f02cd3ba105771ae2de6754df2ede1210d3a +0 -0
  119. package/templates/common/.content/.gradial-dam/blobs/sha256/dd/b7/ddb7e9642be08317bcd4b2bd26b58ba8d0d34dd7239e03e684a4f3634fa3845b +0 -0
  120. package/templates/common/.content/.gradial-dam/blobs/sha256/fb/32/fb3260e8286ea8231ea1c77b9c0be92d6edd9026565731eb14cfdcfd6b1b75b3 +0 -0
  121. package/templates/common/.content/config/robots.txt +0 -2
  122. package/templates/common/.content/config/site.json +0 -197
  123. package/templates/common/.content/dam/folders/seed/ast_site_about_hero/asset.json +0 -9
  124. package/templates/common/.content/dam/folders/seed/ast_site_about_hero/editorial.json +0 -14
  125. package/templates/common/.content/dam/folders/seed/ast_site_about_hero/governance.json +0 -23
  126. package/templates/common/.content/dam/folders/seed/ast_site_about_hero/versions/v0001.json +0 -37
  127. package/templates/common/.content/dam/folders/seed/ast_site_home_hero/asset.json +0 -9
  128. package/templates/common/.content/dam/folders/seed/ast_site_home_hero/editorial.json +0 -14
  129. package/templates/common/.content/dam/folders/seed/ast_site_home_hero/governance.json +0 -23
  130. package/templates/common/.content/dam/folders/seed/ast_site_home_hero/versions/v0001.json +0 -37
  131. package/templates/common/.content/fragments/navbar/_index.json +0 -25
  132. package/templates/common/.content/fragments/site-footer/_index.json +0 -62
  133. package/templates/common/.content/pages/about/_index.json +0 -51
  134. package/templates/nextjs/template/.aci.yaml +0 -26
@@ -0,0 +1,774 @@
1
+ #!/usr/bin/env node
2
+ import { realpathSync } from 'node:fs';
3
+ import { mkdir, writeFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { fileURLToPath, pathToFileURL } from 'node:url';
6
+ import { z } from 'zod';
7
+ import { ImageSchema } from '../types/image.ts';
8
+ import { VideoSchema } from '../types/video.ts';
9
+ import { getBlockRefMeta } from '../types/block-ref.ts';
10
+ const DATA_TYPE_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/;
11
+ // Shim React global so JSX-containing component files can be loaded.
12
+ // The compile-registry only extracts contract metadata (schemas, imageSlots)
13
+ // and never renders components, but importing .tsx files requires the JSX
14
+ // runtime to be resolvable. Next.js uses jsx: "preserve" which leaves React
15
+ // references in the transpiled output.
16
+ try {
17
+ const React = await import('react');
18
+ if (!globalThis.React)
19
+ globalThis.React = React;
20
+ }
21
+ catch { /* react not available — component files without JSX will still work */ }
22
+ if (isMainModule()) {
23
+ const args = parseArgs(process.argv.slice(2));
24
+ try {
25
+ const result = await compileRegistry(args);
26
+ console.log(JSON.stringify({ success: true, ...result, errors: [] }, null, 2));
27
+ }
28
+ catch (error) {
29
+ console.error(JSON.stringify({ success: false, errors: [String(error?.message ?? error)] }, null, 2));
30
+ process.exitCode = 1;
31
+ }
32
+ }
33
+ async function compileRegistry(options) {
34
+ const required = ['components', 'layouts', 'output'];
35
+ for (const key of required) {
36
+ if (!options[key])
37
+ throw new Error(`missing --${key}`);
38
+ }
39
+ const components = await loadDefaultArray(options.components, 'component registry');
40
+ const layouts = await loadDefaultArray(options.layouts, 'layout registry');
41
+ const dataModels = options.data
42
+ ? await loadDefaultArray(options.data, 'data model registry')
43
+ : [];
44
+ const outDir = path.resolve(options.output);
45
+ const schemaDir = path.join(outDir, 'schemas');
46
+ await mkdir(schemaDir, { recursive: true });
47
+ const seenComponents = new Set();
48
+ const componentEntries = [];
49
+ const schemaFiles = [];
50
+ for (const [index, component] of components.entries()) {
51
+ validateComponent(component, index, seenComponents);
52
+ const componentId = component.id;
53
+ const schema = compileSchema(component.props, componentId);
54
+ const schemaFile = `schemas/${componentId}.schema.json`;
55
+ const schemaPath = path.join(outDir, schemaFile);
56
+ await writeFile(schemaPath, JSON.stringify(schema, null, 2) + '\n');
57
+ schemaFiles.push(schemaPath);
58
+ const imageSlots = normalizeImageSlots(component.imageSlots);
59
+ const slots = extractBlockSlots(component.props, componentId);
60
+ componentEntries.push({
61
+ id: componentId,
62
+ name: componentId,
63
+ props: schemaFile,
64
+ ...(Object.keys(imageSlots).length ? { imageSlots } : {}),
65
+ ...(Object.keys(slots).length ? { slots } : {}),
66
+ });
67
+ }
68
+ const seenLayouts = new Set();
69
+ const layoutEntries = layouts.map((layout, index) => validateLayout(layout, index, seenLayouts));
70
+ const seenDataModels = new Set();
71
+ const dataModelEntries = [];
72
+ if (dataModels.length > 0) {
73
+ await mkdir(path.join(schemaDir, 'data'), { recursive: true });
74
+ }
75
+ for (const [index, dataModel] of dataModels.entries()) {
76
+ validateDataModel(dataModel, index, seenDataModels);
77
+ const schema = compileSchema(dataModel.schema, dataModel.type);
78
+ const schemaFile = `schemas/data/${dataModel.type}.schema.json`;
79
+ const schemaPath = path.join(outDir, schemaFile);
80
+ await writeFile(schemaPath, JSON.stringify(schema, null, 2) + '\n');
81
+ schemaFiles.push(schemaPath);
82
+ dataModelEntries.push({
83
+ type: dataModel.type,
84
+ schemaFile,
85
+ });
86
+ }
87
+ const knownNames = new Set(componentEntries.map((c) => c.id));
88
+ for (const entry of componentEntries) {
89
+ for (const [slotName, slot] of Object.entries(entry.slots ?? {})) {
90
+ for (const childName of slot.allowed) {
91
+ if (!knownNames.has(childName)) {
92
+ throw new Error(`${entry.id}.slots.${slotName}.allowed references unknown component "${childName}"`);
93
+ }
94
+ }
95
+ }
96
+ }
97
+ const registry = {
98
+ manifestVersion: '1',
99
+ generatedAt: new Date().toISOString(),
100
+ components: componentEntries,
101
+ layouts: layoutEntries,
102
+ dataModels: dataModelEntries,
103
+ };
104
+ const registryPath = path.join(outDir, 'registry.json');
105
+ await writeFile(registryPath, JSON.stringify(registry, null, 2) + '\n');
106
+ const pageSchemaPath = await generatePageSchema(schemaDir, componentEntries, outDir);
107
+ schemaFiles.push(pageSchemaPath);
108
+ return { registry: registryPath, schemas: schemaFiles };
109
+ }
110
+ async function loadDefaultArray(modulePath, label) {
111
+ const mod = await import(pathToFileURL(path.resolve(modulePath)).href);
112
+ const value = mod.default ?? mod.registry ?? mod.components ?? mod.layouts ?? mod.dataModels;
113
+ if (!Array.isArray(value))
114
+ throw new Error(`${label} must export an array`);
115
+ return value;
116
+ }
117
+ function validateDataModel(dataModel, index, seen) {
118
+ if (!dataModel || typeof dataModel !== 'object') {
119
+ throw new Error(`dataModels[${index}] must be an object`);
120
+ }
121
+ if (typeof dataModel.type !== 'string' || !DATA_TYPE_PATTERN.test(dataModel.type)) {
122
+ throw new Error(`dataModels[${index}].type must match ${DATA_TYPE_PATTERN}`);
123
+ }
124
+ if (seen.has(dataModel.type)) {
125
+ throw new Error(`duplicate data model type ${dataModel.type}`);
126
+ }
127
+ seen.add(dataModel.type);
128
+ if (!dataModel.schema)
129
+ throw new Error(`dataModels[${index}].schema is required`);
130
+ validateZodSubset(dataModel.schema, `dataModels[${index}].schema`);
131
+ }
132
+ function validateComponent(component, index, seen) {
133
+ if (!component || typeof component !== 'object')
134
+ throw new Error(`components[${index}] must be an object`);
135
+ if (!component.id)
136
+ throw new Error(`components[${index}].id is required`);
137
+ if (seen.has(component.id))
138
+ throw new Error(`duplicate component id ${component.id}`);
139
+ seen.add(component.id);
140
+ if (!component.props)
141
+ throw new Error(`components[${index}].props is required`);
142
+ validateZodSubset(component.props, `components[${index}].props`);
143
+ validateImageSlotSchemaMatch(component, index);
144
+ }
145
+ function validateImageSlotSchemaMatch(component, index) {
146
+ const imageSlots = normalizeImageSlots(component.imageSlots);
147
+ const mediaFields = collectMediaFields(component.props);
148
+ const imageFields = new Set(mediaFields.filter(([, k]) => k === 'image').map(([p]) => p));
149
+ const allMediaFields = new Set(mediaFields.map(([p]) => p));
150
+ for (const field of imageFields) {
151
+ if (!Object.prototype.hasOwnProperty.call(imageSlots, field)) {
152
+ throw new Error(`components[${index}].imageSlots.${field} is required because props field ${field} uses an image schema`);
153
+ }
154
+ }
155
+ for (const field of Object.keys(imageSlots)) {
156
+ if (!allMediaFields.has(field)) {
157
+ throw new Error(`components[${index}].props.${field} must use an image or asset schema because imageSlots.${field} is declared`);
158
+ }
159
+ }
160
+ }
161
+ function extractBlockSlots(schema, componentId) {
162
+ const shape = getSchemaShape(schema);
163
+ if (!shape)
164
+ return {};
165
+ const out = {};
166
+ for (const [name, fieldSchema] of Object.entries(shape)) {
167
+ const unwrapped = unwrapOptionalSchema(fieldSchema);
168
+ const meta = getBlockRefMeta(unwrapped) ?? getBlockRefMeta(unwrapArraySchema(unwrapped));
169
+ if (meta) {
170
+ out[name] = {
171
+ allowed: [...meta.allowed],
172
+ multiple: meta.multiple,
173
+ required: false,
174
+ };
175
+ }
176
+ }
177
+ return out;
178
+ }
179
+ function unwrapOptionalSchema(schema) {
180
+ let current = schema;
181
+ const seen = new Set();
182
+ while (current && typeof current === 'object' && !seen.has(current)) {
183
+ seen.add(current);
184
+ const def = current._def;
185
+ if (!def || typeof def !== 'object')
186
+ break;
187
+ const type = def.type;
188
+ const typeName = def.typeName;
189
+ if (type === 'optional' || type === 'nullable' || type === 'default' ||
190
+ typeName === 'ZodOptional' || typeName === 'ZodNullable' || typeName === 'ZodDefault') {
191
+ const inner = def.innerType ?? def.schema;
192
+ if (inner) {
193
+ current = inner;
194
+ continue;
195
+ }
196
+ }
197
+ break;
198
+ }
199
+ return current;
200
+ }
201
+ function getSchemaShape(schema) {
202
+ if (!schema || typeof schema !== 'object')
203
+ return null;
204
+ const def = schema._def;
205
+ if (!def)
206
+ return null;
207
+ const shape = typeof def.shape === 'function' ? def.shape() : def.shape;
208
+ if (shape && typeof shape === 'object')
209
+ return shape;
210
+ return null;
211
+ }
212
+ function unwrapArraySchema(schema) {
213
+ if (!schema || typeof schema !== 'object')
214
+ return null;
215
+ const def = schema._def;
216
+ if (!def)
217
+ return null;
218
+ if (def.type === 'array' && def.element)
219
+ return def.element;
220
+ if (def.typeName === 'ZodArray' && def.type)
221
+ return def.type;
222
+ return null;
223
+ }
224
+ function normalizeImageSlots(imageSlots) {
225
+ if (imageSlots == null)
226
+ return {};
227
+ if (typeof imageSlots !== 'object' || Array.isArray(imageSlots)) {
228
+ throw new Error('imageSlots must be an object');
229
+ }
230
+ const out = {};
231
+ for (const [key, slot] of Object.entries(imageSlots)) {
232
+ if (!key)
233
+ throw new Error('imageSlots keys must be non-empty');
234
+ if (!slot || typeof slot !== 'object' || Array.isArray(slot)) {
235
+ throw new Error(`imageSlots.${key} must be an object`);
236
+ }
237
+ if (!Array.isArray(slot.outputs) || !slot.outputs.length) {
238
+ throw new Error(`imageSlots.${key}.outputs must be a non-empty array`);
239
+ }
240
+ if (!Array.isArray(slot.formats) || !slot.formats.length) {
241
+ throw new Error(`imageSlots.${key}.formats must be a non-empty array`);
242
+ }
243
+ if (typeof slot.sizes !== 'string' || !slot.sizes.trim()) {
244
+ throw new Error(`imageSlots.${key}.sizes must be a non-empty string`);
245
+ }
246
+ out[key] = {
247
+ outputs: slot.outputs.map((output, outputIndex) => normalizeSlotOutput(key, output, outputIndex)),
248
+ formats: slot.formats.map((format, formatIndex) => {
249
+ if (typeof format !== 'string' || !format.trim()) {
250
+ throw new Error(`imageSlots.${key}.formats[${formatIndex}] must be a non-empty string`);
251
+ }
252
+ return format;
253
+ }),
254
+ sizes: slot.sizes,
255
+ };
256
+ }
257
+ return out;
258
+ }
259
+ function normalizeSlotOutput(slotKey, output, outputIndex) {
260
+ if (!output || typeof output !== 'object' || Array.isArray(output)) {
261
+ throw new Error(`imageSlots.${slotKey}.outputs[${outputIndex}] must be an object`);
262
+ }
263
+ if (typeof output.aspectRatio !== 'string' || !output.aspectRatio.trim()) {
264
+ throw new Error(`imageSlots.${slotKey}.outputs[${outputIndex}].aspectRatio must be a non-empty string`);
265
+ }
266
+ if (!Array.isArray(output.widths) || !output.widths.length) {
267
+ throw new Error(`imageSlots.${slotKey}.outputs[${outputIndex}].widths must be a non-empty array`);
268
+ }
269
+ const widths = output.widths.map((width, widthIndex) => {
270
+ if (!Number.isInteger(width) || width <= 0) {
271
+ throw new Error(`imageSlots.${slotKey}.outputs[${outputIndex}].widths[${widthIndex}] must be a positive integer`);
272
+ }
273
+ return width;
274
+ });
275
+ return {
276
+ aspectRatio: output.aspectRatio,
277
+ widths,
278
+ ...(typeof output.media === 'string' && output.media.trim() ? { media: output.media } : {}),
279
+ };
280
+ }
281
+ /**
282
+ * Walks a Zod schema and returns [dotPath, kind] pairs for every field
283
+ * that contains an image or asset schema.
284
+ * kind = 'image' for ImageSchema-only fields, 'asset' for unions (image | video).
285
+ */
286
+ function collectMediaFields(schema, prefix = '') {
287
+ if (!schema)
288
+ return [];
289
+ if (prefix && isImageSchema(schema))
290
+ return [[prefix, 'image']];
291
+ if (prefix && isVideoSchema(schema))
292
+ return [];
293
+ const itemSchema = arrayItemSchema(schema);
294
+ if (itemSchema)
295
+ return collectMediaFields(itemSchema, prefix);
296
+ const unionOptions = unionOptionSchemas(schema);
297
+ if (unionOptions) {
298
+ const hasImage = unionOptions.some((opt) => isImageSchema(opt));
299
+ const hasVideo = unionOptions.some((opt) => isVideoSchema(opt));
300
+ if (prefix && hasImage && hasVideo)
301
+ return [[prefix, 'asset']];
302
+ if (prefix && hasVideo)
303
+ return [];
304
+ return unionOptions.flatMap((opt) => collectMediaFields(opt, prefix));
305
+ }
306
+ const shape = getSchemaShape(unwrapSchema(schema));
307
+ if (!shape)
308
+ return [];
309
+ return Object.entries(shape).flatMap(([field, fieldSchema]) => collectMediaFields(fieldSchema, prefix ? `${prefix}.${field}` : field));
310
+ }
311
+ function unionOptionSchemas(schema) {
312
+ const def = unwrapSchema(schema)?._def;
313
+ if (!def)
314
+ return null;
315
+ if (Array.isArray(def.options))
316
+ return def.options;
317
+ if (def.options instanceof Map)
318
+ return Array.from(def.options.values());
319
+ return null;
320
+ }
321
+ function arrayItemSchema(schema) {
322
+ let current = schema;
323
+ const seen = new Set();
324
+ while (current && !seen.has(current)) {
325
+ seen.add(current);
326
+ const def = current?._def;
327
+ if (!def) {
328
+ // Zod 4 mini: check for ~standard array type
329
+ if (current?.type === 'array' && current?.element)
330
+ return current.element;
331
+ return null;
332
+ }
333
+ if (def.typeName === 'ZodArray')
334
+ return def.type;
335
+ // Zod 4: array element might be in def.element
336
+ if (def.type === 'array' && def.element)
337
+ return def.element;
338
+ current = def.innerType ?? def.schema;
339
+ }
340
+ return null;
341
+ }
342
+ function isImageSchema(schema) {
343
+ const unwrapped = unwrapSchema(schema);
344
+ if (unwrapped === ImageSchema)
345
+ return true;
346
+ try {
347
+ const compiled = z.toJSONSchema(unwrapped, {
348
+ target: 'draft-2020-12',
349
+ unrepresentable: 'throw',
350
+ });
351
+ const properties = compiled?.properties ?? {};
352
+ const required = new Set(compiled?.required ?? []);
353
+ return compiled?.type === 'object'
354
+ && properties.$type?.const === 'gradial.image'
355
+ && properties.assetId?.type === 'string'
356
+ && properties.versionId?.type === 'string'
357
+ && properties.alt?.type === 'string'
358
+ && properties.sources?.type === 'array'
359
+ && required.has('$type')
360
+ && required.has('assetId')
361
+ && required.has('versionId')
362
+ && required.has('alt')
363
+ && required.has('sources');
364
+ }
365
+ catch {
366
+ return false;
367
+ }
368
+ }
369
+ function isVideoSchema(schema) {
370
+ const unwrapped = unwrapSchema(schema);
371
+ if (unwrapped === VideoSchema)
372
+ return true;
373
+ try {
374
+ const compiled = z.toJSONSchema(unwrapped, {
375
+ target: 'draft-2020-12',
376
+ unrepresentable: 'throw',
377
+ });
378
+ return compiled?.type === 'object'
379
+ && compiled?.properties?.$type?.const === 'gradial.video';
380
+ }
381
+ catch {
382
+ return false;
383
+ }
384
+ }
385
+ function unwrapSchema(schema) {
386
+ let current = schema;
387
+ const seen = new Set();
388
+ while (current && !seen.has(current)) {
389
+ seen.add(current);
390
+ if (current === ImageSchema)
391
+ return current;
392
+ const def = current._def;
393
+ const next = def?.innerType ?? def?.schema;
394
+ if (!next)
395
+ break;
396
+ current = next;
397
+ }
398
+ return current;
399
+ }
400
+ function validateLayout(layout, index, seen) {
401
+ if (!layout || typeof layout !== 'object')
402
+ throw new Error(`layouts[${index}] must be an object`);
403
+ if (!layout.name)
404
+ throw new Error(`layouts[${index}].name is required`);
405
+ if (seen.has(layout.name))
406
+ throw new Error(`duplicate layout name ${layout.name}`);
407
+ seen.add(layout.name);
408
+ if (!Array.isArray(layout.slots))
409
+ throw new Error(`layouts[${index}].slots must be an array`);
410
+ const slots = [];
411
+ const seenSlots = new Set();
412
+ for (const [slotIndex, slot] of layout.slots.entries()) {
413
+ if (!slot?.name)
414
+ throw new Error(`layouts[${index}].slots[${slotIndex}].name is required`);
415
+ if (seenSlots.has(slot.name))
416
+ throw new Error(`duplicate slot name ${slot.name} in layout ${layout.name}`);
417
+ seenSlots.add(slot.name);
418
+ slots.push({ name: slot.name, required: Boolean(slot.required) });
419
+ }
420
+ const entry = { name: layout.name, slots };
421
+ if (layout.defaults && typeof layout.defaults === 'object') {
422
+ entry.defaults = layout.defaults;
423
+ }
424
+ return entry;
425
+ }
426
+ function compileSchema(schema, name) {
427
+ const toJSONSchema = z.toJSONSchema;
428
+ if (typeof toJSONSchema !== 'function') {
429
+ throw new Error('Zod 4 is required: install zod@^4.0.0');
430
+ }
431
+ const compiled = toJSONSchema(schema, {
432
+ target: 'draft-2020-12',
433
+ unrepresentable: 'throw',
434
+ });
435
+ return {
436
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
437
+ title: name,
438
+ ...compiled,
439
+ };
440
+ }
441
+ async function generatePageSchema(schemaDir, componentEntries, outDir) {
442
+ const { readFile } = await import('node:fs/promises');
443
+ const defs = {
444
+ baseBlock: {
445
+ type: 'object',
446
+ required: ['$type', 'id', 'component', 'props'],
447
+ additionalProperties: true,
448
+ properties: {
449
+ $type: { const: 'component' },
450
+ id: { type: 'string', minLength: 1 },
451
+ component: { type: 'string' },
452
+ props: { type: 'object' },
453
+ slots: {
454
+ type: 'object',
455
+ additionalProperties: {
456
+ type: 'array',
457
+ items: { $ref: '#/$defs/baseBlock' },
458
+ },
459
+ },
460
+ },
461
+ },
462
+ damAssetRef: {
463
+ type: 'object',
464
+ required: ['$type', 'assetId'],
465
+ additionalProperties: true,
466
+ properties: {
467
+ $type: { const: 'dam.assetRef' },
468
+ assetId: { type: 'string', minLength: 1 },
469
+ version: { type: 'string', minLength: 1 },
470
+ alt: { type: 'string' },
471
+ },
472
+ },
473
+ staticAssetRef: {
474
+ type: 'object',
475
+ required: ['$type', 'src'],
476
+ additionalProperties: true,
477
+ properties: {
478
+ $type: { const: 'static.assetRef' },
479
+ src: { type: 'string', minLength: 1 },
480
+ alt: { type: 'string' },
481
+ mediaType: { type: 'string' },
482
+ width: { type: 'number' },
483
+ height: { type: 'number' },
484
+ },
485
+ },
486
+ assetRef: {
487
+ oneOf: [
488
+ { $ref: '#/$defs/damAssetRef' },
489
+ { $ref: '#/$defs/staticAssetRef' },
490
+ ],
491
+ },
492
+ };
493
+ const oneOf = [];
494
+ for (const entry of componentEntries) {
495
+ const schemaPath = path.join(outDir, entry.props);
496
+ const raw = await readFile(schemaPath, 'utf-8');
497
+ const componentSchema = JSON.parse(raw);
498
+ const { $schema: _s, title: _t, ...propsSchema } = componentSchema;
499
+ replaceImagesWithDamAssetRef(propsSchema, defs.damAssetRef);
500
+ const defName = `${entry.name}Block`;
501
+ defs[defName] = {
502
+ allOf: [
503
+ { $ref: '#/$defs/baseBlock' },
504
+ {
505
+ properties: {
506
+ component: { const: entry.name },
507
+ props: propsSchema,
508
+ },
509
+ },
510
+ ],
511
+ };
512
+ oneOf.push({ $ref: `#/$defs/${defName}` });
513
+ }
514
+ const pageSchema = {
515
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
516
+ $id: 'https://baremetal.local/schemas/page.schema.json',
517
+ title: 'bare-metal Page',
518
+ type: 'object',
519
+ required: ['$type', 'id', 'status', 'layout', 'metadata', 'regions'],
520
+ additionalProperties: true,
521
+ properties: {
522
+ $type: { const: 'page' },
523
+ id: { type: 'string', minLength: 1 },
524
+ status: { type: 'string', minLength: 1 },
525
+ layout: { type: 'string', minLength: 1 },
526
+ metadata: {
527
+ type: 'object',
528
+ required: ['title'],
529
+ additionalProperties: true,
530
+ properties: {
531
+ title: { type: 'string', minLength: 1 },
532
+ description: { type: 'string' },
533
+ canonical: { type: 'string' },
534
+ },
535
+ },
536
+ regions: {
537
+ type: 'object',
538
+ additionalProperties: {
539
+ type: 'array',
540
+ items: { oneOf },
541
+ },
542
+ },
543
+ },
544
+ $defs: defs,
545
+ };
546
+ const pageSchemaPath = path.join(schemaDir, 'page.schema.json');
547
+ await writeFile(pageSchemaPath, JSON.stringify(pageSchema, null, 2) + '\n');
548
+ return pageSchemaPath;
549
+ }
550
+ function replaceImagesWithDamAssetRef(schema, damAssetRef) {
551
+ if (!schema || typeof schema !== 'object')
552
+ return schema;
553
+ if (schema.$ref)
554
+ return;
555
+ if (Array.isArray(schema.anyOf)) {
556
+ for (const s of schema.anyOf)
557
+ replaceImagesWithDamAssetRef(s, damAssetRef);
558
+ }
559
+ if (Array.isArray(schema.allOf)) {
560
+ for (const s of schema.allOf)
561
+ replaceImagesWithDamAssetRef(s, damAssetRef);
562
+ }
563
+ if (Array.isArray(schema.oneOf)) {
564
+ for (const s of schema.oneOf)
565
+ replaceImagesWithDamAssetRef(s, damAssetRef);
566
+ }
567
+ if (schema.properties) {
568
+ for (const [key, value] of Object.entries(schema.properties)) {
569
+ if (isJsonAssetRef(value)) {
570
+ schema.properties[key] = { $ref: '#/$defs/assetRef' };
571
+ }
572
+ else {
573
+ replaceImagesWithDamAssetRef(value, damAssetRef);
574
+ }
575
+ }
576
+ }
577
+ if (schema.items) {
578
+ if (isJsonAssetRef(schema.items)) {
579
+ schema.items = { $ref: '#/$defs/assetRef' };
580
+ }
581
+ else {
582
+ replaceImagesWithDamAssetRef(schema.items, damAssetRef);
583
+ }
584
+ }
585
+ }
586
+ function isJsonAssetRef(node) {
587
+ if (node?.type !== 'object' || !node?.properties?.$type)
588
+ return false;
589
+ const constVal = node.properties.$type.const;
590
+ return constVal === 'gradial.image' || constVal === 'gradial.video';
591
+ }
592
+ function validateZodSubset(schema, pathLabel, seen = new Set()) {
593
+ if (seen.has(schema))
594
+ return;
595
+ seen.add(schema);
596
+ const def = schema?._def;
597
+ if (!def)
598
+ throw new Error(`${pathLabel}: expected a Zod schema`);
599
+ const typeName = def.typeName ?? def.type;
600
+ if (unsupportedZodType(typeName)) {
601
+ throw new Error(`${pathLabel}: unsupported Zod feature ${typeName}`);
602
+ }
603
+ rejectUnsupportedChecks(def, pathLabel);
604
+ for (const child of childSchemas(def)) {
605
+ validateZodSubset(child.schema, `${pathLabel}${child.path}`, seen);
606
+ }
607
+ }
608
+ function unsupportedZodType(typeName) {
609
+ return new Set([
610
+ 'ZodEffects',
611
+ 'ZodPipeline',
612
+ 'ZodLazy',
613
+ 'ZodPromise',
614
+ 'ZodFunction',
615
+ 'ZodTransform',
616
+ 'pipe',
617
+ 'lazy',
618
+ 'promise',
619
+ 'function',
620
+ 'transform',
621
+ 'custom',
622
+ ]).has(String(typeName));
623
+ }
624
+ function rejectUnsupportedChecks(def, pathLabel) {
625
+ if (!Array.isArray(def.checks))
626
+ return;
627
+ for (const [index, check] of def.checks.entries()) {
628
+ const checkType = check?.def?.type ?? check?._def?.type ?? check?._zod?.def?.check;
629
+ if (String(checkType) === 'custom') {
630
+ throw new Error(`${pathLabel}.checks[${index}]: unsupported Zod feature custom`);
631
+ }
632
+ }
633
+ }
634
+ function childSchemas(def) {
635
+ const children = [];
636
+ if (def.innerType)
637
+ children.push({ path: '.inner', schema: def.innerType });
638
+ if (def.schema)
639
+ children.push({ path: '.schema', schema: def.schema });
640
+ if (def.valueType)
641
+ children.push({ path: '.value', schema: def.valueType });
642
+ if (def.keyType)
643
+ children.push({ path: '.key', schema: def.keyType });
644
+ if (Array.isArray(def.options))
645
+ def.options.forEach((schema, index) => children.push({ path: `.options[${index}]`, schema }));
646
+ if (Array.isArray(def.items))
647
+ def.items.forEach((schema, index) => children.push({ path: `.items[${index}]`, schema }));
648
+ const shape = typeof def.shape === 'function' ? def.shape() : def.shape;
649
+ if (shape && typeof shape === 'object') {
650
+ for (const [name, schema] of Object.entries(shape))
651
+ children.push({ path: `.shape.${name}`, schema });
652
+ }
653
+ return children;
654
+ }
655
+ // ---------------------------------------------------------------------------
656
+ // Nested block detection — walks generated JSON schemas to find blockRef
657
+ // patterns (objects with `component` enum + `props`) and validates that
658
+ // referenced component names exist in the registry.
659
+ // ---------------------------------------------------------------------------
660
+ function collectNestedBlockRefs(schema, knownNames, componentName) {
661
+ const results = [];
662
+ _walkSchemaForBlockRefs(schema, schema, '', results, knownNames, componentName);
663
+ return results;
664
+ }
665
+ function _walkSchemaForBlockRefs(node, rootSchema, currentPath, results, knownNames, componentName) {
666
+ if (!node || typeof node !== 'object')
667
+ return;
668
+ // Resolve $ref
669
+ if (node.$ref) {
670
+ const resolved = resolveJsonSchemaRef(node.$ref, rootSchema);
671
+ if (resolved)
672
+ _walkSchemaForBlockRefs(resolved, rootSchema, currentPath, results, knownNames, componentName);
673
+ return;
674
+ }
675
+ // Check if this object matches the blockRef signature:
676
+ // { type: "object", properties: { id, component, props } }
677
+ if (isBlockRefSchema(node)) {
678
+ const allow = extractBlockRefAllow(node, rootSchema);
679
+ // Validate allowed names exist
680
+ for (const name of allow) {
681
+ if (!knownNames.has(name)) {
682
+ throw new Error(`${componentName}: nested block allowlist references unknown component "${name}" at ${currentPath || '(root)'}`);
683
+ }
684
+ }
685
+ results.push({
686
+ path: currentPath || '(root)',
687
+ ...(allow.length > 0 ? { allow } : {}),
688
+ });
689
+ return; // Don't recurse into the block schema itself
690
+ }
691
+ // Recurse into object properties
692
+ if (node.properties) {
693
+ for (const [key, propSchema] of Object.entries(node.properties)) {
694
+ const nextPath = currentPath ? `${currentPath}.${key}` : key;
695
+ _walkSchemaForBlockRefs(propSchema, rootSchema, nextPath, results, knownNames, componentName);
696
+ }
697
+ }
698
+ // Recurse into array items
699
+ if (node.items) {
700
+ _walkSchemaForBlockRefs(node.items, rootSchema, `${currentPath}[]`, results, knownNames, componentName);
701
+ }
702
+ // Recurse into anyOf / allOf / oneOf
703
+ for (const keyword of ['anyOf', 'allOf', 'oneOf']) {
704
+ if (Array.isArray(node[keyword])) {
705
+ for (const sub of node[keyword]) {
706
+ _walkSchemaForBlockRefs(sub, rootSchema, currentPath, results, knownNames, componentName);
707
+ }
708
+ }
709
+ }
710
+ }
711
+ function isBlockRefSchema(node) {
712
+ if (node.type !== 'object' || !node.properties)
713
+ return false;
714
+ const props = node.properties;
715
+ // Must have component and props fields
716
+ if (!props.component || !props.props)
717
+ return false;
718
+ // component must be a string or enum
719
+ const comp = props.component;
720
+ if (comp.type !== 'string' && !Array.isArray(comp.enum))
721
+ return false;
722
+ // props must be an object (record)
723
+ if (props.props.type !== 'object')
724
+ return false;
725
+ return true;
726
+ }
727
+ function extractBlockRefAllow(node, rootSchema) {
728
+ let comp = node.properties.component;
729
+ // Resolve $ref on the component field
730
+ if (comp.$ref) {
731
+ comp = resolveJsonSchemaRef(comp.$ref, rootSchema) ?? comp;
732
+ }
733
+ if (Array.isArray(comp.enum)) {
734
+ return comp.enum.filter((v) => typeof v === 'string');
735
+ }
736
+ return [];
737
+ }
738
+ function resolveJsonSchemaRef(ref, rootSchema) {
739
+ if (!ref || !ref.startsWith('#/'))
740
+ return null;
741
+ const parts = ref.slice(2).split('/');
742
+ let current = rootSchema;
743
+ for (const part of parts) {
744
+ current = current?.[part];
745
+ }
746
+ return current ?? null;
747
+ }
748
+ function parseArgs(argv) {
749
+ const out = {};
750
+ for (let i = 0; i < argv.length; i++) {
751
+ const arg = argv[i];
752
+ if (!arg.startsWith('--'))
753
+ continue;
754
+ const body = arg.slice(2);
755
+ const equals = body.indexOf('=');
756
+ if (equals >= 0) {
757
+ out[body.slice(0, equals)] = body.slice(equals + 1);
758
+ continue;
759
+ }
760
+ out[body] = argv[++i];
761
+ }
762
+ return out;
763
+ }
764
+ function isMainModule() {
765
+ if (!process.argv[1])
766
+ return false;
767
+ try {
768
+ return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(path.resolve(process.argv[1]));
769
+ }
770
+ catch {
771
+ return false;
772
+ }
773
+ }
774
+ export { compileRegistry, generatePageSchema };