@gradial/aci 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +47 -2
  2. package/bin/aci +0 -0
  3. package/bin/aci.js +157 -0
  4. package/dist/assets/index.d.ts +3 -0
  5. package/dist/assets/index.js +3 -0
  6. package/dist/astro/index.d.ts +24 -2
  7. package/dist/astro/index.js +42 -4
  8. package/dist/block-ref.d.ts +34 -0
  9. package/dist/block-ref.js +34 -0
  10. package/dist/content/index.d.ts +0 -3
  11. package/dist/content/index.js +0 -3
  12. package/dist/content/provider.d.ts +32 -8
  13. package/dist/content/provider.js +26 -16
  14. package/dist/content/routes.d.ts +6 -12
  15. package/dist/content/routes.js +9 -55
  16. package/dist/content/validation.js +1 -1
  17. package/dist/define-component.d.ts +1 -0
  18. package/dist/define-component.js +1 -0
  19. package/dist/define-layout.d.ts +1 -0
  20. package/dist/define-layout.js +6 -1
  21. package/dist/dev/browser.d.ts +1 -1
  22. package/dist/dev/browser.js +1 -1
  23. package/dist/dev/index.d.ts +9 -3
  24. package/dist/dev/index.js +74 -8
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.js +4 -0
  27. package/dist/next/asset-route.d.ts +9 -0
  28. package/dist/next/asset-route.js +15 -0
  29. package/dist/next/config.d.ts +6 -0
  30. package/dist/next/config.js +25 -0
  31. package/dist/next/dev-refresh.js +4 -4
  32. package/dist/next/edge-config.d.ts +1 -0
  33. package/dist/next/edge-config.js +92 -0
  34. package/dist/next/index.d.ts +2 -0
  35. package/dist/next/index.js +2 -0
  36. package/dist/next/middleware.js +4 -6
  37. package/dist/next/server.d.ts +9 -24
  38. package/dist/next/server.js +100 -152
  39. package/dist/providers/file.d.ts +11 -17
  40. package/dist/providers/file.js +44 -78
  41. package/dist/providers/s3.d.ts +26 -0
  42. package/dist/providers/s3.js +174 -0
  43. package/dist/react/GradialMedia.d.ts +24 -0
  44. package/dist/react/GradialMedia.js +31 -0
  45. package/dist/react/GradialPicture.d.ts +14 -0
  46. package/dist/react/GradialPicture.js +30 -0
  47. package/dist/react/GradialVideoPlayer.d.ts +13 -0
  48. package/dist/react/GradialVideoPlayer.js +28 -0
  49. package/dist/react/index.d.ts +3 -0
  50. package/dist/react/index.js +3 -0
  51. package/dist/sveltekit/index.d.ts +18 -2
  52. package/dist/sveltekit/index.js +40 -4
  53. package/dist/testing/index.d.ts +14 -12
  54. package/dist/testing/index.js +41 -28
  55. package/dist/types/component.d.ts +24 -2
  56. package/dist/types/config.d.ts +4 -0
  57. package/dist/types/image.d.ts +51 -0
  58. package/dist/types/image.js +58 -0
  59. package/dist/types/index.d.ts +3 -0
  60. package/dist/types/index.js +3 -0
  61. package/dist/types/layout.d.ts +12 -0
  62. package/dist/types/media.d.ts +69 -0
  63. package/dist/types/media.js +86 -0
  64. package/dist/types/video.d.ts +70 -0
  65. package/dist/types/video.js +22 -0
  66. package/package.json +30 -2
  67. package/src/cli/compile-registry.mjs +303 -0
  68. package/src/cli/validate-content.mjs +489 -0
