@gradial/aci 0.1.20-preview.3 → 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 -108
- 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 +12 -3
- package/dist/astro/index.js +20 -5
- package/dist/content/assets.d.ts +17 -0
- package/dist/content/assets.js +65 -0
- package/dist/content/index.d.ts +1 -0
- package/dist/content/index.js +1 -0
- package/dist/define-component.d.ts +1 -2
- package/dist/define-component.js +73 -5
- 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 -6
- package/dist/index.js +8 -9
- package/dist/next/asset-route.js +2 -2
- package/dist/next/config.d.ts +2 -2
- package/dist/next/config.js +5 -5
- 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 +2 -2
- package/dist/next/middleware.js +6 -6
- package/dist/next/page.d.ts +1 -1
- package/dist/next/page.js +14 -12
- package/dist/next/render.d.ts +3 -3
- package/dist/next/render.js +11 -9
- package/dist/next/server.d.ts +12 -12
- package/dist/next/server.js +1 -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 +2 -2
- package/dist/registry.js +3 -3
- package/dist/types/block-ref.d.ts +26 -0
- package/dist/types/block-ref.js +28 -0
- package/dist/types/component.d.ts +36 -32
- package/dist/types/config.d.ts +1 -1
- 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/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 +28 -76
- package/src/cli/compile-registry.mjs +120 -63
- 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/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 -183
- 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 -25
- package/dist/sveltekit/index.js +0 -66
- package/dist/sveltekit/preview.d.ts +0 -5
- package/dist/sveltekit/preview.js +0 -33
- package/src/types/component.ts +0 -49
- package/src/types/config.ts +0 -37
- 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 -47
- package/src/types/render-mode.ts +0 -10
- 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 varyDimensions = component.varyDimensions ?? component.vary;
|
|
56
57
|
const imageSlots = normalizeImageSlots(component.imageSlots);
|
|
57
|
-
const
|
|
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
|
-
} : { static: true, ssr: true, clientIsland: false },
|
|
66
|
-
renderModes: renderModes ? {
|
|
67
|
-
canStatic: Boolean(renderModes.canStatic ?? renderModes.static),
|
|
68
|
-
canSSR: Boolean(renderModes.canSSR ?? renderModes.ssr),
|
|
69
|
-
canClientIsland: Boolean(renderModes.canClientIsland ?? renderModes.clientIsland),
|
|
70
|
-
} : { canStatic: true, canSSR: true, canClientIsland: false },
|
|
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
|
|
|
@@ -148,29 +137,84 @@ function validateDataModel(dataModel, index, seen) {
|
|
|
148
137
|
|
|
149
138
|
function validateComponent(component, index, seen) {
|
|
150
139
|
if (!component || typeof component !== 'object') throw new Error(`components[${index}] must be an object`);
|
|
151
|
-
if (!component.
|
|
152
|
-
if (seen.has(component.
|
|
153
|
-
seen.add(component.
|
|
154
|
-
if (!component.
|
|
155
|
-
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`);
|
|
156
145
|
validateImageSlotSchemaMatch(component, index);
|
|
157
146
|
}
|
|
158
147
|
|
|
159
148
|
function validateImageSlotSchemaMatch(component, index) {
|
|
160
149
|
const imageSlots = normalizeImageSlots(component.imageSlots);
|
|
161
|
-
const imageFields =
|
|
150
|
+
const imageFields = collectImageFields(component.props);
|
|
162
151
|
for (const field of imageFields) {
|
|
163
152
|
if (!Object.prototype.hasOwnProperty.call(imageSlots, field)) {
|
|
164
|
-
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`);
|
|
165
154
|
}
|
|
166
155
|
}
|
|
167
156
|
for (const field of Object.keys(imageSlots)) {
|
|
168
157
|
if (!imageFields.has(field)) {
|
|
169
|
-
throw new Error(`components[${index}].
|
|
158
|
+
throw new Error(`components[${index}].props.${field} must use an image schema because imageSlots.${field} is declared`);
|
|
170
159
|
}
|
|
171
160
|
}
|
|
172
161
|
}
|
|
173
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; }
|
|
194
|
+
}
|
|
195
|
+
break;
|
|
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;
|
|
216
|
+
}
|
|
217
|
+
|
|
174
218
|
function normalizeImageSlots(imageSlots) {
|
|
175
219
|
if (imageSlots == null) return {};
|
|
176
220
|
if (typeof imageSlots !== 'object' || Array.isArray(imageSlots)) {
|
|
@@ -228,34 +272,34 @@ function normalizeSlotOutput(slotKey, output, outputIndex) {
|
|
|
228
272
|
};
|
|
229
273
|
}
|
|
230
274
|
|
|
231
|
-
function
|
|
275
|
+
function collectImageFields(schema) {
|
|
232
276
|
const out = new Set();
|
|
233
277
|
|
|
234
|
-
|
|
278
|
+
collectImageFieldsFromSchema(schema, '', out);
|
|
235
279
|
|
|
236
280
|
return out;
|
|
237
281
|
}
|
|
238
282
|
|
|
239
|
-
function
|
|
283
|
+
function collectImageFieldsFromSchema(schema, prefix, out) {
|
|
240
284
|
if (!schema) {
|
|
241
285
|
return;
|
|
242
286
|
}
|
|
243
287
|
|
|
244
|
-
if (prefix &&
|
|
288
|
+
if (prefix && isImageSchema(schema)) {
|
|
245
289
|
out.add(prefix);
|
|
246
290
|
return;
|
|
247
291
|
}
|
|
248
292
|
|
|
249
293
|
const itemSchema = arrayItemSchema(schema);
|
|
250
294
|
if (itemSchema) {
|
|
251
|
-
|
|
295
|
+
collectImageFieldsFromSchema(itemSchema, prefix, out);
|
|
252
296
|
return;
|
|
253
297
|
}
|
|
254
298
|
|
|
255
299
|
const unionOptions = unionOptionSchemas(schema);
|
|
256
300
|
if (unionOptions) {
|
|
257
301
|
for (const option of unionOptions) {
|
|
258
|
-
|
|
302
|
+
collectImageFieldsFromSchema(option, prefix, out);
|
|
259
303
|
}
|
|
260
304
|
return;
|
|
261
305
|
}
|
|
@@ -268,7 +312,7 @@ function collectGradialImageFieldsFromSchema(schema, prefix, out) {
|
|
|
268
312
|
|
|
269
313
|
for (const [field, fieldSchema] of Object.entries(shape)) {
|
|
270
314
|
const nextPrefix = prefix ? `${prefix}.${field}` : field;
|
|
271
|
-
|
|
315
|
+
collectImageFieldsFromSchema(fieldSchema, nextPrefix, out);
|
|
272
316
|
}
|
|
273
317
|
}
|
|
274
318
|
|
|
@@ -299,21 +343,27 @@ function arrayItemSchema(schema) {
|
|
|
299
343
|
return null;
|
|
300
344
|
}
|
|
301
345
|
|
|
302
|
-
function
|
|
346
|
+
function isImageSchema(schema) {
|
|
303
347
|
const unwrapped = unwrapSchema(schema);
|
|
304
|
-
if (unwrapped ===
|
|
348
|
+
if (unwrapped === ImageSchema) return true;
|
|
305
349
|
try {
|
|
306
350
|
const compiled = z.toJSONSchema(unwrapped, {
|
|
307
351
|
target: 'draft-2020-12',
|
|
308
352
|
unrepresentable: 'throw',
|
|
309
353
|
});
|
|
310
|
-
const
|
|
311
|
-
const
|
|
312
|
-
|| (Array.isArray(sources?.anyOf) && sources.anyOf.some((s) => s?.type === 'array'));
|
|
354
|
+
const properties = compiled?.properties ?? {};
|
|
355
|
+
const required = new Set(compiled?.required ?? []);
|
|
313
356
|
return compiled?.type === 'object'
|
|
314
|
-
&&
|
|
315
|
-
&&
|
|
316
|
-
&&
|
|
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');
|
|
317
367
|
} catch {
|
|
318
368
|
return false;
|
|
319
369
|
}
|
|
@@ -324,7 +374,7 @@ function unwrapSchema(schema) {
|
|
|
324
374
|
const seen = new Set();
|
|
325
375
|
while (current && !seen.has(current)) {
|
|
326
376
|
seen.add(current);
|
|
327
|
-
if (current ===
|
|
377
|
+
if (current === ImageSchema) return current;
|
|
328
378
|
const def = current._def;
|
|
329
379
|
const next = def?.innerType ?? def?.schema;
|
|
330
380
|
if (!next) break;
|
|
@@ -381,6 +431,13 @@ async function generatePageSchema(schemaDir, componentEntries, outDir) {
|
|
|
381
431
|
id: { type: 'string', minLength: 1 },
|
|
382
432
|
component: { type: 'string' },
|
|
383
433
|
props: { type: 'object' },
|
|
434
|
+
slots: {
|
|
435
|
+
type: 'object',
|
|
436
|
+
additionalProperties: {
|
|
437
|
+
type: 'array',
|
|
438
|
+
items: { $ref: '#/$defs/baseBlock' },
|
|
439
|
+
},
|
|
440
|
+
},
|
|
384
441
|
},
|
|
385
442
|
},
|
|
386
443
|
damAssetRef: {
|
|
@@ -397,11 +454,11 @@ async function generatePageSchema(schemaDir, componentEntries, outDir) {
|
|
|
397
454
|
};
|
|
398
455
|
const oneOf = [];
|
|
399
456
|
for (const entry of componentEntries) {
|
|
400
|
-
const schemaPath = path.join(outDir, entry.
|
|
457
|
+
const schemaPath = path.join(outDir, entry.props);
|
|
401
458
|
const raw = await readFile(schemaPath, 'utf-8');
|
|
402
459
|
const componentSchema = JSON.parse(raw);
|
|
403
460
|
const { $schema: _s, title: _t, ...propsSchema } = componentSchema;
|
|
404
|
-
|
|
461
|
+
replaceImagesWithDamAssetRef(propsSchema, defs.damAssetRef);
|
|
405
462
|
const defName = `${entry.name}Block`;
|
|
406
463
|
defs[defName] = {
|
|
407
464
|
allOf: [
|
|
@@ -453,37 +510,37 @@ async function generatePageSchema(schemaDir, componentEntries, outDir) {
|
|
|
453
510
|
return pageSchemaPath;
|
|
454
511
|
}
|
|
455
512
|
|
|
456
|
-
function
|
|
513
|
+
function replaceImagesWithDamAssetRef(schema, damAssetRef) {
|
|
457
514
|
if (!schema || typeof schema !== 'object') return schema;
|
|
458
515
|
if (schema.$ref) return;
|
|
459
516
|
if (Array.isArray(schema.anyOf)) {
|
|
460
|
-
for (const s of schema.anyOf)
|
|
517
|
+
for (const s of schema.anyOf) replaceImagesWithDamAssetRef(s, damAssetRef);
|
|
461
518
|
}
|
|
462
519
|
if (Array.isArray(schema.allOf)) {
|
|
463
|
-
for (const s of schema.allOf)
|
|
520
|
+
for (const s of schema.allOf) replaceImagesWithDamAssetRef(s, damAssetRef);
|
|
464
521
|
}
|
|
465
522
|
if (Array.isArray(schema.oneOf)) {
|
|
466
|
-
for (const s of schema.oneOf)
|
|
523
|
+
for (const s of schema.oneOf) replaceImagesWithDamAssetRef(s, damAssetRef);
|
|
467
524
|
}
|
|
468
525
|
if (schema.properties) {
|
|
469
526
|
for (const [key, value] of Object.entries(schema.properties)) {
|
|
470
|
-
if (
|
|
527
|
+
if (isJsonImage(value)) {
|
|
471
528
|
schema.properties[key] = { $ref: '#/$defs/damAssetRef' };
|
|
472
529
|
} else {
|
|
473
|
-
|
|
530
|
+
replaceImagesWithDamAssetRef(value, damAssetRef);
|
|
474
531
|
}
|
|
475
532
|
}
|
|
476
533
|
}
|
|
477
534
|
if (schema.items) {
|
|
478
|
-
if (
|
|
535
|
+
if (isJsonImage(schema.items)) {
|
|
479
536
|
schema.items = { $ref: '#/$defs/damAssetRef' };
|
|
480
537
|
} else {
|
|
481
|
-
|
|
538
|
+
replaceImagesWithDamAssetRef(schema.items, damAssetRef);
|
|
482
539
|
}
|
|
483
540
|
}
|
|
484
541
|
}
|
|
485
542
|
|
|
486
|
-
function
|
|
543
|
+
function isJsonImage(node) {
|
|
487
544
|
return node?.type === 'object'
|
|
488
545
|
&& node?.properties?.$type?.const === 'gradial.image';
|
|
489
546
|
}
|
|
@@ -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) {
|
package/src/types/image.ts
CHANGED
|
@@ -1,28 +1,47 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Image source variant (responsive derivative)
|
|
5
|
+
//
|
|
6
|
+
// Each entry is one concrete derivative produced by the compiler for the asset
|
|
7
|
+
// slot. Renderers pick the primary variant and build a srcset from the list.
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
3
10
|
export interface ImageSource {
|
|
11
|
+
/** Derivative URL. */
|
|
4
12
|
src: string;
|
|
13
|
+
/** Intrinsic width in pixels. */
|
|
5
14
|
width: number;
|
|
15
|
+
/** Intrinsic height in pixels. */
|
|
6
16
|
height: number;
|
|
17
|
+
/** MIME type hint, e.g. image/webp. */
|
|
7
18
|
type: string;
|
|
8
19
|
}
|
|
9
20
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Image — DAM-backed image asset with compiler-resolved derivatives
|
|
23
|
+
//
|
|
24
|
+
// Content authors write an asset reference: { "$type": "dam.assetRef", ... }
|
|
25
|
+
// The compiler resolves it to this Image value based on the asset's media type.
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
16
27
|
|
|
17
|
-
export interface
|
|
28
|
+
export interface Image {
|
|
29
|
+
/** Discriminator — always 'gradial.image'. */
|
|
18
30
|
$type: 'gradial.image';
|
|
31
|
+
/** DAM asset identifier. */
|
|
19
32
|
assetId: string;
|
|
33
|
+
/** DAM version identifier. */
|
|
20
34
|
versionId: string;
|
|
35
|
+
/** Accessible description of the image content. */
|
|
21
36
|
alt: string;
|
|
22
|
-
|
|
23
|
-
sources:
|
|
37
|
+
/** Responsive derivative variants produced for the asset slot. */
|
|
38
|
+
sources: ImageSource[];
|
|
24
39
|
}
|
|
25
40
|
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Image slot contract (declared by components that accept image props)
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
26
45
|
export interface ImageSlotContract {
|
|
27
46
|
outputs: SlotOutput[];
|
|
28
47
|
formats: string[];
|
|
@@ -35,66 +54,66 @@ export interface SlotOutput {
|
|
|
35
54
|
media?: string;
|
|
36
55
|
}
|
|
37
56
|
|
|
38
|
-
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Zod schema
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
export const ImageSourceSchema = z.object({
|
|
62
|
+
src: z.string().min(1),
|
|
63
|
+
width: z.number().int().nonnegative(),
|
|
64
|
+
height: z.number().int().nonnegative(),
|
|
65
|
+
type: z.string().min(1),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export const ImageSchema = z.object({
|
|
39
69
|
$type: z.literal('gradial.image'),
|
|
40
70
|
assetId: z.string().min(1),
|
|
41
71
|
versionId: z.string().min(1),
|
|
42
72
|
alt: z.string(),
|
|
43
|
-
|
|
44
|
-
src: z.string().min(1),
|
|
45
|
-
width: z.number().int().nonnegative(),
|
|
46
|
-
height: z.number().int().nonnegative(),
|
|
47
|
-
type: z.string().min(1),
|
|
48
|
-
}),
|
|
49
|
-
sources: z.array(z.object({
|
|
50
|
-
media: z.string().optional(),
|
|
51
|
-
type: z.string().min(1),
|
|
52
|
-
srcset: z.string().min(1),
|
|
53
|
-
sizes: z.string().optional(),
|
|
54
|
-
})).nullable().default([]),
|
|
73
|
+
sources: z.array(ImageSourceSchema).min(1),
|
|
55
74
|
});
|
|
56
75
|
|
|
57
|
-
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Shared attrs helper — consumed by React, Astro, and Svelte components
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
58
79
|
|
|
59
|
-
export
|
|
60
|
-
const imgAttrs = renderAttrs({
|
|
61
|
-
...attrs,
|
|
62
|
-
src: image.fallback.src,
|
|
63
|
-
alt: image.alt,
|
|
64
|
-
width: image.fallback.width > 0 ? image.fallback.width : undefined,
|
|
65
|
-
height: image.fallback.height > 0 ? image.fallback.height : undefined,
|
|
66
|
-
});
|
|
67
|
-
const img = `<img${imgAttrs}>`;
|
|
68
|
-
if (!image.sources?.length) {
|
|
69
|
-
return img;
|
|
70
|
-
}
|
|
71
|
-
const sources = image.sources.map((source) => `<source${renderAttrs({
|
|
72
|
-
media: source.media,
|
|
73
|
-
type: source.type,
|
|
74
|
-
srcset: source.srcset,
|
|
75
|
-
sizes: source.sizes,
|
|
76
|
-
})}>`).join('');
|
|
77
|
-
return `<picture>${sources}${img}</picture>`;
|
|
78
|
-
}
|
|
80
|
+
export type ImageAttrs = Record<string, unknown>;
|
|
79
81
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Picks the primary variant from an image's sources.
|
|
84
|
+
* Uses the largest width; ties fall back to the first entry.
|
|
85
|
+
*/
|
|
86
|
+
export function primaryImageSource(image: Image): ImageSource {
|
|
87
|
+
return image.sources.reduce((largest, source) =>
|
|
88
|
+
source.width > largest.width ? source : largest,
|
|
89
|
+
image.sources[0],
|
|
90
|
+
);
|
|
88
91
|
}
|
|
89
92
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Computes the full set of <img> attributes for an Image.
|
|
95
|
+
*
|
|
96
|
+
* Framework components (React <Picture>, Astro <img>, Svelte <img>) call this
|
|
97
|
+
* and spread the result into their native element. No HTML string serialization.
|
|
98
|
+
*/
|
|
99
|
+
export function imageAttrs(image: Image, overrides: ImageAttrs = {}): ImageAttrs {
|
|
100
|
+
const primary = primaryImageSource(image);
|
|
101
|
+
const srcset = image.sources
|
|
102
|
+
.map((s) => `${s.src} ${s.width}w`)
|
|
103
|
+
.join(', ');
|
|
104
|
+
const sizes = image.sources
|
|
105
|
+
.map((s) => `(max-width: ${s.width}px) ${s.width}px`)
|
|
106
|
+
.join(', ');
|
|
93
107
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
.
|
|
97
|
-
.
|
|
98
|
-
.
|
|
99
|
-
|
|
108
|
+
return {
|
|
109
|
+
src: primary.src,
|
|
110
|
+
alt: image.alt,
|
|
111
|
+
width: primary.width > 0 ? primary.width : undefined,
|
|
112
|
+
height: primary.height > 0 ? primary.height : undefined,
|
|
113
|
+
srcSet: srcset || undefined,
|
|
114
|
+
sizes: sizes || undefined,
|
|
115
|
+
loading: 'lazy',
|
|
116
|
+
decoding: 'async',
|
|
117
|
+
...overrides,
|
|
118
|
+
};
|
|
100
119
|
}
|