@gradial/aci 0.1.0 → 0.1.1

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 (50) hide show
  1. package/README.md +1 -1
  2. package/bin/aci.js +45 -0
  3. package/dist/assets/index.d.ts +1 -0
  4. package/dist/assets/index.js +1 -0
  5. package/dist/astro/index.d.ts +24 -2
  6. package/dist/astro/index.js +42 -4
  7. package/dist/content/index.d.ts +0 -3
  8. package/dist/content/index.js +0 -3
  9. package/dist/content/provider.d.ts +32 -8
  10. package/dist/content/provider.js +26 -16
  11. package/dist/content/routes.d.ts +6 -12
  12. package/dist/content/routes.js +9 -55
  13. package/dist/content/validation.js +1 -1
  14. package/dist/define-layout.js +5 -1
  15. package/dist/dev/browser.d.ts +1 -1
  16. package/dist/dev/browser.js +1 -1
  17. package/dist/dev/index.d.ts +3 -3
  18. package/dist/dev/index.js +8 -8
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.js +1 -0
  21. package/dist/next/config.d.ts +14 -0
  22. package/dist/next/config.js +22 -0
  23. package/dist/next/dev-refresh.js +4 -4
  24. package/dist/next/edge-config.d.ts +1 -0
  25. package/dist/next/edge-config.js +92 -0
  26. package/dist/next/index.d.ts +2 -0
  27. package/dist/next/index.js +2 -0
  28. package/dist/next/middleware.js +4 -6
  29. package/dist/next/server.d.ts +5 -24
  30. package/dist/next/server.js +47 -152
  31. package/dist/providers/file.d.ts +11 -17
  32. package/dist/providers/file.js +44 -78
  33. package/dist/providers/s3.d.ts +24 -0
  34. package/dist/providers/s3.js +162 -0
  35. package/dist/sveltekit/index.d.ts +18 -2
  36. package/dist/sveltekit/index.js +35 -4
  37. package/dist/testing/index.d.ts +14 -12
  38. package/dist/testing/index.js +41 -28
  39. package/dist/types/component.d.ts +19 -2
  40. package/dist/types/config.d.ts +4 -0
  41. package/dist/types/image.d.ts +51 -0
  42. package/dist/types/image.js +58 -0
  43. package/dist/types/index.d.ts +1 -0
  44. package/dist/types/index.js +1 -0
  45. package/dist/types/layout.d.ts +12 -0
  46. package/package.json +26 -2
  47. package/src/cli/compile-registry.mjs +162 -0
  48. package/src/cli/css-stub-loader.mjs +27 -0
  49. package/src/cli/generate-registry.mjs +72 -0
  50. package/src/cli/validate-content.mjs +391 -0