@@ -0,0 +1,489 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * validate-content — validate authored page content against compiled JSON schemas.
4
+ *
5
+ * Usage:
6
+ * node validate-content.mjs \
7
+ * --registry <path-to-compiled-registry-dir> \
8
+ * --compiled <path-to-compiled-content-dir>
9
+ *
10
+ * Prerequisites:
11
+ * 1. Run compile-registry to generate registry.json + schemas/*.schema.json
12
+ * 2. Run content build to generate compiled routes + fragments
13
+ *
14
+ * Validates every block in the COMPILED output against JSON schemas.
15
+ * This validates the build result — after DAM resolution, fragment inlining, etc.
16
+ * Fails with exit code 1 if any published page has validation errors.
17
+ * Warns (but does not fail) for draft pages.
18
+ *
19
+ * Uses the JSON schemas generated by compile-registry — portable, no React/CSS
20
+ * imports needed, same schemas usable by Go backend or other tools.
21
+ */
22
+ import { readdir, readFile } from 'node:fs/promises';
23
+ import path from 'node:path';
24
+
25
+ const args = parseArgs(process.argv.slice(2));
26
+
27
+ try {
28
+ const result = await validateContent(args);
29
+ console.log(JSON.stringify(result, null, 2));
30
+ if (!result.success) process.exitCode = 1;
31
+ } catch (error) {
32
+ console.error(JSON.stringify({
33
+ success: false,
34
+ errors: [{ message: String(error?.message ?? error) }],
35
+ }, null, 2));
36
+ process.exitCode = 1;
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Main
41
+ // ---------------------------------------------------------------------------
42
+
43
+ async function validateContent(options) {
44
+ if (!options.registry) throw new Error('missing --registry (path to compiled registry directory)');
45
+ if (!options.compiled) throw new Error('missing --compiled (path to compiled content output directory)');
46
+
47
+ const registryDir = path.resolve(options.registry);
48
+ const contentRoot = path.resolve(options.compiled);
49
+
50
+ // Load compiled registry
51
+ const registryPath = path.join(registryDir, 'registry.json');
52
+ const registryRaw = await readFile(registryPath, 'utf-8');
53
+ const registry = JSON.parse(registryRaw);
54
+
55
+ // Build schema map: component name → JSON Schema object
56
+ // Build nested blocks map: component name → array of { path, allow? }
57
+ const schemaMap = new Map();
58
+ const knownComponents = new Set();
59
+ const nestedBlocksMap = new Map();
60
+ for (const component of registry.components) {
61
+ knownComponents.add(component.name);
62
+ if (component.schemaFile) {
63
+ const schemaPath = path.join(registryDir, component.schemaFile);
64
+ const schemaRaw = await readFile(schemaPath, 'utf-8');
65
+ schemaMap.set(component.name, JSON.parse(schemaRaw));
66
+ }
67
+ if (Array.isArray(component.nestedBlocks) && component.nestedBlocks.length > 0) {
68
+ nestedBlocksMap.set(component.name, component.nestedBlocks);
69
+ }
70
+ }
71
+
72
+ // Find all compiled route files
73
+ const routesDir = path.join(contentRoot, 'routes');
74
+ const pageFiles = await findJsonFiles(routesDir);
75
+
76
+ const errors = [];
77
+ const warnings = [];
78
+ let pagesValidated = 0;
79
+ let blocksValidated = 0;
80
+
81
+ for (const pageFile of pageFiles) {
82
+ const relativePath = path.relative(contentRoot, pageFile);
83
+ let pageData;
84
+ try {
85
+ const raw = await readFile(pageFile, 'utf-8');
86
+ pageData = JSON.parse(raw);
87
+ } catch (err) {
88
+ errors.push({ page: relativePath, message: `failed to parse JSON: ${err.message}` });
89
+ continue;
90
+ }
91
+
92
+ pagesValidated++;
93
+ const isPublished = pageData.status === 'published';
94
+ const collector = isPublished ? errors : warnings;
95
+
96
+ const blocks = collectBlocks(pageData);
97
+
98
+ for (const block of blocks) {
99
+ blocksValidated++;
100
+
101
+ if (!block.component) {
102
+ collector.push({
103
+ page: relativePath,
104
+ blockId: block.id ?? '<unknown>',
105
+ message: 'block is missing "component" field',
106
+ });
107
+ continue;
108
+ }
109
+
110
+ // Check component exists in registry
111
+ if (!knownComponents.has(block.component)) {
112
+ collector.push({
113
+ page: relativePath,
114
+ blockId: block.id ?? '<unknown>',
115
+ component: block.component,
116
+ message: `unknown component: "${block.component}"`,
117
+ });
118
+ continue;
119
+ }
120
+
121
+ // Validate props against JSON Schema
122
+ const schema = schemaMap.get(block.component);
123
+ if (!schema) continue; // no schema file — component registered but no schema to check
124
+
125
+ const props = block.props ?? {};
126
+ const issues = validateJsonSchema(props, schema, 'props');
127
+ for (const issue of issues) {
128
+ collector.push({
129
+ page: relativePath,
130
+ blockId: block.id ?? '<unknown>',
131
+ component: block.component,
132
+ path: issue.path,
133
+ message: issue.message,
134
+ });
135
+ }
136
+
137
+ // Recursively validate nested blocks (blockRef patterns)
138
+ const nestedIssues = validateNestedBlocks(
139
+ block.component, props, knownComponents, schemaMap, nestedBlocksMap,
140
+ );
141
+ for (const issue of nestedIssues) {
142
+ collector.push({
143
+ page: relativePath,
144
+ blockId: block.id ?? '<unknown>',
145
+ component: block.component,
146
+ ...issue,
147
+ });
148
+ }
149
+ }
150
+ }
151
+
152
+ // Also validate fragments
153
+ const fragmentsDir = path.join(contentRoot, 'fragments');
154
+ const fragmentFiles = await findJsonFiles(fragmentsDir);
155
+
156
+ for (const fragmentFile of fragmentFiles) {
157
+ const relativePath = path.relative(contentRoot, fragmentFile);
158
+ let fragmentData;
159
+ try {
160
+ const raw = await readFile(fragmentFile, 'utf-8');
161
+ fragmentData = JSON.parse(raw);
162
+ } catch (err) {
163
+ errors.push({ fragment: relativePath, message: `failed to parse JSON: ${err.message}` });
164
+ continue;
165
+ }
166
+
167
+ if (!fragmentData.component) continue;
168
+ blocksValidated++;
169
+
170
+ if (!knownComponents.has(fragmentData.component)) {
171
+ errors.push({
172
+ fragment: relativePath,
173
+ component: fragmentData.component,
174
+ message: `unknown component: "${fragmentData.component}"`,
175
+ });
176
+ continue;
177
+ }
178
+
179
+ const schema = schemaMap.get(fragmentData.component);
180
+ if (!schema) continue;
181
+
182
+ const props = fragmentData.props ?? {};
183
+ const issues = validateJsonSchema(props, schema, 'props');
184
+ for (const issue of issues) {
185
+ errors.push({
186
+ fragment: relativePath,
187
+ component: fragmentData.component,
188
+ path: issue.path,
189
+ message: issue.message,
190
+ });
191
+ }
192
+ }
193
+
194
+ const success = errors.length === 0;
195
+ return {
196
+ success,
197
+ pagesValidated,
198
+ blocksValidated,
199
+ errors,
200
+ ...(warnings.length > 0 ? { warnings } : {}),
201
+ };
202
+ }
203
+
204
+ // ---------------------------------------------------------------------------
205
+ // Lightweight JSON Schema draft-2020-12 validator
206
+ //
207
+ // Covers the subset that Zod generates via z.toJSONSchema:
208
+ // type, required, properties, additionalProperties,
209
+ // minLength, minimum, maximum, enum, const,
210
+ // items, minItems, maxItems, anyOf, allOf, $ref (local)
211
+ // ---------------------------------------------------------------------------
212
+
213
+ function validateJsonSchema(value, schema, pathPrefix = '') {
214
+ const issues = [];
215
+ _validate(value, schema, pathPrefix, issues, schema);
216
+ return issues;
217
+ }
218
+
219
+ function _validate(value, schema, currentPath, issues, rootSchema) {
220
+ if (!schema || typeof schema !== 'object') return;
221
+
222
+ // $ref — resolve local references
223
+ if (schema.$ref) {
224
+ const resolved = resolveRef(schema.$ref, rootSchema);
225
+ if (resolved) {
226
+ _validate(value, resolved, currentPath, issues, rootSchema);
227
+ }
228
+ return;
229
+ }
230
+
231
+ // anyOf — value must match at least one
232
+ if (Array.isArray(schema.anyOf)) {
233
+ const anyMatches = schema.anyOf.some((sub) => {
234
+ const subIssues = [];
235
+ _validate(value, sub, currentPath, subIssues, rootSchema);
236
+ return subIssues.length === 0;
237
+ });
238
+ if (!anyMatches) {
239
+ issues.push({ path: currentPath, message: `${currentPath}: does not match any allowed schema` });
240
+ }
241
+ return;
242
+ }
243
+
244
+ // allOf — value must match all
245
+ if (Array.isArray(schema.allOf)) {
246
+ for (const sub of schema.allOf) {
247
+ _validate(value, sub, currentPath, issues, rootSchema);
248
+ }
249
+ return;
250
+ }
251
+
252
+ // const
253
+ if ('const' in schema) {
254
+ if (value !== schema.const) {
255
+ issues.push({ path: currentPath, message: `${currentPath}: expected constant "${schema.const}", got ${JSON.stringify(value)}` });
256
+ }
257
+ return;
258
+ }
259
+
260
+ // enum
261
+ if (Array.isArray(schema.enum)) {
262
+ if (!schema.enum.includes(value)) {
263
+ issues.push({ path: currentPath, message: `${currentPath}: expected one of [${schema.enum.join(', ')}], got ${JSON.stringify(value)}` });
264
+ }
265
+ return;
266
+ }
267
+
268
+ // type checking
269
+ const types = schema.type ? (Array.isArray(schema.type) ? schema.type : [schema.type]) : null;
270
+ if (types) {
271
+ const actualType = getJsonType(value);
272
+ const typeMatch = types.some((t) =>
273
+ t === actualType || (t === 'integer' && actualType === 'number' && Number.isInteger(value))
274
+ );
275
+ if (!typeMatch) {
276
+ // Allow null for nullable schemas
277
+ if (value !== null || !types.includes('null')) {
278
+ issues.push({ path: currentPath, message: `${currentPath}: expected type ${types.join('|')}, got ${actualType}` });
279
+ return; // Stop further checks if type is wrong
280
+ }
281
+ }
282
+ }
283
+
284
+ if (value === null || value === undefined) return;
285
+
286
+ // String checks
287
+ if (typeof value === 'string') {
288
+ if (typeof schema.minLength === 'number' && value.length < schema.minLength) {
289
+ issues.push({ path: currentPath, message: `${currentPath}: string length ${value.length} is less than minLength ${schema.minLength}` });
290
+ }
291
+ if (typeof schema.maxLength === 'number' && value.length > schema.maxLength) {
292
+ issues.push({ path: currentPath, message: `${currentPath}: string length ${value.length} exceeds maxLength ${schema.maxLength}` });
293
+ }
294
+ }
295
+
296
+ // Number checks
297
+ if (typeof value === 'number') {
298
+ if (typeof schema.minimum === 'number' && value < schema.minimum) {
299
+ issues.push({ path: currentPath, message: `${currentPath}: ${value} is less than minimum ${schema.minimum}` });
300
+ }
301
+ if (typeof schema.maximum === 'number' && value > schema.maximum) {
302
+ issues.push({ path: currentPath, message: `${currentPath}: ${value} exceeds maximum ${schema.maximum}` });
303
+ }
304
+ }
305
+
306
+ // Array checks
307
+ if (Array.isArray(value)) {
308
+ if (typeof schema.minItems === 'number' && value.length < schema.minItems) {
309
+ issues.push({ path: currentPath, message: `${currentPath}: array has ${value.length} items, minimum is ${schema.minItems}` });
310
+ }
311
+ if (typeof schema.maxItems === 'number' && value.length > schema.maxItems) {
312
+ issues.push({ path: currentPath, message: `${currentPath}: array has ${value.length} items, maximum is ${schema.maxItems}` });
313
+ }
314
+ if (schema.items) {
315
+ for (let i = 0; i < value.length; i++) {
316
+ _validate(value[i], schema.items, `${currentPath}[${i}]`, issues, rootSchema);
317
+ }
318
+ }
319
+ }
320
+
321
+ // Object checks
322
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
323
+ // Required fields
324
+ if (Array.isArray(schema.required)) {
325
+ for (const key of schema.required) {
326
+ if (!(key in value) || value[key] === undefined) {
327
+ issues.push({ path: `${currentPath}.${key}`, message: `${currentPath}.${key}: required field is missing` });
328
+ }
329
+ }
330
+ }
331
+ // Properties
332
+ if (schema.properties) {
333
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
334
+ if (key in value) {
335
+ _validate(value[key], propSchema, `${currentPath}.${key}`, issues, rootSchema);
336
+ }
337
+ }
338
+ }
339
+ }
340
+ }
341
+
342
+ function getJsonType(value) {
343
+ if (value === null) return 'null';
344
+ if (Array.isArray(value)) return 'array';
345
+ return typeof value;
346
+ }
347
+
348
+ function resolveRef(ref, rootSchema) {
349
+ if (!ref.startsWith('#/')) return null;
350
+ const parts = ref.slice(2).split('/');
351
+ let current = rootSchema;
352
+ for (const part of parts) {
353
+ current = current?.[part];
354
+ }
355
+ return current ?? null;
356
+ }
357
+
358
+ // ---------------------------------------------------------------------------
359
+ // Content helpers
360
+ // ---------------------------------------------------------------------------
361
+
362
+ // ---------------------------------------------------------------------------
363
+ // Nested block validation — recursively validates blocks inside blockRef slots
364
+ // ---------------------------------------------------------------------------
365
+
366
+ function validateNestedBlocks(parentComponent, props, knownComponents, schemaMap, nestedBlocksMap) {
367
+ const issues = [];
368
+ // Find all block-shaped arrays in props by walking the data
369
+ const nestedBlocks = findNestedBlockArrays(props);
370
+ for (const { path: blockPath, blocks } of nestedBlocks) {
371
+ for (const [index, nestedBlock] of blocks.entries()) {
372
+ if (!nestedBlock || typeof nestedBlock !== 'object') continue;
373
+ const childComponent = nestedBlock.component;
374
+ if (!childComponent || typeof childComponent !== 'string') continue;
375
+
376
+ const nestedPath = `${blockPath}[${index}]`;
377
+
378
+ // Check nested component exists in registry
379
+ if (!knownComponents.has(childComponent)) {
380
+ issues.push({
381
+ path: `${nestedPath}.component`,
382
+ message: `${parentComponent} → ${nestedPath}: unknown nested component "${childComponent}"`,
383
+ });
384
+ continue;
385
+ }
386
+
387
+ // Validate nested block's props against its own schema
388
+ const childSchema = schemaMap.get(childComponent);
389
+ if (!childSchema) continue;
390
+
391
+ const childProps = nestedBlock.props ?? {};
392
+ const childIssues = validateJsonSchema(childProps, childSchema, `${nestedPath}.props`);
393
+ for (const issue of childIssues) {
394
+ issues.push({
395
+ nestedComponent: childComponent,
396
+ path: issue.path,
397
+ message: `${parentComponent} → ${childComponent}: ${issue.message}`,
398
+ });
399
+ }
400
+
401
+ // Recurse further if the nested component itself has nested blocks
402
+ const deeperIssues = validateNestedBlocks(
403
+ childComponent, childProps, knownComponents, schemaMap, nestedBlocksMap,
404
+ );
405
+ issues.push(...deeperIssues);
406
+ }
407
+ }
408
+ return issues;
409
+ }
410
+
411
+ /**
412
+ * Walks a props object looking for arrays of block-shaped objects
413
+ * (objects with `component` + `props` fields).
414
+ */
415
+ function findNestedBlockArrays(obj, prefix = 'props') {
416
+ const results = [];
417
+ if (!obj || typeof obj !== 'object') return results;
418
+
419
+ for (const [key, value] of Object.entries(obj)) {
420
+ const currentPath = `${prefix}.${key}`;
421
+ if (Array.isArray(value)) {
422
+ // Check if array contains block-shaped objects
423
+ const hasBlocks = value.some(
424
+ (item) => item && typeof item === 'object' && typeof item.component === 'string' && 'props' in item,
425
+ );
426
+ if (hasBlocks) {
427
+ results.push({ path: currentPath, blocks: value });
428
+ }
429
+ // Also recurse into array items that are objects (for nested structures like tabs[].blocks[])
430
+ for (const item of value) {
431
+ if (item && typeof item === 'object' && !Array.isArray(item)) {
432
+ results.push(...findNestedBlockArrays(item, currentPath + '[]'));
433
+ }
434
+ }
435
+ } else if (typeof value === 'object' && value !== null) {
436
+ results.push(...findNestedBlockArrays(value, currentPath));
437
+ }
438
+ }
439
+ return results;
440
+ }
441
+
442
+ function collectBlocks(pageData) {
443
+ const blocks = [];
444
+ if (pageData.regions && typeof pageData.regions === 'object') {
445
+ for (const region of Object.values(pageData.regions)) {
446
+ if (Array.isArray(region)) blocks.push(...region);
447
+ else if (region && typeof region === 'object' && Array.isArray(region.children)) blocks.push(...region.children);
448
+ }
449
+ }
450
+ if (Array.isArray(pageData.blocks)) blocks.push(...pageData.blocks);
451
+ return blocks;
452
+ }
453
+
454
+ async function findPageFiles(dir) {
455
+ return findFilesMatching(dir, (name) => name === '_index.json');
456
+ }
457
+
458
+ async function findJsonFiles(dir) {
459
+ return findFilesMatching(dir, (name) => name.endsWith('.json'));
460
+ }
461
+
462
+ async function findFilesMatching(dir, predicate) {
463
+ const results = [];
464
+ try { await collectFiles(dir, predicate, results); }
465
+ catch (err) { if (err.code !== 'ENOENT') throw err; }
466
+ return results;
467
+ }
468
+
469
+ async function collectFiles(dir, predicate, results) {
470
+ const entries = await readdir(dir, { withFileTypes: true });
471
+ for (const entry of entries) {
472
+ const full = path.join(dir, entry.name);
473
+ if (entry.isDirectory()) await collectFiles(full, predicate, results);
474
+ else if (predicate(entry.name)) results.push(full);
475
+ }
476
+ }
477
+
478
+ function parseArgs(argv) {
479
+ const out = {};
480
+ for (let i = 0; i < argv.length; i++) {
481
+ const arg = argv[i];
482
+ if (!arg.startsWith('--')) continue;
483
+ const body = arg.slice(2);
484
+ const equals = body.indexOf('=');
485
+ if (equals >= 0) { out[body.slice(0, equals)] = body.slice(equals + 1); continue; }
486
+ out[body] = argv[++i];
487
+ }
488
+ return out;
489
+ }