@gradial/aci 0.1.20-preview.2 → 0.1.20
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 +48 -110
- package/bin/{aci → aci-darwin-arm64} +0 -0
- package/bin/aci.js +99 -6
- package/dist/assets/index.d.ts +3 -3
- package/dist/assets/index.js +3 -3
- package/dist/astro/index.d.ts +14 -3
- package/dist/astro/index.js +24 -5
- package/dist/content/assets.d.ts +17 -0
- package/dist/content/assets.js +65 -0
- package/dist/content/index.d.ts +2 -0
- package/dist/content/index.js +2 -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/define-component.d.ts +1 -2
- package/dist/define-component.js +73 -11
- package/dist/define-layout.d.ts +2 -0
- package/dist/define-layout.js +3 -0
- package/dist/dev/content-watch.d.ts +37 -0
- package/dist/dev/content-watch.js +154 -0
- package/dist/dev/index.d.ts +24 -43
- package/dist/dev/index.js +4 -169
- package/dist/index.d.ts +14 -5
- package/dist/index.js +8 -8
- package/dist/next/asset-route.d.ts +2 -0
- package/dist/next/asset-route.js +12 -4
- package/dist/next/config.d.ts +2 -2
- package/dist/next/config.js +6 -6
- package/dist/next/content-watch.js +6 -1
- package/dist/next/index.d.ts +2 -2
- package/dist/next/index.js +2 -2
- package/dist/next/middleware.d.ts +7 -3
- package/dist/next/middleware.js +12 -7
- package/dist/next/page.d.ts +16 -15
- package/dist/next/page.js +30 -10
- package/dist/next/render.d.ts +5 -0
- package/dist/next/render.js +17 -0
- package/dist/next/root-layout.d.ts +4 -0
- package/dist/next/root-layout.js +5 -0
- package/dist/next/server.d.ts +14 -13
- package/dist/next/server.js +13 -3
- 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/react/Media.d.ts +24 -0
- package/dist/react/Media.js +30 -0
- package/dist/react/Picture.d.ts +12 -0
- package/dist/react/Picture.js +11 -0
- package/dist/react/VideoPlayer.d.ts +13 -0
- package/dist/react/{GradialVideoPlayer.js → VideoPlayer.js} +6 -5
- package/dist/react/index.d.ts +333 -3
- package/dist/react/index.js +89 -3
- package/dist/registry.d.ts +15 -0
- package/dist/registry.js +10 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.js +15 -2
- package/dist/types/block-ref.d.ts +26 -0
- package/dist/types/block-ref.js +28 -0
- package/dist/types/component.d.ts +36 -41
- package/dist/types/config.d.ts +1 -2
- package/dist/types/image.d.ts +33 -20
- package/dist/types/image.js +42 -50
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/link.d.ts +38 -0
- package/dist/types/link.js +58 -0
- package/dist/types/media.d.ts +16 -35
- package/dist/types/media.js +15 -84
- package/dist/types/page.d.ts +1 -2
- package/dist/types/render-mode.d.ts +0 -1
- package/dist/types/renderer.d.ts +1 -1
- package/dist/types/rich-text.d.ts +40 -0
- package/dist/types/rich-text.js +39 -0
- package/dist/types/video.d.ts +7 -13
- package/dist/types/video.js +3 -3
- package/package.json +32 -74
- package/src/cli/compile-registry.mjs +236 -59
- package/src/cli/validate-content.mjs +38 -75
- package/src/types/image.ts +79 -60
- package/templates/astro/template/.aci.yaml +27 -0
- package/templates/astro/template/.storybook/main.ts +79 -0
- package/templates/astro/template/.storybook/manager.ts +6 -0
- package/templates/astro/template/.storybook/preview.ts +33 -0
- package/templates/astro/template/astro.config.mjs +37 -0
- package/templates/astro/template/package.json.tmpl +41 -0
- package/templates/astro/template/src/cms/contracts/layouts/index.ts +18 -0
- package/templates/astro/template/src/cms/registry.ts +28 -0
- package/templates/astro/template/src/components/chrome/Footer.astro +107 -0
- package/templates/astro/template/src/components/chrome/Header.astro +94 -0
- package/templates/astro/template/src/components/elements/AtomicBlock.astro +15 -0
- package/templates/astro/template/src/components/elements/AtomicBlocks.astro +16 -0
- package/templates/astro/template/src/components/elements/Button.astro +51 -0
- package/templates/astro/template/src/components/elements/ButtonBlock.astro +25 -0
- package/templates/astro/template/src/components/elements/Container.astro +11 -0
- package/templates/astro/template/src/components/elements/Icon.astro +31 -0
- package/templates/astro/template/src/components/elements/Logo.astro +29 -0
- package/templates/astro/template/src/components/elements/RemoteImage.astro +41 -0
- package/templates/astro/template/src/components/sections/ContainerSection.astro +32 -0
- package/templates/astro/template/src/components/sections/FeatureCard.astro +23 -0
- package/templates/astro/template/src/components/sections/FeatureGrid.astro +35 -0
- package/templates/astro/template/src/components/sections/HomeHero.astro +51 -0
- package/templates/astro/template/src/components/sections/StatsBar.astro +33 -0
- package/templates/astro/template/src/design-system/styles.css +399 -0
- package/templates/astro/template/src/design-system/tokens/colors.css +60 -0
- package/templates/astro/template/src/design-system/tokens/components.css +69 -0
- package/templates/astro/template/src/design-system/tokens/index.css +6 -0
- package/templates/astro/template/src/design-system/tokens/radii.css +5 -0
- package/templates/astro/template/src/design-system/tokens/shadows.css +5 -0
- package/templates/astro/template/src/design-system/tokens/spacing.css +13 -0
- package/templates/astro/template/src/design-system/tokens/typography.css +24 -0
- package/templates/astro/template/src/env.d.ts +1 -0
- package/templates/astro/template/src/fixtures/atomicBlocks.ts +33 -0
- package/templates/astro/template/src/fixtures/composition.ts +31 -0
- package/templates/astro/template/src/fixtures/index.ts +17 -0
- package/templates/astro/template/src/fixtures/sections.ts +39 -0
- package/templates/astro/template/src/layouts/SiteLayout.astro +30 -0
- package/templates/astro/template/src/pages/[...slug].astro +61 -0
- package/templates/astro/template/src/render/RenderPage.astro +66 -0
- package/templates/astro/template/src/stories/CompositionStoryFrame.astro +15 -0
- package/templates/astro/template/src/stories/HomeHero.stories.ts +24 -0
- package/templates/astro/template/src/stories/SectionStoryFrame.astro +16 -0
- package/templates/astro/template/src/stories/StoryDocFrame.astro +31 -0
- package/templates/astro/template/tsconfig.json +11 -0
- package/templates/common/.agents/AGENTS.md +37 -0
- package/templates/common/.agents/skills/aci/SKILL.md +162 -0
- package/templates/common/.agents/skills/authoring/SKILL.md +590 -0
- package/templates/common/.agents/skills/authoring/references/block-registry.md +151 -0
- package/templates/common/.agents/skills/authoring/references/contracts.md +153 -0
- package/templates/common/.agents/skills/authoring/references/design-system.md +171 -0
- package/templates/common/.agents/skills/authoring/references/image-assets.md +73 -0
- package/templates/common/.agents/skills/authoring/references/repo-structure.md +157 -0
- package/templates/common/.agents/skills/migration/SKILL.md +160 -0
- package/templates/common/.agents/skills/migration/references/analyzing-source.md +215 -0
- package/templates/common/.agents/skills/migration/references/component-migration.md +200 -0
- package/templates/common/.agents/skills/migration/references/selector-map.md +31 -0
- package/templates/common/.content/.gradial-dam/blobs/sha256/7f/bf/7fbfc7fe230ff12688c496f660119e41d9725c4504d82e310c15665c73c09292 +0 -0
- package/templates/common/.content/.gradial-dam/blobs/sha256/a0/f5/a0f506689de9b15a7055f70103a3f02cd3ba105771ae2de6754df2ede1210d3a +0 -0
- package/templates/common/.content/.gradial-dam/blobs/sha256/dd/b7/ddb7e9642be08317bcd4b2bd26b58ba8d0d34dd7239e03e684a4f3634fa3845b +0 -0
- package/templates/common/.content/.gradial-dam/blobs/sha256/fb/32/fb3260e8286ea8231ea1c77b9c0be92d6edd9026565731eb14cfdcfd6b1b75b3 +0 -0
- package/templates/common/.content/config/site.json +192 -0
- package/templates/common/.content/dam/folders/seed/ast_site_about_hero/asset.json +9 -0
- package/templates/common/.content/dam/folders/seed/ast_site_about_hero/editorial.json +14 -0
- package/templates/common/.content/dam/folders/seed/ast_site_about_hero/governance.json +23 -0
- package/templates/common/.content/dam/folders/seed/ast_site_about_hero/versions/v0001.json +37 -0
- package/templates/common/.content/dam/folders/seed/ast_site_home_hero/asset.json +9 -0
- package/templates/common/.content/dam/folders/seed/ast_site_home_hero/editorial.json +14 -0
- package/templates/common/.content/dam/folders/seed/ast_site_home_hero/governance.json +23 -0
- package/templates/common/.content/dam/folders/seed/ast_site_home_hero/versions/v0001.json +37 -0
- package/templates/common/.content/fragments/navbar/_index.json +25 -0
- package/templates/common/.content/fragments/site-footer/_index.json +62 -0
- package/templates/common/.content/pages/about/_index.json +51 -0
- package/templates/common/.content/pages/home/_index.json +112 -0
- package/templates/common/.content/schemas/site.schema.json +123 -0
- package/templates/common/README.md.tmpl +45 -0
- package/templates/common/contracts/components/button.contract.ts +19 -0
- package/templates/common/contracts/components/container.contract.ts +19 -0
- package/templates/common/contracts/components/featureCard.contract.ts +15 -0
- package/templates/common/contracts/components/featureGrid.contract.ts +16 -0
- package/templates/common/contracts/components/homeHero.contract.ts +36 -0
- package/templates/common/contracts/components/index.ts +21 -0
- package/templates/common/contracts/components/siteFooter.contract.ts +45 -0
- package/templates/common/contracts/components/siteNavigation.contract.ts +30 -0
- package/templates/common/contracts/components/statsBar.contract.ts +19 -0
- package/templates/common/design-system/styles.css +346 -0
- package/templates/common/design-system/tokens/colors.css +110 -0
- package/templates/common/design-system/tokens/components.css +86 -0
- package/templates/common/design-system/tokens/index.css +6 -0
- package/templates/common/design-system/tokens/radii.css +5 -0
- package/templates/common/design-system/tokens/shadows.css +9 -0
- package/templates/common/design-system/tokens/spacing.css +26 -0
- package/templates/common/design-system/tokens/typography.css +32 -0
- package/templates/common/eslint-rules/no-raw-tailwind-values.js +127 -0
- package/templates/common/public/assets/Dwire-model.png +0 -0
- package/templates/common/public/assets/bare-metal-logo.png +0 -0
- package/templates/common/public/assets/meridian-logo.svg +8 -0
- package/templates/common/public/assets/seattle.avif +0 -0
- package/templates/nextjs/template/.aci.yaml +25 -0
- package/templates/nextjs/template/.storybook/main.ts +13 -0
- package/templates/nextjs/template/.storybook/preview.ts +34 -0
- package/templates/nextjs/template/eslint.config.mjs +36 -0
- package/templates/nextjs/template/next-env.d.ts +6 -0
- package/templates/nextjs/template/next.config.ts +8 -0
- package/templates/nextjs/template/package.json.tmpl +46 -0
- package/templates/nextjs/template/postcss.config.mjs +7 -0
- package/templates/nextjs/template/src/app/[[...slug]]/page.tsx +8 -0
- package/templates/nextjs/template/src/app/gradial/assets/[releaseId]/[...path]/route.ts +31 -0
- package/templates/nextjs/template/src/app/layout.tsx +29 -0
- package/templates/nextjs/template/src/cms/contracts/components/button.contract.ts +19 -0
- package/templates/nextjs/template/src/cms/contracts/components/container.contract.ts +20 -0
- package/templates/nextjs/template/src/cms/contracts/components/homeHero.contract.ts +36 -0
- package/templates/nextjs/template/src/cms/contracts/components/index.ts +15 -0
- package/templates/nextjs/template/src/cms/contracts/components/siteFooter.contract.ts +45 -0
- package/templates/nextjs/template/src/cms/contracts/components/siteNavigation.contract.ts +30 -0
- package/templates/nextjs/template/src/cms/contracts/layouts/index.ts +16 -0
- package/templates/nextjs/template/src/cms/registry.ts +28 -0
- package/templates/nextjs/template/src/components/chrome/Footer.tsx +86 -0
- package/templates/nextjs/template/src/components/chrome/Header.tsx +74 -0
- package/templates/nextjs/template/src/components/elements/AtomicBlock.tsx +13 -0
- package/templates/nextjs/template/src/components/elements/Button.tsx +40 -0
- package/templates/nextjs/template/src/components/elements/ButtonBlock.tsx +18 -0
- package/templates/nextjs/template/src/components/elements/Container.tsx +14 -0
- package/templates/nextjs/template/src/components/elements/Icon.tsx +29 -0
- package/templates/nextjs/template/src/components/elements/Logo.tsx +38 -0
- package/templates/nextjs/template/src/components/sections/ContainerSection.tsx +28 -0
- package/templates/nextjs/template/src/components/sections/FeatureCard.tsx +25 -0
- package/templates/nextjs/template/src/components/sections/FeatureGrid.tsx +36 -0
- package/templates/nextjs/template/src/components/sections/HomeHero.tsx +62 -0
- package/templates/nextjs/template/src/components/sections/StatsBar.tsx +30 -0
- package/templates/nextjs/template/src/middleware.ts +4 -0
- package/templates/nextjs/template/src/stories/Button.stories.tsx +38 -0
- package/templates/nextjs/template/src/stories/Header.stories.tsx +30 -0
- package/templates/nextjs/template/src/stories/HomeHero.stories.tsx +33 -0
- package/templates/nextjs/template/tsconfig.json +23 -0
- package/templates/nextjs/template/vercel.json +6 -0
- package/dist/content/cache.test.d.ts +0 -1
- package/dist/content/cache.test.js +0 -86
- package/dist/content/contract.d.ts +0 -83
- package/dist/content/contract.js +0 -104
- package/dist/content/tailwind-validator.d.ts +0 -6
- package/dist/content/tailwind-validator.js +0 -31
- package/dist/content/validation.d.ts +0 -108
- package/dist/content/validation.js +0 -184
- 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/react/GradialMedia.d.ts +0 -24
- package/dist/react/GradialMedia.js +0 -31
- package/dist/react/GradialPicture.d.ts +0 -14
- package/dist/react/GradialPicture.js +0 -30
- package/dist/react/GradialVideoPlayer.d.ts +0 -13
- package/dist/sveltekit/index.d.ts +0 -23
- package/dist/sveltekit/index.js +0 -62
- package/dist/sveltekit/preview.d.ts +0 -5
- package/dist/sveltekit/preview.js +0 -33
- package/src/cli/verify-renderer.mjs +0 -73
- package/src/types/component.ts +0 -59
- package/src/types/config.ts +0 -38
- package/src/types/data.ts +0 -47
- package/src/types/index.ts +0 -10
- package/src/types/layout.ts +0 -29
- package/src/types/media.ts +0 -125
- package/src/types/page.ts +0 -48
- package/src/types/render-mode.ts +0 -18
- package/src/types/renderer.ts +0 -83
- package/src/types/video.ts +0 -66
|
@@ -3,7 +3,8 @@ import { mkdir, writeFile } from 'node:fs/promises';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
5
|
import { z } from 'zod';
|
|
6
|
-
import {
|
|
6
|
+
import { ImageSchema } from '../types/image.ts';
|
|
7
|
+
import { getBlockRefMeta } from '../types/block-ref.ts';
|
|
7
8
|
|
|
8
9
|
const DATA_TYPE_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
9
10
|
|
|
@@ -47,30 +48,20 @@ async function compileRegistry(options) {
|
|
|
47
48
|
const schemaFiles = [];
|
|
48
49
|
for (const [index, component] of components.entries()) {
|
|
49
50
|
validateComponent(component, index, seenComponents);
|
|
50
|
-
const
|
|
51
|
-
const
|
|
51
|
+
const componentId = component.id;
|
|
52
|
+
const schema = compileSchema(component.props, componentId);
|
|
53
|
+
const schemaFile = `schemas/${componentId}.schema.json`;
|
|
52
54
|
const schemaPath = path.join(outDir, schemaFile);
|
|
53
55
|
await writeFile(schemaPath, JSON.stringify(schema, null, 2) + '\n');
|
|
54
56
|
schemaFiles.push(schemaPath);
|
|
55
|
-
const renderModes = component.renderModes ?? component.render;
|
|
56
|
-
const varyDimensions = component.varyDimensions ?? component.vary;
|
|
57
57
|
const imageSlots = normalizeImageSlots(component.imageSlots);
|
|
58
|
+
const slots = extractBlockSlots(component.props, componentId);
|
|
58
59
|
componentEntries.push({
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
static: Boolean(renderModes.canStatic ?? renderModes.static),
|
|
63
|
-
ssr: Boolean(renderModes.canSSR ?? renderModes.ssr),
|
|
64
|
-
clientIsland: Boolean(renderModes.canClientIsland ?? renderModes.clientIsland),
|
|
65
|
-
},
|
|
66
|
-
renderModes: {
|
|
67
|
-
canStatic: Boolean(renderModes.canStatic ?? renderModes.static),
|
|
68
|
-
canSSR: Boolean(renderModes.canSSR ?? renderModes.ssr),
|
|
69
|
-
canClientIsland: Boolean(renderModes.canClientIsland ?? renderModes.clientIsland),
|
|
70
|
-
},
|
|
71
|
-
...(component.defaultIslandMode ? { defaultIslandMode: component.defaultIslandMode } : {}),
|
|
72
|
-
...(varyDimensions?.length ? { vary: varyDimensions, varyDimensions } : {}),
|
|
60
|
+
id: componentId,
|
|
61
|
+
name: componentId,
|
|
62
|
+
props: schemaFile,
|
|
73
63
|
...(Object.keys(imageSlots).length ? { imageSlots } : {}),
|
|
64
|
+
...(Object.keys(slots).length ? { slots } : {}),
|
|
74
65
|
});
|
|
75
66
|
}
|
|
76
67
|
|
|
@@ -95,16 +86,14 @@ async function compileRegistry(options) {
|
|
|
95
86
|
});
|
|
96
87
|
}
|
|
97
88
|
|
|
98
|
-
|
|
99
|
-
// validate that referenced component names exist in the registry.
|
|
100
|
-
const knownNames = new Set(componentEntries.map((c) => c.name));
|
|
89
|
+
const knownNames = new Set(componentEntries.map((c) => c.id));
|
|
101
90
|
for (const entry of componentEntries) {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
91
|
+
for (const [slotName, slot] of Object.entries(entry.slots ?? {})) {
|
|
92
|
+
for (const childName of slot.allowed) {
|
|
93
|
+
if (!knownNames.has(childName)) {
|
|
94
|
+
throw new Error(`${entry.id}.slots.${slotName}.allowed references unknown component "${childName}"`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
108
97
|
}
|
|
109
98
|
}
|
|
110
99
|
|
|
@@ -117,6 +106,10 @@ async function compileRegistry(options) {
|
|
|
117
106
|
};
|
|
118
107
|
const registryPath = path.join(outDir, 'registry.json');
|
|
119
108
|
await writeFile(registryPath, JSON.stringify(registry, null, 2) + '\n');
|
|
109
|
+
|
|
110
|
+
const pageSchemaPath = await generatePageSchema(schemaDir, componentEntries, outDir);
|
|
111
|
+
schemaFiles.push(pageSchemaPath);
|
|
112
|
+
|
|
120
113
|
return { registry: registryPath, schemas: schemaFiles };
|
|
121
114
|
}
|
|
122
115
|
|
|
@@ -144,33 +137,82 @@ function validateDataModel(dataModel, index, seen) {
|
|
|
144
137
|
|
|
145
138
|
function validateComponent(component, index, seen) {
|
|
146
139
|
if (!component || typeof component !== 'object') throw new Error(`components[${index}] must be an object`);
|
|
147
|
-
if (!component.
|
|
148
|
-
if (seen.has(component.
|
|
149
|
-
seen.add(component.
|
|
150
|
-
if (!component.
|
|
151
|
-
validateZodSubset(component.
|
|
140
|
+
if (!component.id) throw new Error(`components[${index}].id is required`);
|
|
141
|
+
if (seen.has(component.id)) throw new Error(`duplicate component id ${component.id}`);
|
|
142
|
+
seen.add(component.id);
|
|
143
|
+
if (!component.props) throw new Error(`components[${index}].props is required`);
|
|
144
|
+
validateZodSubset(component.props, `components[${index}].props`);
|
|
152
145
|
validateImageSlotSchemaMatch(component, index);
|
|
153
|
-
const renderModes = component.renderModes ?? component.render;
|
|
154
|
-
if (!renderModes) throw new Error(`components[${index}].renderModes is required`);
|
|
155
|
-
const enabled = Boolean(renderModes.canStatic ?? renderModes.static)
|
|
156
|
-
|| Boolean(renderModes.canSSR ?? renderModes.ssr)
|
|
157
|
-
|| Boolean(renderModes.canClientIsland ?? renderModes.clientIsland);
|
|
158
|
-
if (!enabled) throw new Error(`components[${index}].renderModes must enable at least one mode`);
|
|
159
146
|
}
|
|
160
147
|
|
|
161
148
|
function validateImageSlotSchemaMatch(component, index) {
|
|
162
149
|
const imageSlots = normalizeImageSlots(component.imageSlots);
|
|
163
|
-
const imageFields =
|
|
150
|
+
const imageFields = collectImageFields(component.props);
|
|
164
151
|
for (const field of imageFields) {
|
|
165
152
|
if (!Object.prototype.hasOwnProperty.call(imageSlots, field)) {
|
|
166
|
-
throw new Error(`components[${index}].imageSlots.${field} is required because
|
|
153
|
+
throw new Error(`components[${index}].imageSlots.${field} is required because props field ${field} uses an image schema`);
|
|
167
154
|
}
|
|
168
155
|
}
|
|
169
156
|
for (const field of Object.keys(imageSlots)) {
|
|
170
157
|
if (!imageFields.has(field)) {
|
|
171
|
-
throw new Error(`components[${index}].
|
|
158
|
+
throw new Error(`components[${index}].props.${field} must use an image schema because imageSlots.${field} is declared`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function extractBlockSlots(schema, componentId) {
|
|
164
|
+
const shape = getSchemaShape(schema);
|
|
165
|
+
if (!shape) return {};
|
|
166
|
+
const out = {};
|
|
167
|
+
for (const [name, fieldSchema] of Object.entries(shape)) {
|
|
168
|
+
const unwrapped = unwrapOptionalSchema(fieldSchema);
|
|
169
|
+
const meta = getBlockRefMeta(unwrapped) ?? getBlockRefMeta(unwrapArraySchema(unwrapped));
|
|
170
|
+
if (meta) {
|
|
171
|
+
out[name] = {
|
|
172
|
+
allowed: [...meta.allowed],
|
|
173
|
+
multiple: meta.multiple,
|
|
174
|
+
required: false,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function unwrapOptionalSchema(schema) {
|
|
182
|
+
let current = schema;
|
|
183
|
+
const seen = new Set();
|
|
184
|
+
while (current && typeof current === 'object' && !seen.has(current)) {
|
|
185
|
+
seen.add(current);
|
|
186
|
+
const def = current._def;
|
|
187
|
+
if (!def || typeof def !== 'object') break;
|
|
188
|
+
const type = def.type;
|
|
189
|
+
const typeName = def.typeName;
|
|
190
|
+
if (type === 'optional' || type === 'nullable' || type === 'default' ||
|
|
191
|
+
typeName === 'ZodOptional' || typeName === 'ZodNullable' || typeName === 'ZodDefault') {
|
|
192
|
+
const inner = def.innerType ?? def.schema;
|
|
193
|
+
if (inner) { current = inner; continue; }
|
|
172
194
|
}
|
|
195
|
+
break;
|
|
173
196
|
}
|
|
197
|
+
return current;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function getSchemaShape(schema) {
|
|
201
|
+
if (!schema || typeof schema !== 'object') return null;
|
|
202
|
+
const def = schema._def;
|
|
203
|
+
if (!def) return null;
|
|
204
|
+
const shape = typeof def.shape === 'function' ? def.shape() : def.shape;
|
|
205
|
+
if (shape && typeof shape === 'object') return shape;
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function unwrapArraySchema(schema) {
|
|
210
|
+
if (!schema || typeof schema !== 'object') return null;
|
|
211
|
+
const def = schema._def;
|
|
212
|
+
if (!def) return null;
|
|
213
|
+
if (def.type === 'array' && def.element) return def.element;
|
|
214
|
+
if (def.typeName === 'ZodArray' && def.type) return def.type;
|
|
215
|
+
return null;
|
|
174
216
|
}
|
|
175
217
|
|
|
176
218
|
function normalizeImageSlots(imageSlots) {
|
|
@@ -230,34 +272,34 @@ function normalizeSlotOutput(slotKey, output, outputIndex) {
|
|
|
230
272
|
};
|
|
231
273
|
}
|
|
232
274
|
|
|
233
|
-
function
|
|
275
|
+
function collectImageFields(schema) {
|
|
234
276
|
const out = new Set();
|
|
235
277
|
|
|
236
|
-
|
|
278
|
+
collectImageFieldsFromSchema(schema, '', out);
|
|
237
279
|
|
|
238
280
|
return out;
|
|
239
281
|
}
|
|
240
282
|
|
|
241
|
-
function
|
|
283
|
+
function collectImageFieldsFromSchema(schema, prefix, out) {
|
|
242
284
|
if (!schema) {
|
|
243
285
|
return;
|
|
244
286
|
}
|
|
245
287
|
|
|
246
|
-
if (prefix &&
|
|
288
|
+
if (prefix && isImageSchema(schema)) {
|
|
247
289
|
out.add(prefix);
|
|
248
290
|
return;
|
|
249
291
|
}
|
|
250
292
|
|
|
251
293
|
const itemSchema = arrayItemSchema(schema);
|
|
252
294
|
if (itemSchema) {
|
|
253
|
-
|
|
295
|
+
collectImageFieldsFromSchema(itemSchema, prefix, out);
|
|
254
296
|
return;
|
|
255
297
|
}
|
|
256
298
|
|
|
257
299
|
const unionOptions = unionOptionSchemas(schema);
|
|
258
300
|
if (unionOptions) {
|
|
259
301
|
for (const option of unionOptions) {
|
|
260
|
-
|
|
302
|
+
collectImageFieldsFromSchema(option, prefix, out);
|
|
261
303
|
}
|
|
262
304
|
return;
|
|
263
305
|
}
|
|
@@ -270,7 +312,7 @@ function collectGradialImageFieldsFromSchema(schema, prefix, out) {
|
|
|
270
312
|
|
|
271
313
|
for (const [field, fieldSchema] of Object.entries(shape)) {
|
|
272
314
|
const nextPrefix = prefix ? `${prefix}.${field}` : field;
|
|
273
|
-
|
|
315
|
+
collectImageFieldsFromSchema(fieldSchema, nextPrefix, out);
|
|
274
316
|
}
|
|
275
317
|
}
|
|
276
318
|
|
|
@@ -301,21 +343,27 @@ function arrayItemSchema(schema) {
|
|
|
301
343
|
return null;
|
|
302
344
|
}
|
|
303
345
|
|
|
304
|
-
function
|
|
346
|
+
function isImageSchema(schema) {
|
|
305
347
|
const unwrapped = unwrapSchema(schema);
|
|
306
|
-
if (unwrapped ===
|
|
348
|
+
if (unwrapped === ImageSchema) return true;
|
|
307
349
|
try {
|
|
308
350
|
const compiled = z.toJSONSchema(unwrapped, {
|
|
309
351
|
target: 'draft-2020-12',
|
|
310
352
|
unrepresentable: 'throw',
|
|
311
353
|
});
|
|
312
|
-
const
|
|
313
|
-
const
|
|
314
|
-
|| (Array.isArray(sources?.anyOf) && sources.anyOf.some((s) => s?.type === 'array'));
|
|
354
|
+
const properties = compiled?.properties ?? {};
|
|
355
|
+
const required = new Set(compiled?.required ?? []);
|
|
315
356
|
return compiled?.type === 'object'
|
|
316
|
-
&&
|
|
317
|
-
&&
|
|
318
|
-
&&
|
|
357
|
+
&& properties.$type?.const === 'gradial.image'
|
|
358
|
+
&& properties.assetId?.type === 'string'
|
|
359
|
+
&& properties.versionId?.type === 'string'
|
|
360
|
+
&& properties.alt?.type === 'string'
|
|
361
|
+
&& properties.sources?.type === 'array'
|
|
362
|
+
&& required.has('$type')
|
|
363
|
+
&& required.has('assetId')
|
|
364
|
+
&& required.has('versionId')
|
|
365
|
+
&& required.has('alt')
|
|
366
|
+
&& required.has('sources');
|
|
319
367
|
} catch {
|
|
320
368
|
return false;
|
|
321
369
|
}
|
|
@@ -326,7 +374,7 @@ function unwrapSchema(schema) {
|
|
|
326
374
|
const seen = new Set();
|
|
327
375
|
while (current && !seen.has(current)) {
|
|
328
376
|
seen.add(current);
|
|
329
|
-
if (current ===
|
|
377
|
+
if (current === ImageSchema) return current;
|
|
330
378
|
const def = current._def;
|
|
331
379
|
const next = def?.innerType ?? def?.schema;
|
|
332
380
|
if (!next) break;
|
|
@@ -349,7 +397,11 @@ function validateLayout(layout, index, seen) {
|
|
|
349
397
|
seenSlots.add(slot.name);
|
|
350
398
|
slots.push({ name: slot.name, required: Boolean(slot.required) });
|
|
351
399
|
}
|
|
352
|
-
|
|
400
|
+
const entry = { name: layout.name, slots };
|
|
401
|
+
if (layout.defaults && typeof layout.defaults === 'object') {
|
|
402
|
+
entry.defaults = layout.defaults;
|
|
403
|
+
}
|
|
404
|
+
return entry;
|
|
353
405
|
}
|
|
354
406
|
|
|
355
407
|
function compileSchema(schema, name) {
|
|
@@ -368,6 +420,131 @@ function compileSchema(schema, name) {
|
|
|
368
420
|
};
|
|
369
421
|
}
|
|
370
422
|
|
|
423
|
+
async function generatePageSchema(schemaDir, componentEntries, outDir) {
|
|
424
|
+
const { readFile } = await import('node:fs/promises');
|
|
425
|
+
const defs = {
|
|
426
|
+
baseBlock: {
|
|
427
|
+
type: 'object',
|
|
428
|
+
required: ['id', 'component', 'props'],
|
|
429
|
+
additionalProperties: true,
|
|
430
|
+
properties: {
|
|
431
|
+
id: { type: 'string', minLength: 1 },
|
|
432
|
+
component: { type: 'string' },
|
|
433
|
+
props: { type: 'object' },
|
|
434
|
+
slots: {
|
|
435
|
+
type: 'object',
|
|
436
|
+
additionalProperties: {
|
|
437
|
+
type: 'array',
|
|
438
|
+
items: { $ref: '#/$defs/baseBlock' },
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
damAssetRef: {
|
|
444
|
+
type: 'object',
|
|
445
|
+
required: ['$type', 'assetId'],
|
|
446
|
+
additionalProperties: true,
|
|
447
|
+
properties: {
|
|
448
|
+
$type: { const: 'dam.assetRef' },
|
|
449
|
+
assetId: { type: 'string', minLength: 1 },
|
|
450
|
+
version: { type: 'string', minLength: 1 },
|
|
451
|
+
alt: { type: 'string' },
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
const oneOf = [];
|
|
456
|
+
for (const entry of componentEntries) {
|
|
457
|
+
const schemaPath = path.join(outDir, entry.props);
|
|
458
|
+
const raw = await readFile(schemaPath, 'utf-8');
|
|
459
|
+
const componentSchema = JSON.parse(raw);
|
|
460
|
+
const { $schema: _s, title: _t, ...propsSchema } = componentSchema;
|
|
461
|
+
replaceImagesWithDamAssetRef(propsSchema, defs.damAssetRef);
|
|
462
|
+
const defName = `${entry.name}Block`;
|
|
463
|
+
defs[defName] = {
|
|
464
|
+
allOf: [
|
|
465
|
+
{ $ref: '#/$defs/baseBlock' },
|
|
466
|
+
{
|
|
467
|
+
properties: {
|
|
468
|
+
component: { const: entry.name },
|
|
469
|
+
props: propsSchema,
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
],
|
|
473
|
+
};
|
|
474
|
+
oneOf.push({ $ref: `#/$defs/${defName}` });
|
|
475
|
+
}
|
|
476
|
+
const pageSchema = {
|
|
477
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
478
|
+
$id: 'https://baremetal.local/schemas/page.schema.json',
|
|
479
|
+
title: 'bare-metal Page',
|
|
480
|
+
type: 'object',
|
|
481
|
+
required: ['$type', 'id', 'status', 'layout', 'metadata', 'regions'],
|
|
482
|
+
additionalProperties: true,
|
|
483
|
+
properties: {
|
|
484
|
+
$type: { const: 'page' },
|
|
485
|
+
id: { type: 'string', minLength: 1 },
|
|
486
|
+
status: { type: 'string', minLength: 1 },
|
|
487
|
+
layout: { type: 'string', minLength: 1 },
|
|
488
|
+
metadata: {
|
|
489
|
+
type: 'object',
|
|
490
|
+
required: ['title'],
|
|
491
|
+
additionalProperties: true,
|
|
492
|
+
properties: {
|
|
493
|
+
title: { type: 'string', minLength: 1 },
|
|
494
|
+
description: { type: 'string' },
|
|
495
|
+
canonical: { type: 'string' },
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
regions: {
|
|
499
|
+
type: 'object',
|
|
500
|
+
additionalProperties: {
|
|
501
|
+
type: 'array',
|
|
502
|
+
items: { oneOf },
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
$defs: defs,
|
|
507
|
+
};
|
|
508
|
+
const pageSchemaPath = path.join(schemaDir, 'page.schema.json');
|
|
509
|
+
await writeFile(pageSchemaPath, JSON.stringify(pageSchema, null, 2) + '\n');
|
|
510
|
+
return pageSchemaPath;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function replaceImagesWithDamAssetRef(schema, damAssetRef) {
|
|
514
|
+
if (!schema || typeof schema !== 'object') return schema;
|
|
515
|
+
if (schema.$ref) return;
|
|
516
|
+
if (Array.isArray(schema.anyOf)) {
|
|
517
|
+
for (const s of schema.anyOf) replaceImagesWithDamAssetRef(s, damAssetRef);
|
|
518
|
+
}
|
|
519
|
+
if (Array.isArray(schema.allOf)) {
|
|
520
|
+
for (const s of schema.allOf) replaceImagesWithDamAssetRef(s, damAssetRef);
|
|
521
|
+
}
|
|
522
|
+
if (Array.isArray(schema.oneOf)) {
|
|
523
|
+
for (const s of schema.oneOf) replaceImagesWithDamAssetRef(s, damAssetRef);
|
|
524
|
+
}
|
|
525
|
+
if (schema.properties) {
|
|
526
|
+
for (const [key, value] of Object.entries(schema.properties)) {
|
|
527
|
+
if (isJsonImage(value)) {
|
|
528
|
+
schema.properties[key] = { $ref: '#/$defs/damAssetRef' };
|
|
529
|
+
} else {
|
|
530
|
+
replaceImagesWithDamAssetRef(value, damAssetRef);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (schema.items) {
|
|
535
|
+
if (isJsonImage(schema.items)) {
|
|
536
|
+
schema.items = { $ref: '#/$defs/damAssetRef' };
|
|
537
|
+
} else {
|
|
538
|
+
replaceImagesWithDamAssetRef(schema.items, damAssetRef);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function isJsonImage(node) {
|
|
544
|
+
return node?.type === 'object'
|
|
545
|
+
&& node?.properties?.$type?.const === 'gradial.image';
|
|
546
|
+
}
|
|
547
|
+
|
|
371
548
|
function validateZodSubset(schema, pathLabel, seen = new Set()) {
|
|
372
549
|
if (seen.has(schema)) return;
|
|
373
550
|
seen.add(schema);
|
|
@@ -53,20 +53,17 @@ 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? }
|
|
57
56
|
const schemaMap = new Map();
|
|
58
57
|
const knownComponents = new Set();
|
|
59
|
-
const
|
|
58
|
+
const componentMap = new Map();
|
|
60
59
|
for (const component of registry.components) {
|
|
61
60
|
knownComponents.add(component.name);
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
componentMap.set(component.name, component);
|
|
62
|
+
if (component.props) {
|
|
63
|
+
const schemaPath = path.join(registryDir, component.props);
|
|
64
64
|
const schemaRaw = await readFile(schemaPath, 'utf-8');
|
|
65
65
|
schemaMap.set(component.name, JSON.parse(schemaRaw));
|
|
66
66
|
}
|
|
67
|
-
if (Array.isArray(component.nestedBlocks) && component.nestedBlocks.length > 0) {
|
|
68
|
-
nestedBlocksMap.set(component.name, component.nestedBlocks);
|
|
69
|
-
}
|
|
70
67
|
}
|
|
71
68
|
|
|
72
69
|
// Find all compiled route files
|
|
@@ -134,10 +131,7 @@ async function validateContent(options) {
|
|
|
134
131
|
});
|
|
135
132
|
}
|
|
136
133
|
|
|
137
|
-
|
|
138
|
-
const nestedIssues = validateNestedBlocks(
|
|
139
|
-
block.component, props, knownComponents, schemaMap, nestedBlocksMap,
|
|
140
|
-
);
|
|
134
|
+
const nestedIssues = validateNestedSlots(block, componentMap, schemaMap);
|
|
141
135
|
for (const issue of nestedIssues) {
|
|
142
136
|
collector.push({
|
|
143
137
|
page: relativePath,
|
|
@@ -360,83 +354,52 @@ function resolveRef(ref, rootSchema) {
|
|
|
360
354
|
// ---------------------------------------------------------------------------
|
|
361
355
|
|
|
362
356
|
// ---------------------------------------------------------------------------
|
|
363
|
-
//
|
|
357
|
+
// Block ref validation — recursively validates nested blocks inside props.
|
|
364
358
|
// ---------------------------------------------------------------------------
|
|
365
359
|
|
|
366
|
-
function
|
|
360
|
+
function validateNestedSlots(parentBlock, componentMap, schemaMap, path = 'props') {
|
|
367
361
|
const issues = [];
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
362
|
+
const parentComponent = componentMap.get(parentBlock.component);
|
|
363
|
+
if (!parentComponent) return issues;
|
|
364
|
+
|
|
365
|
+
const blockSlots = parentComponent.slots ?? {};
|
|
366
|
+
for (const [slotName, slot] of Object.entries(blockSlots)) {
|
|
367
|
+
const blocks = parentBlock.props?.[slotName];
|
|
368
|
+
if (!Array.isArray(blocks)) {
|
|
369
|
+
if (slot.required) {
|
|
370
|
+
issues.push({ path: `${path}.${slotName}`, message: `${parentBlock.component}: required block ref "${slotName}" has no children` });
|
|
371
|
+
}
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (!slot.multiple && blocks.length > 1) {
|
|
375
|
+
issues.push({ path: `${path}.${slotName}`, message: `${parentBlock.component}: block ref "${slotName}" allows at most one child` });
|
|
376
|
+
}
|
|
371
377
|
for (const [index, nestedBlock] of blocks.entries()) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
});
|
|
378
|
+
const nestedPath = `${path}.${slotName}[${index}]`;
|
|
379
|
+
if (!nestedBlock || typeof nestedBlock !== 'object' || typeof nestedBlock.component !== 'string') {
|
|
380
|
+
issues.push({ path: nestedPath, message: `${parentBlock.component}: block ref child must be a block` });
|
|
384
381
|
continue;
|
|
385
382
|
}
|
|
386
383
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
});
|
|
384
|
+
if (!componentMap.has(nestedBlock.component)) {
|
|
385
|
+
issues.push({ path: `${nestedPath}.component`, message: `${parentBlock.component}: unknown nested component "${nestedBlock.component}"` });
|
|
386
|
+
continue;
|
|
399
387
|
}
|
|
400
|
-
|
|
401
|
-
|
|
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 });
|
|
388
|
+
if (!slot.allowed.includes(nestedBlock.component)) {
|
|
389
|
+
issues.push({ path: `${nestedPath}.component`, message: `${parentBlock.component}: component "${nestedBlock.component}" is not allowed in block ref "${slotName}"` });
|
|
428
390
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
391
|
+
|
|
392
|
+
const childSchema = schemaMap.get(nestedBlock.component);
|
|
393
|
+
const childProps = nestedBlock.props ?? {};
|
|
394
|
+
if (childSchema) {
|
|
395
|
+
for (const issue of validateJsonSchema(childProps, childSchema, `${nestedPath}.props`)) {
|
|
396
|
+
issues.push({ nestedComponent: nestedBlock.component, path: issue.path, message: `${parentBlock.component} → ${nestedBlock.component}: ${issue.message}` });
|
|
433
397
|
}
|
|
434
398
|
}
|
|
435
|
-
|
|
436
|
-
results.push(...findNestedBlockArrays(value, currentPath));
|
|
399
|
+
issues.push(...validateNestedSlots(nestedBlock, componentMap, schemaMap, nestedPath));
|
|
437
400
|
}
|
|
438
401
|
}
|
|
439
|
-
return
|
|
402
|
+
return issues;
|
|
440
403
|
}
|
|
441
404
|
|
|
442
405
|
function collectBlocks(pageData) {
|