@gradial/aci 0.1.1 → 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 (47) hide show
  1. package/README.md +46 -1
  2. package/bin/aci +0 -0
  3. package/bin/aci.js +139 -27
  4. package/dist/assets/index.d.ts +2 -0
  5. package/dist/assets/index.js +2 -0
  6. package/dist/astro/index.js +4 -4
  7. package/dist/block-ref.d.ts +34 -0
  8. package/dist/block-ref.js +34 -0
  9. package/dist/define-component.d.ts +1 -0
  10. package/dist/define-component.js +1 -0
  11. package/dist/define-layout.d.ts +1 -0
  12. package/dist/define-layout.js +1 -0
  13. package/dist/dev/index.d.ts +6 -0
  14. package/dist/dev/index.js +66 -0
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.js +4 -1
  17. package/dist/next/asset-route.d.ts +9 -0
  18. package/dist/next/asset-route.js +15 -0
  19. package/dist/next/config.d.ts +2 -10
  20. package/dist/next/config.js +5 -2
  21. package/dist/next/server.d.ts +4 -0
  22. package/dist/next/server.js +53 -0
  23. package/dist/providers/s3.d.ts +2 -0
  24. package/dist/providers/s3.js +13 -1
  25. package/dist/react/GradialMedia.d.ts +24 -0
  26. package/dist/react/GradialMedia.js +31 -0
  27. package/dist/react/GradialPicture.d.ts +14 -0
  28. package/dist/react/GradialPicture.js +30 -0
  29. package/dist/react/GradialVideoPlayer.d.ts +13 -0
  30. package/dist/react/GradialVideoPlayer.js +28 -0
  31. package/dist/react/index.d.ts +3 -0
  32. package/dist/react/index.js +3 -0
  33. package/dist/sveltekit/index.js +6 -1
  34. package/dist/types/component.d.ts +8 -3
  35. package/dist/types/image.d.ts +2 -2
  36. package/dist/types/image.js +1 -1
  37. package/dist/types/index.d.ts +2 -0
  38. package/dist/types/index.js +2 -0
  39. package/dist/types/media.d.ts +69 -0
  40. package/dist/types/media.js +86 -0
  41. package/dist/types/video.d.ts +70 -0
  42. package/dist/types/video.js +22 -0
  43. package/package.json +13 -9
  44. package/src/cli/compile-registry.mjs +142 -1
  45. package/src/cli/validate-content.mjs +98 -0
  46. package/src/cli/css-stub-loader.mjs +0 -27
  47. package/src/cli/generate-registry.mjs +0 -72
@@ -5,6 +5,16 @@ import { pathToFileURL } from 'node:url';
5
5
  import { z } from 'zod';
6
6
  import { GradialImageSchema } from '../types/image.ts';
7
7
 
8
+ // Shim React global so JSX-containing component files can be loaded.
9
+ // The compile-registry only extracts contract metadata (schemas, imageSlots)
10
+ // and never renders components, but importing .tsx files requires the JSX
11
+ // runtime to be resolvable. Next.js uses jsx: "preserve" which leaves React
12
+ // references in the transpiled output.
13
+ try {
14
+ const React = await import('react');
15
+ if (!globalThis.React) globalThis.React = React;
16
+ } catch { /* react not available — component files without JSX will still work */ }
17
+
8
18
  const args = parseArgs(process.argv.slice(2));
9
19
 