@@ -0,0 +1,162 @@
1
+ import crypto from 'node:crypto';
2
+ import { FragmentNotFoundError, PageNotFoundError } from '../content/provider.js';
3
+ import { normalizeRoute } from '../content/routes.js';
4
+ // ---------------------------------------------------------------------------
5
+ // S3 content provider — reads compiled manifest + payloads from S3
6
+ // ---------------------------------------------------------------------------
7
+ export class S3ContentProvider {
8
+ config;
9
+ #manifest;
10
+ constructor(config = {}) {
11
+ this.config = resolveS3Config(config);
12
+ }
13
+ async listRoutes() {
14
+ const manifest = await this.manifest();
15
+ return Object.values(manifest.routes).sort((a, b) => a.path.localeCompare(b.path));
16
+ }
17
+ async getSiteConfig() {
18
+ const manifest = await this.manifest();
19
+ return await this.getJSON(manifest.siteConfigRef);
20
+ }
21
+ async getPage(route) {
22
+ const manifest = await this.manifest();
23
+ const normalized = normalizeRoute(route);
24
+ const entry = Object.values(manifest.routes).find((candidate) => normalizeRoute(candidate.path) === normalized);
25
+ if (!entry) {
26
+ throw new PageNotFoundError(route);
27
+ }
28
+ try {
29
+ return await this.getJSON(entry.payloadRef);
30
+ }
31
+ catch (error) {
32
+ throw new PageNotFoundError(route, error);
33
+ }
34
+ }
35
+ async getFragment(id) {
36
+ const manifest = await this.manifest();
37
+ const entry = manifest.fragments[id];
38
+ if (!entry) {
39
+ throw new FragmentNotFoundError(id);
40
+ }
41
+ try {
42
+ return await this.getJSON(entry.payloadRef);
43
+ }
44
+ catch (error) {
45
+ throw new FragmentNotFoundError(id, error);
46
+ }
47
+ }
48
+ async manifest() {
49
+ this.#manifest ??= this.getJSON('manifest.json');
50
+ return await this.#manifest;
51
+ }
52
+ async getJSON(key) {
53
+ return await getS3JSON(joinKey(this.config.keyPrefix, key), this.config);
54
+ }
55
+ }
56
+ // ---------------------------------------------------------------------------
57
+ // S3 fetch with AWS SigV4 signing
58
+ // ---------------------------------------------------------------------------
59
+ export async function getS3JSON(key, config) {
60
+ const url = s3ObjectURL(config, key);
61
+ const signedHeaders = signedS3Headers('GET', url, config);
62
+ const res = await fetch(url, {
63
+ method: 'GET',
64
+ headers: signedHeaders,
65
+ cache: 'no-store'
66
+ });
67
+ if (!res.ok) {
68
+ throw new Error(`S3 GET ${key} failed with status ${res.status}: ${await res.text()}`);
69
+ }
70
+ return (await res.json());
71
+ }
72
+ // ---------------------------------------------------------------------------
73
+ // Internal helpers
74
+ // ---------------------------------------------------------------------------
75
+ function resolveS3Config(config) {
76
+ const resolved = {
77
+ bucket: config.bucket || process.env.ACI_S3_BUCKET || '',
78
+ keyPrefix: config.keyPrefix || process.env.ACI_S3_KEY_PREFIX || '',
79
+ region: config.region || process.env.AWS_REGION || 'us-east-1',
80
+ accessKeyId: config.accessKeyId || process.env.AWS_ACCESS_KEY_ID || '',
81
+ secretAccessKey: config.secretAccessKey || process.env.AWS_SECRET_ACCESS_KEY || '',
82
+ sessionToken: config.sessionToken || process.env.AWS_SESSION_TOKEN || '',
83
+ endpoint: trimSlash(config.endpoint || process.env.ACI_S3_ENDPOINT || '')
84
+ };
85
+ for (const key of ['bucket', 'accessKeyId', 'secretAccessKey']) {
86
+ if (!resolved[key])
87
+ throw new Error(`S3 config missing ${key}`);
88
+ }
89
+ return resolved;
90
+ }
91
+ function s3ObjectURL(config, key) {
92
+ const encodedKey = encodeKey(key);
93
+ if (config.endpoint) {
94
+ return new URL(`${config.endpoint}/${encodeURIComponent(config.bucket)}${encodedKey}`);
95
+ }
96
+ return new URL(`https://${config.bucket}.s3.${config.region}.amazonaws.com${encodedKey}`);
97
+ }
98
+ function signedS3Headers(method, url, config) {
99
+ const now = new Date();
100
+ const amzDate = amzTimestamp(now);
101
+ const dateStamp = amzDate.slice(0, 8);
102
+ const payloadHash = sha256Hex('');
103
+ const headers = new Headers({
104
+ host: url.host,
105
+ 'x-amz-content-sha256': payloadHash,
106
+ 'x-amz-date': amzDate
107
+ });
108
+ if (config.sessionToken)
109
+ headers.set('x-amz-security-token', config.sessionToken);
110
+ const signedHeaderNames = Array.from(headers.keys()).sort();
111
+ const canonicalHeaders = signedHeaderNames.map((k) => `${k}:${headers.get(k)?.trim()}\n`).join('');
112
+ const canonicalRequest = [
113
+ method,
114
+ url.pathname,
115
+ url.searchParams.toString(),
116
+ canonicalHeaders,
117
+ signedHeaderNames.join(';'),
118
+ payloadHash
119
+ ].join('\n');
120
+ const credentialScope = `${dateStamp}/${config.region}/s3/aws4_request`;
121
+ const stringToSign = [
122
+ 'AWS4-HMAC-SHA256',
123
+ amzDate,
124
+ credentialScope,
125
+ sha256Hex(canonicalRequest)
126
+ ].join('\n');
127
+ const signingKey = awsSigningKey(config.secretAccessKey, dateStamp, config.region, 's3');
128
+ const signature = hmacHex(signingKey, stringToSign);
129
+ headers.set('authorization', `AWS4-HMAC-SHA256 Credential=${config.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaderNames.join(';')}, Signature=${signature}`);
130
+ return headers;
131
+ }
132
+ function awsSigningKey(secret, dateStamp, region, service) {
133
+ const kDate = hmac(Buffer.from(`AWS4${secret}`, 'utf8'), dateStamp);
134
+ const kRegion = hmac(kDate, region);
135
+ const kService = hmac(kRegion, service);
136
+ return hmac(kService, 'aws4_request');
137
+ }
138
+ function amzTimestamp(date) {
139
+ return date.toISOString().replace(/[:-]|\.\d{3}/g, '');
140
+ }
141
+ function hmac(key, value) {
142
+ return crypto.createHmac('sha256', key).update(value, 'utf8').digest();
143
+ }
144
+ function hmacHex(key, value) {
145
+ return crypto.createHmac('sha256', key).update(value, 'utf8').digest('hex');
146
+ }
147
+ function sha256Hex(value) {
148
+ return crypto.createHash('sha256').update(value, 'utf8').digest('hex');
149
+ }
150
+ function trimSlash(value) {
151
+ return value.replace(/\/+$/, '');
152
+ }
153
+ function encodeKey(key) {
154
+ return `/${key.split('/').map(encodeURIComponent).join('/')}`;
155
+ }
156
+ function joinKey(...parts) {
157
+ return parts
158
+ .flatMap((part) => part.split('/'))
159
+ .map((part) => part.trim())
160
+ .filter(Boolean)
161
+ .join('/');
162
+ }
@@ -1,2 +1,18 @@
1
- import { type BareMetalContentWatchOptions, type VitePluginLike } from '../dev/index.js';
2
- export declare function baremetalSvelteKit(options?: BareMetalContentWatchOptions): VitePluginLike;
1
+ import { type GradialContentWatchOptions, type VitePluginLike } from '../dev/index.js';
2
+ /**
3
+ * Generates prerender entries from the compiled content manifest for SvelteKit.
4
+ *
5
+ * Usage in svelte.config.js:
6
+ * ```js
7
+ * import { gradialEntries } from '@gradial/aci/sveltekit';
8
+ * export default { kit: { prerender: { entries: await gradialEntries() } } };
9
+ * ```
10
+ *
11
+ * Or in a catch-all `+page.server.ts`:
12
+ * ```ts
13
+ * import { gradialEntries } from '@gradial/aci/sveltekit';
14
+ * export const entries = gradialEntries;
15
+ * ```
16
+ */
17
+ export declare function gradialEntries(): Promise<string[]>;
18
+ export declare function gradialSvelteKit(options?: GradialContentWatchOptions): VitePluginLike;
@@ -1,9 +1,40 @@
1
- import { baremetalContentWatchPlugin, devRefreshPort, devRefreshScriptTag, } from '../dev/index.js';
2
- export function baremetalSvelteKit(options = {}) {
3
- const watchPlugin = baremetalContentWatchPlugin(options);
1
+ import { gradialContentWatchPlugin, devRefreshPort, devRefreshScriptTag, } from '../dev/index.js';
2
+ // ---------------------------------------------------------------------------
3
+ // Entries helper — for SvelteKit SSG prerendering
4
+ // ---------------------------------------------------------------------------
5
+ /**
6
+ * Generates prerender entries from the compiled content manifest for SvelteKit.
7
+ *
8
+ * Usage in svelte.config.js:
9
+ * ```js
10
+ * import { gradialEntries } from '@gradial/aci/sveltekit';
11
+ * export default { kit: { prerender: { entries: await gradialEntries() } } };
12
+ * ```
13
+ *
14
+ * Or in a catch-all `+page.server.ts`:
15
+ * ```ts
16
+ * import { gradialEntries } from '@gradial/aci/sveltekit';
17
+ * export const entries = gradialEntries;
18
+ * ```
19
+ */
20
+ export async function gradialEntries() {
21
+ const { FileContentProvider } = await import('../providers/file.js');
22
+ const { normalizeRoute } = await import('../content/routes.js');
23
+ const provider = new FileContentProvider();
24
+ const routes = await provider.listRoutes();
25
+ return routes.map((entry) => {
26
+ const normalized = normalizeRoute(entry.path);
27
+ return normalized === '/' ? '/' : `/${normalized.replace(/^\/|\/$/g, '')}/`;
28
+ });
29
+ }
30
+ // ---------------------------------------------------------------------------
31
+ // SvelteKit Vite plugin
32
+ // ---------------------------------------------------------------------------
33
+ export function gradialSvelteKit(options = {}) {
34
+ const watchPlugin = gradialContentWatchPlugin(options);
4
35
  return {
5
36
  ...watchPlugin,
6
- name: 'baremetal-sveltekit',
37
+ name: 'gradial-sveltekit',
7
38
  transformIndexHtml(html) {
8
39
  if (process.env.NODE_ENV === 'production') {
9
40
  return html;
@@ -1,20 +1,22 @@
1
- import { type ContentProvider, type RenderInputOptions } from '../content/provider.js';
1
+ import { type ContentProvider, type RouteEntry, type RenderInputOptions } from '../content/provider.js';
2
2
  import type { KernelPage, KernelRouteMetadata, KernelSiteConfig, RenderInput } from '../content/types.js';
3
- export interface FixtureContentProviderOptions<TPage extends KernelPage = KernelPage, TSiteConfig extends KernelSiteConfig = KernelSiteConfig> {
4
- siteConfig?: TSiteConfig;
5
- pages?: Record<string, TPage | null>;
3
+ export interface FixtureContentProviderOptions {
4
+ siteConfig?: KernelSiteConfig;
5
+ pages?: Record<string, KernelPage>;
6
+ fragments?: Record<string, unknown>;
6
7
  }
7
- export declare class FixtureContentProvider<TPage extends KernelPage = KernelPage, TSiteConfig extends KernelSiteConfig = KernelSiteConfig> implements ContentProvider<TPage, TSiteConfig> {
8
+ export declare class FixtureContentProvider implements ContentProvider {
8
9
  private siteConfig;
9
10
  private pages;
10
- constructor(options?: FixtureContentProviderOptions<TPage, TSiteConfig>);
11
- loadSiteConfig(): Promise<TSiteConfig>;
12
- loadPage(route?: string): Promise<TPage | null>;
13
- listRoutes(): Promise<string[]>;
14
- listPublishedRoutes(): Promise<string[]>;
11
+ private fragments;
12
+ constructor(options?: FixtureContentProviderOptions);
13
+ getSiteConfig<T = unknown>(): Promise<T>;
14
+ getPage<T = unknown>(route: string): Promise<T>;
15
+ getFragment<T = unknown>(id: string): Promise<T>;
16
+ listRoutes(): Promise<RouteEntry[]>;
17
+ loadRenderInput(route?: string, options?: RenderInputOptions): Promise<RenderInput>;
15
18
  resolveRouteMetadata(route?: string): Promise<KernelRouteMetadata>;
16
- loadRenderInput(route?: string, options?: RenderInputOptions): Promise<RenderInput<TPage, TSiteConfig>>;
17
19
  }
18
- export declare function createFixtureContentProvider<TPage extends KernelPage = KernelPage, TSiteConfig extends KernelSiteConfig = KernelSiteConfig>(options?: FixtureContentProviderOptions<TPage, TSiteConfig>): FixtureContentProvider<TPage, TSiteConfig>;
20
+ export declare function createFixtureContentProvider(options?: FixtureContentProviderOptions): FixtureContentProvider;
19
21
  export declare function defaultFixtureSiteConfig(): KernelSiteConfig;
20
22
  export declare function defaultFixturePage(): KernelPage;
@@ -1,51 +1,64 @@
1
- import { loadRenderInput, routeMetadataForContent, } from '../content/provider.js';
1
+ import { PageNotFoundError, FragmentNotFoundError, loadRenderInput, routeMetadataForContent, } from '../content/provider.js';
2
2
  import { normalizeRoute } from '../content/routes.js';
3
3
  export class FixtureContentProvider {
4
4
  siteConfig;
5
5
  pages;
6
+ fragments;
6
7
  constructor(options = {}) {
7
8
  this.siteConfig = cloneFixture(options.siteConfig ?? defaultFixtureSiteConfig());
8
9
  this.pages = new Map();
9
- const inputPages = options.pages ?? {
10
- '/': defaultFixturePage(),
11
- };
10
+ this.fragments = new Map();
11
+ const inputPages = options.pages ?? { '/': defaultFixturePage() };
12
12
  for (const [route, page] of Object.entries(inputPages)) {
13
- if (page) {
14
- this.pages.set(normalizeRoute(route), cloneFixture(page));
13
+ this.pages.set(normalizeRoute(route), cloneFixture(page));
14
+ }
15
+ if (options.fragments) {
16
+ for (const [id, fragment] of Object.entries(options.fragments)) {
17
+ this.fragments.set(id, cloneFixture(fragment));
15
18
  }
16
19
  }
17
20
  }
18
- async loadSiteConfig() {
21
+ async getSiteConfig() {
19
22
  return cloneFixture(this.siteConfig);
20
23
  }
21
- async loadPage(route = '/') {
24
+ async getPage(route) {
22
25
  const page = this.pages.get(normalizeRoute(route));
23
- return page ? cloneFixture(page) : null;
24
- }
25
- async listRoutes() {
26
- return [...this.pages.keys()].sort((left, right) => left.localeCompare(right));
26
+ if (!page) {
27
+ throw new PageNotFoundError(route);
28
+ }
29
+ return cloneFixture(page);
27
30
  }
28
- async listPublishedRoutes() {
29
- const routes = await this.listRoutes();
30
- const published = [];
31
- for (const route of routes) {
32
- const page = await this.loadPage(route);
33
- if (page?.status === 'published') {
34
- published.push(route);
35
- }
31
+ async getFragment(id) {
32
+ const fragment = this.fragments.get(id);
33
+ if (fragment === undefined) {
34
+ throw new FragmentNotFoundError(id);
36
35
  }
37
- return published;
36
+ return cloneFixture(fragment);
38
37
  }
39
- async resolveRouteMetadata(route = '/') {
40
- const [siteConfig, page] = await Promise.all([
41
- this.loadSiteConfig(),
42
- this.loadPage(route),
43
- ]);
44
- return routeMetadataForContent(siteConfig, page);
38
+ async listRoutes() {
39
+ return [...this.pages.entries()]
40
+ .sort(([a], [b]) => a.localeCompare(b))
41
+ .map(([routePath, page]) => ({
42
+ slug: routePath === '/' ? 'home' : routePath.replace(/^\//, ''),
43
+ path: routePath,
44
+ payloadRef: `routes/${routePath === '/' ? 'home' : routePath.replace(/^\//, '')}.json`,
45
+ digest: '',
46
+ }));
45
47
  }
46
48
  async loadRenderInput(route = '/', options = {}) {
47
49
  return loadRenderInput(this, route, options);
48
50
  }
51
+ async resolveRouteMetadata(route = '/') {
52
+ const siteConfig = await this.getSiteConfig();
53
+ let page;
54
+ try {
55
+ page = await this.getPage(route);
56
+ }
57
+ catch {
58
+ page = null;
59
+ }
60
+ return routeMetadataForContent(siteConfig, page);
61
+ }
49
62
  }
50
63
  export function createFixtureContentProvider(options = {}) {
51
64
  return new FixtureContentProvider(options);
@@ -56,7 +69,7 @@ export function defaultFixtureSiteConfig() {
56
69
  $type: 'site',
57
70
  status: 'published',
58
71
  title: 'Fixture Site',
59
- domain: 'www.baremetal.local',
72
+ domain: 'www.aci.local',
60
73
  defaultLocale: 'en-us',
61
74
  seo: {
62
75
  title: 'Fixture Site',
@@ -1,26 +1,43 @@
1
1
  import type { IslandMode, VaryDimension } from './render-mode.js';
2
+ import type { ImageSlotContract } from './image.js';
3
+ import type { z } from 'zod';
2
4
  export interface ComponentRenderModes {
3
5
  canStatic: boolean;
4
6
  canSSR: boolean;
5
7
  canClientIsland: boolean;
6
8
  }
9
+ /**
10
+ * A CMS-registered component — sync or async (server components).
11
+ * Accepts content props derived from the Zod schema.
12
+ */
13
+ type CmsComponentFn<TProps = any> = ((props: TProps) => import('react').ReactElement | null) | ((props: TProps) => Promise<import('react').ReactElement | null>);
14
+ /**
15
+ * Infers the props type from a schema.
16
+ * If TSchema is a Zod type, returns z.infer<TSchema>.
17
+ * Otherwise returns Record<string, unknown>.
18
+ */
19
+ type InferContentProps<TSchema> = TSchema extends z.ZodType<infer T> ? T : Record<string, unknown>;
7
20
  export interface ComponentDefinition<TSchema = unknown> {
8
21
  name: string;
9
- component?: unknown;
22
+ /** The React component that renders this CMS block. Typed against the schema. */
23
+ component?: CmsComponentFn<InferContentProps<TSchema>>;
10
24
  schema: TSchema;
11
25
  renderModes?: ComponentRenderModes;
12
26
  render?: ComponentRenderModes;
13
27
  defaultIslandMode?: IslandMode;
14
28
  varyDimensions?: VaryDimension[];
15
29
  vary?: VaryDimension[];
30
+ imageSlots?: Record<string, ImageSlotContract>;
16
31
  }
17
32
  export interface ComponentContract<TSchema = unknown> {
18
33
  name: string;
19
- component?: unknown;
34
+ component?: CmsComponentFn<InferContentProps<TSchema>>;
20
35
  schema: TSchema;
21
36
  renderModes: ComponentRenderModes;
22
37
  render: ComponentRenderModes;
23
38
  defaultIslandMode?: IslandMode;
24
39
  varyDimensions?: VaryDimension[];
25
40
  vary?: VaryDimension[];
41
+ imageSlots?: Record<string, ImageSlotContract>;
26
42
  }
43
+ export type { CmsComponentFn, InferContentProps };
@@ -12,6 +12,7 @@ export interface BareMetalConfig {
12
12
  rendererEntry: string;
13
13
  capabilities: RenderCapabilities;
14
14
  routes: RouteConfig;
15
+ dam?: DAMConfig;
15
16
  externalDependencies?: ExternalDependency[];
16
17
  rendererProtocol: 'http' | 'stdio-json';
17
18
  }
@@ -26,3 +27,6 @@ export interface ExternalDependency {
26
27
  description?: string;
27
28
  cacheTTL?: number;
28
29
  }
30
+ export interface DAMConfig {
31
+ autoApproveUploads?: boolean;
32
+ }
@@ -0,0 +1,51 @@
1
+ import { z } from 'zod';
2
+ export interface ImageSource {
3
+ src: string;
4
+ width: number;
5
+ height: number;
6
+ type: string;
7
+ }
8
+ export interface PictureSource {
9
+ media?: string;
10
+ type: string;
11
+ srcset: string;
12
+ sizes?: string;
13
+ }
14
+ export interface GradialImage {
15
+ $type: 'gradial.image';
16
+ assetId: string;
17
+ versionId: string;
18
+ alt: string;
19
+ fallback: ImageSource;
20
+ sources: PictureSource[];
21
+ }
22
+ export interface ImageSlotContract {
23
+ outputs: SlotOutput[];
24
+ formats: string[];
25
+ sizes: string;
26
+ }
27
+ export interface SlotOutput {
28
+ aspectRatio: string;
29
+ widths: number[];
30
+ media?: string;
31
+ }
32
+ export declare const GradialImageSchema: z.ZodObject<{
33
+ $type: z.ZodLiteral<"gradial.image">;
34
+ assetId: z.ZodString;
35
+ versionId: z.ZodString;
36
+ alt: z.ZodString;
37
+ fallback: z.ZodObject<{
38
+ src: z.ZodString;
39
+ width: z.ZodNumber;
40
+ height: z.ZodNumber;
41
+ type: z.ZodString;
42
+ }, z.core.$strip>;
43
+ sources: z.ZodArray<z.ZodObject<{
44
+ media: z.ZodOptional<z.ZodString>;
45
+ type: z.ZodString;
46
+ srcset: z.ZodString;
47
+ sizes: z.ZodOptional<z.ZodString>;
48
+ }, z.core.$strip>>;
49
+ }, z.core.$strip>;
50
+ export type ImageHTMLAttributes = Record<string, string | number | boolean | undefined | null>;
51
+ export declare function renderImageHTML(image: GradialImage, attrs?: ImageHTMLAttributes): string;
@@ -0,0 +1,58 @@
1
+ import { z } from 'zod';
2
+ export const GradialImageSchema = z.object({
3
+ $type: z.literal('gradial.image'),
4
+ assetId: z.string().min(1),
5
+ versionId: z.string().min(1),
6
+ alt: z.string(),
7
+ fallback: z.object({
8
+ src: z.string().min(1),
9
+ width: z.number().int().nonnegative(),
10
+ height: z.number().int().nonnegative(),
11
+ type: z.string().min(1),
12
+ }),
13
+ sources: z.array(z.object({
14
+ media: z.string().optional(),
15
+ type: z.string().min(1),
16
+ srcset: z.string().min(1),
17
+ sizes: z.string().optional(),
18
+ })),
19
+ });
20
+ export function renderImageHTML(image, attrs = {}) {
21
+ const imgAttrs = renderAttrs({
22
+ ...attrs,
23
+ src: image.fallback.src,
24
+ alt: image.alt,
25
+ width: image.fallback.width > 0 ? image.fallback.width : undefined,
26
+ height: image.fallback.height > 0 ? image.fallback.height : undefined,
27
+ });
28
+ const img = `<img${imgAttrs}>`;
29
+ if (!image.sources.length) {
30
+ return img;
31
+ }
32
+ const sources = image.sources.map((source) => `<source${renderAttrs({
33
+ media: source.media,
34
+ type: source.type,
35
+ srcset: source.srcset,
36
+ sizes: source.sizes,
37
+ })}>`).join('');
38
+ return `<picture>${sources}${img}</picture>`;
39
+ }
40
+ function renderAttrs(attrs) {
41
+ const rendered = Object.entries(attrs)
42
+ .filter(([, value]) => value !== undefined && value !== null && value !== false)
43
+ .map(([name, value]) => value === true ? escapeName(name) : `${escapeName(name)}="${escapeAttr(String(value))}"`);
44
+ if (!rendered.length) {
45
+ return '';
46
+ }
47
+ return ' ' + rendered.join(' ');
48
+ }
49
+ function escapeName(name) {
50
+ return name.replace(/[^A-Za-z0-9_:-]/g, '');
51
+ }
52
+ function escapeAttr(value) {
53
+ return value
54
+ .replace(/&/g, '&amp;')
55
+ .replace(/"/g, '&quot;')
56
+ .replace(/</g, '&lt;')
57
+ .replace(/>/g, '&gt;');
58
+ }
@@ -4,3 +4,4 @@ export * from './layout.js';
4
4
  export * from './page.js';
5
5
  export * from './renderer.js';
6
6
  export * from './render-mode.js';
7
+ export * from './image.js';
@@ -4,3 +4,4 @@ export * from './layout.js';
4
4
  export * from './page.js';
5
5
  export * from './renderer.js';
6
6
  export * from './render-mode.js';
7
+ export * from './image.js';
@@ -1,12 +1,24 @@
1
+ import type { FragmentRefNode } from './page.js';
1
2
  export interface LayoutSlot {
2
3
  name: string;
3
4
  required: boolean;
4
5
  }
6
+ /** Default content for non-required layout slots, keyed by slot name. */
7
+ export type LayoutDefaults = Record<string, FragmentRefNode[]>;
5
8
  export interface LayoutDefinition {
6
9
  name: string;
7
10
  slots: LayoutSlot[];
11
+ /** Default fragment references for slots not provided by the page. */
12
+ defaults?: LayoutDefaults;
8
13
  }
9
14
  export interface LayoutContract {
10
15
  name: string;
11
16
  slots: LayoutSlot[];
17
+ defaults?: LayoutDefaults;
18
+ }
19
+ /** A compiled layout file as stored on disk. */
20
+ export interface CompiledLayout {
21
+ name: string;
22
+ slots: LayoutSlot[];
23
+ defaults: LayoutDefaults;
12
24
  }
package/package.json CHANGED
@@ -1,16 +1,20 @@
1
1
  {
2
2
  "name": "@gradial/aci",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
7
7
  "files": [
8
8
  "dist",
9
+ "bin/aci.js",
9
10
  "src/cli/*.mjs",
10
11
  "README.md"
11
12
  ],
13
+ "bin": {
14
+ "aci": "./bin/aci.js"
15
+ },
12
16
  "publishConfig": {
13
- "access": "restricted"
17
+ "access": "public"
14
18
  },
15
19
  "exports": {
16
20
  ".": {
@@ -41,14 +45,26 @@
41
45
  "types": "./dist/providers/file.d.ts",
42
46
  "import": "./dist/providers/file.js"
43
47
  },
48
+ "./providers/s3": {
49
+ "types": "./dist/providers/s3.d.ts",
50
+ "import": "./dist/providers/s3.js"
51
+ },
44
52
  "./dev": {
45
53
  "types": "./dist/dev/index.d.ts",
46
54
  "import": "./dist/dev/index.js"
47
55
  },
56
+ "./assets": {
57
+ "types": "./dist/assets/index.d.ts",
58
+ "import": "./dist/assets/index.js"
59
+ },
48
60
  "./astro": {
49
61
  "types": "./dist/astro/index.d.ts",
50
62
  "import": "./dist/astro/index.js"
51
63
  },
64
+ "./next": {
65
+ "types": "./dist/next/index.d.ts",
66
+ "import": "./dist/next/index.js"
67
+ },
52
68
  "./next/server": {
53
69
  "types": "./dist/next/server.d.ts",
54
70
  "import": "./dist/next/server.js",
@@ -72,6 +88,7 @@
72
88
  "import": "./dist/testing/index.js"
73
89
  },
74
90
  "./cli/compile-registry": "./src/cli/compile-registry.mjs",
91
+ "./cli/validate-content": "./src/cli/validate-content.mjs",
75
92
  "./cli/verify-renderer": "./src/cli/verify-renderer.mjs"
76
93
  },
77
94
  "scripts": {
@@ -99,6 +116,13 @@
99
116
  "optional": true
100
117
  }
101
118
  },
119
+ "optionalDependencies": {
120
+ "@gradial/cli-darwin-arm64": "0.1.0",
121
+ "@gradial/cli-darwin-x64": "0.1.0",
122
+ "@gradial/cli-linux-x64": "0.1.0",
123
+ "@gradial/cli-linux-arm64": "0.1.0",
124
+ "@gradial/cli-win32-x64": "0.1.0"
125
+ },
102
126
  "devDependencies": {
103
127
  "@vercel/edge-config": "^1.4.3",
104
128
  "@types/node": "^24.10.1",