10
20
  try {
@@ -62,6 +72,19 @@ async function compileRegistry(options) {
62
72
  const seenLayouts = new Set();
63
73
  const layoutEntries = layouts.map((layout, index) => validateLayout(layout, index, seenLayouts));
64
74
 
75
+ // Post-compilation: detect nested block patterns in JSON schemas and
76
+ // validate that referenced component names exist in the registry.
77
+ const knownNames = new Set(componentEntries.map((c) => c.name));
78
+ for (const entry of componentEntries) {
79
+ const schemaPath = path.join(outDir, entry.schemaFile);
80
+ const schemaRaw = await import('node:fs/promises').then((fs) => fs.readFile(schemaPath, 'utf-8'));
81
+ const schema = JSON.parse(schemaRaw);
82
+ const nestedBlocks = collectNestedBlockRefs(schema, knownNames, entry.name);
83
+ if (nestedBlocks.length > 0) {
84
+ entry.nestedBlocks = nestedBlocks;
85
+ }
86
+ }
87
+
65
88
  const registry = {
66
89
  manifestVersion: '1',
67
90
  generatedAt: new Date().toISOString(),
@@ -192,6 +215,14 @@ function collectGradialImageFieldsFromSchema(schema, prefix, out) {
192
215
  return;
193
216
  }
194
217
 
218
+ const unionOptions = unionOptionSchemas(schema);
219
+ if (unionOptions) {
220
+ for (const option of unionOptions) {
221
+ collectGradialImageFieldsFromSchema(option, prefix, out);
222
+ }
223
+ return;
224
+ }
225
+
195
226
  const def = unwrapSchema(schema)?._def;
196
227
  const shape = typeof def?.shape === 'function' ? def.shape() : def?.shape;
197
228
  if (!shape || typeof shape !== 'object') {
@@ -204,6 +235,14 @@ function collectGradialImageFieldsFromSchema(schema, prefix, out) {
204
235
  }
205
236
  }
206
237
 
238
+ function unionOptionSchemas(schema) {
239
+ const def = unwrapSchema(schema)?._def;
240
+ if (!def) return null;
241
+ if (Array.isArray(def.options)) return def.options;
242
+ if (def.options instanceof Map) return Array.from(def.options.values());
243
+ return null;
244
+ }
245
+
207
246
  function arrayItemSchema(schema) {
208
247
  let current = schema;
209
248
  const seen = new Set();
@@ -231,10 +270,13 @@ function isGradialImageSchema(schema) {
231
270
  target: 'draft-2020-12',
232
271
  unrepresentable: 'throw',
233
272
  });
273
+ const sources = compiled?.properties?.sources;
274
+ const sourcesIsArray = sources?.type === 'array'
275
+ || (Array.isArray(sources?.anyOf) && sources.anyOf.some((s) => s?.type === 'array'));
234
276
  return compiled?.type === 'object'
235
277
  && compiled?.properties?.$type?.const === 'gradial.image'
236
278
  && compiled?.properties?.fallback?.type === 'object'
237
- && compiled?.properties?.sources?.type === 'array';
279
+ && sourcesIsArray;
238
280
  } catch {
239
281
  return false;
240
282
  }
@@ -344,6 +386,105 @@ function childSchemas(def) {
344
386
  return children;
345
387
  }
346
388
 
389
+ // ---------------------------------------------------------------------------
390
+ // Nested block detection — walks generated JSON schemas to find blockRef
391
+ // patterns (objects with `component` enum + `props`) and validates that
392
+ // referenced component names exist in the registry.
393
+ // ---------------------------------------------------------------------------
394
+
395
+ function collectNestedBlockRefs(schema, knownNames, componentName) {
396
+ const results = [];
397
+ _walkSchemaForBlockRefs(schema, schema, '', results, knownNames, componentName);
398
+ return results;
399
+ }
400
+
401
+ function _walkSchemaForBlockRefs(node, rootSchema, currentPath, results, knownNames, componentName) {
402
+ if (!node || typeof node !== 'object') return;
403
+
404
+ // Resolve $ref
405
+ if (node.$ref) {
406
+ const resolved = resolveJsonSchemaRef(node.$ref, rootSchema);
407
+ if (resolved) _walkSchemaForBlockRefs(resolved, rootSchema, currentPath, results, knownNames, componentName);
408
+ return;
409
+ }
410
+
411
+ // Check if this object matches the blockRef signature:
412
+ // { type: "object", properties: { id, component, props } }
413
+ if (isBlockRefSchema(node)) {
414
+ const allow = extractBlockRefAllow(node, rootSchema);
415
+ // Validate allowed names exist
416
+ for (const name of allow) {
417
+ if (!knownNames.has(name)) {
418
+ throw new Error(
419
+ `${componentName}: nested block allowlist references unknown component "${name}" at ${currentPath || '(root)'}`,
420
+ );
421
+ }
422
+ }
423
+ results.push({
424
+ path: currentPath || '(root)',
425
+ ...(allow.length > 0 ? { allow } : {}),
426
+ });
427
+ return; // Don't recurse into the block schema itself
428
+ }
429
+
430
+ // Recurse into object properties
431
+ if (node.properties) {
432
+ for (const [key, propSchema] of Object.entries(node.properties)) {
433
+ const nextPath = currentPath ? `${currentPath}.${key}` : key;
434
+ _walkSchemaForBlockRefs(propSchema, rootSchema, nextPath, results, knownNames, componentName);
435
+ }
436
+ }
437
+
438
+ // Recurse into array items
439
+ if (node.items) {
440
+ _walkSchemaForBlockRefs(node.items, rootSchema, `${currentPath}[]`, results, knownNames, componentName);
441
+ }
442
+
443
+ // Recurse into anyOf / allOf / oneOf
444
+ for (const keyword of ['anyOf', 'allOf', 'oneOf']) {
445
+ if (Array.isArray(node[keyword])) {
446
+ for (const sub of node[keyword]) {
447
+ _walkSchemaForBlockRefs(sub, rootSchema, currentPath, results, knownNames, componentName);
448
+ }
449
+ }
450
+ }
451
+ }
452
+
453
+ function isBlockRefSchema(node) {
454
+ if (node.type !== 'object' || !node.properties) return false;
455
+ const props = node.properties;
456
+ // Must have component and props fields
457
+ if (!props.component || !props.props) return false;
458
+ // component must be a string or enum
459
+ const comp = props.component;
460
+ if (comp.type !== 'string' && !Array.isArray(comp.enum)) return false;
461
+ // props must be an object (record)
462
+ if (props.props.type !== 'object') return false;
463
+ return true;
464
+ }
465
+
466
+ function extractBlockRefAllow(node, rootSchema) {
467
+ let comp = node.properties.component;
468
+ // Resolve $ref on the component field
469
+ if (comp.$ref) {
470
+ comp = resolveJsonSchemaRef(comp.$ref, rootSchema) ?? comp;
471
+ }
472
+ if (Array.isArray(comp.enum)) {
473
+ return comp.enum.filter((v) => typeof v === 'string');
474
+ }
475
+ return [];
476
+ }
477
+
478
+ function resolveJsonSchemaRef(ref, rootSchema) {
479
+ if (!ref || !ref.startsWith('#/')) return null;
480
+ const parts = ref.slice(2).split('/');
481
+ let current = rootSchema;
482
+ for (const part of parts) {
483
+ current = current?.[part];
484
+ }
485
+ return current ?? null;
486
+ }
487
+
347
488
  function parseArgs(argv) {
348
489
  const out = {};
349
490
  for (let i = 0; i < argv.length; i++) {
@@ -53,8 +53,10 @@ async function validateContent(options) {
53
53
  const registry = JSON.parse(registryRaw);
54
54
 
55
55
  // Build schema map: component name → JSON Schema object
56
+ // Build nested blocks map: component name → array of { path, allow? }
56
57
  const schemaMap = new Map();
57
58
  const knownComponents = new Set();
59
+ const nestedBlocksMap = new Map();
58
60
  for (const component of registry.components) {
59
61
  knownComponents.add(component.name);
60
62
  if (component.schemaFile) {
@@ -62,6 +64,9 @@ async function validateContent(options) {
62
64
  const schemaRaw = await readFile(schemaPath, 'utf-8');
63
65
  schemaMap.set(component.name, JSON.parse(schemaRaw));
64
66
  }
67
+ if (Array.isArray(component.nestedBlocks) && component.nestedBlocks.length > 0) {
68
+ nestedBlocksMap.set(component.name, component.nestedBlocks);
69
+ }
65
70
  }
66
71
 
67
72
  // Find all compiled route files
@@ -128,6 +133,19 @@ async function validateContent(options) {
128
133
  message: issue.message,
129
134
  });
130
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
+ }
131
149
  }
132
150
  }
133
151
 
@@ -341,6 +359,86 @@ function resolveRef(ref, rootSchema) {
341
359
  // Content helpers
342
360
  // ---------------------------------------------------------------------------
343
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
+
344
442
  function collectBlocks(pageData) {
345
443
  const blocks = [];
346
444
  if (pageData.regions && typeof pageData.regions === 'object') {
@@ -1,27 +0,0 @@
1
- /**
2
- * Node.js custom loader that stubs CSS module imports.
3
- * Used by CLI tools (compile-registry, validate-content) that need to import
4
- * component files without actually loading CSS.
5
- *
6
- * Usage: node --import tsx --loader ./css-stub-loader.mjs <script>
7
- */
8
-
9
- const CSS_EXTENSIONS = ['.css', '.module.css', '.scss', '.module.scss', '.less'];
10
-
11
- export function resolve(specifier, context, nextResolve) {
12
- if (CSS_EXTENSIONS.some((ext) => specifier.endsWith(ext))) {
13
- return { url: `css-stub:${specifier}`, shortCircuit: true };
14
- }
15
- return nextResolve(specifier, context);
16
- }
17
-
18
- export function load(url, context, nextLoad) {
19
- if (url.startsWith('css-stub:')) {
20
- return {
21
- format: 'module',
22
- source: 'export default {};',
23
- shortCircuit: true,
24
- };
25
- }
26
- return nextLoad(url, context);
27
- }
@@ -1,72 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * generate-registry — Derives a runtime registry from co-located component contracts.
4
- *
5
- * Usage:
6
- * baremetal generate-registry --contracts <path> --output <path>
7
- *
8
- * --contracts Path to the aggregation module (default-exports an array of ComponentContract).
9
- * --output Path to the generated TypeScript file.
10
- *
11
- * The generated file imports the contracts module and derives:
12
- * - `registry`: a flat map of component name -> component function (for the renderer)
13
- * - `contractMap`: a flat map of component name -> full contract (for tooling/validation)
14
- */
15
- import { writeFile, mkdir } from 'node:fs/promises';
16
- import path from 'node:path';
17
-
18
- const args = parseArgs(process.argv.slice(2));
19
-
20
- if (!args.contracts) {
21
- console.error('Error: --contracts <path> is required');
22
- process.exitCode = 1;
23
- } else if (!args.output) {
24
- console.error('Error: --output <path> is required');
25
- process.exitCode = 1;
26
- } else {
27
- try {
28
- await generateRegistry(args);
29
- console.log(`Registry generated at ${args.output}`);
30
- } catch (error) {
31
- console.error(`Error: ${error?.message ?? error}`);
32
- process.exitCode = 1;
33
- }
34
- }
35
-
36
- async function generateRegistry({ contracts, output }) {
37
- const contractsPath = contracts.replace(/\.(ts|tsx|js|mjs)$/, '');
38
- const outputPath = path.resolve(output);
39
-
40
- await mkdir(path.dirname(outputPath), { recursive: true });
41
-
42
- const content = `// AUTO-GENERATED — do not edit. Run \`baremetal generate-registry\` to regenerate.
43
- import type { ComponentContract } from '@baremetal/sdk';
44
- import contracts from '${contractsPath}';
45
-
46
- export const registry = Object.fromEntries(
47
- contracts.filter((c) => c.component != null).map((c) => [c.name, c.component]),
48
- );
49
-
50
- export const contractMap: Record<string, ComponentContract> = Object.fromEntries(
51
- contracts.map((c) => [c.name, c]),
52
- );
53
- `;
54
-
55
- await writeFile(outputPath, content, 'utf-8');
56
- }
57
-
58
- function parseArgs(argv) {
59
- const out = {};
60
- for (let i = 0; i < argv.length; i++) {
61
- const arg = argv[i];
62
- if (!arg.startsWith('--')) continue;
63
- const body = arg.slice(2);
64
- const equals = body.indexOf('=');
65
- if (equals >= 0) {
66
- out[body.slice(0, equals)] = body.slice(equals + 1);
67
- continue;
68
- }
69
- out[body] = argv[++i];
70
- }
71
- return out;
72
- }