@gradial/aci 0.1.52-rc.26 → 0.1.52-rc.29
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/CHANGELOG.md +0 -28
- package/README.md +11 -4
- package/dist/astro/error-page.js +1 -0
- package/dist/astro/index.d.ts +0 -4
- package/dist/astro/index.js +0 -32
- package/dist/astro/page.d.ts +5 -2
- package/dist/astro/page.js +35 -23
- package/dist/cli/compile-registry.mjs +6 -34
- package/dist/content/index.d.ts +2 -1
- package/dist/content/index.js +4 -1
- package/dist/content/provider.d.ts +8 -2
- package/dist/content/provider.js +5 -1
- package/dist/content/resolve-slots.d.ts +24 -2
- package/dist/content/resolve-slots.js +127 -21
- package/dist/content/types.d.ts +5 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/mintlify/blocks.js +4 -1
- package/dist/mintlify/index.d.ts +1 -1
- package/dist/mintlify/index.js +1 -1
- package/dist/mintlify/materialize.d.ts +3 -1
- package/dist/mintlify/materialize.js +105 -10
- package/dist/mintlify/types.d.ts +7 -2
- package/dist/mintlify/validate.js +6 -2
- package/dist/next/config.js +0 -24
- package/dist/next/error-seat-payload.d.ts +2 -1
- package/dist/next/error-seat-payload.js +14 -24
- package/dist/next/error-seat.js +4 -2
- package/dist/next/render.d.ts +1 -1
- package/dist/next/render.js +7 -4
- package/dist/next/server.js +2 -1
- package/dist/testing/conformance/root-identity.js +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +2 -1
- package/dist/types/block-ref.js +2 -2
- package/dist/types/config.d.ts +0 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.js +0 -1
- package/dist/types/page.d.ts +0 -1
- package/package.json +7 -7
- package/src/cli/compile-registry.mjs +6 -30
- package/src/types/block-ref.test.ts +16 -1
- package/src/types/block-ref.ts +2 -2
- package/src/types/config.ts +0 -1
- package/src/types/index.ts +0 -1
- package/src/types/page.ts +0 -1
- package/templates/astro/template/.aci.yaml.tmpl +0 -1
- package/templates/common/.agents/skills/aci/SKILL.md +1 -1
- package/templates/common/.agents/skills/authoring/SKILL.md +190 -39
- package/templates/common/.agents/skills/authoring/references/block-registry.md +4 -3
- package/templates/common/.agents/skills/authoring/references/contracts.md +9 -30
- package/templates/common/.agents/skills/authoring/references/repo-structure.md +5 -4
- package/templates/common/.agents/skills/authoring/references/sdk-server-functions.md +3 -3
- package/templates/common/.agents/skills/migration/references/canon-migration.md +18 -67
- package/templates/common/.content/fragments/navbar/_index.json +24 -0
- package/templates/common/.content/fragments/site-footer/_index.json +61 -0
- package/templates/common/.content/pages/404/_index.json +4 -2
- package/templates/common/.content/pages/_index.json +0 -1
- package/templates/common/.content/pages/error/_index.json +4 -2
- package/templates/common/.content/templates/error.json +24 -0
- package/templates/common/.content/templates/home.json +28 -0
- package/templates/common/.content/templates/site.json +8 -0
- package/templates/common/.content/templates/standard.json +24 -0
- package/templates/common/starter-content/.content/pages/404/_index.json +0 -1
- package/templates/common/starter-content/.content/pages/_index.json +12 -1
- package/templates/common/starter-content/.content/pages/about/_index.json +1 -1
- package/templates/common/starter-content/.content/pages/error/_index.json +0 -1
- package/templates/common/starter-content/.content/patterns/components/button.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/container.json +8 -3
- package/templates/common/starter-content/.content/patterns/components/feature_card.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/feature_grid.json +5 -3
- package/templates/common/starter-content/.content/patterns/components/home_hero.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/personalization_choice.json +42 -0
- package/templates/common/starter-content/.content/patterns/components/site-footer.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/site-navigation.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/stats_bar.json +2 -2
- package/templates/nextjs/template/.aci.yaml +0 -1
- package/templates/nextjs/template/.aci.yaml.tmpl +0 -1
- package/dist/define-layout.d.ts +0 -6
- package/dist/define-layout.js +0 -14
- package/dist/types/layout.d.ts +0 -24
- package/dist/types/layout.js +0 -1
- package/src/types/layout.ts +0 -29
- package/templates/astro/template/src/cms/contracts/layouts/index.ts +0 -18
- package/templates/common/starter-content/.content/patterns/layouts/default.json +0 -14
- package/templates/nextjs/template/src/cms/contracts/layouts/index.ts +0 -16
package/CHANGELOG.md
CHANGED
|
@@ -1,33 +1,5 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.52-rc.26 - 2026-09-30
|
|
4
|
-
|
|
5
|
-
### Fixed
|
|
6
|
-
|
|
7
|
-
- Configure Astro's `build.assetsPrefix` for external CDN builds. Astro
|
|
8
|
-
overrides a nested Vite `base`, which caused real production builds to keep
|
|
9
|
-
emitting root-relative `/assets/` URLs even when a CDN base was configured.
|
|
10
|
-
|
|
11
|
-
## 0.1.52-rc.25 - 2026-09-30
|
|
12
|
-
|
|
13
|
-
### Added
|
|
14
|
-
|
|
15
|
-
- Allow Astro builds to opt into framework-neutral `assets/` output with
|
|
16
|
-
`ACI_ASSET_PATH_LAYOUT=flat-assets`. Next.js rejects that layout until it
|
|
17
|
-
can produce the same public and stored paths safely.
|
|
18
|
-
|
|
19
|
-
## 0.1.52-rc.24 - 2026-09-18
|
|
20
|
-
|
|
21
|
-
### Added
|
|
22
|
-
|
|
23
|
-
- `withAci()` (Astro and Next.js) reads `ACI_ASSET_BASE_TPL` and, when it is
|
|
24
|
-
an absolute CDN URL, sets Vite's `base` / Next.js's `assetPrefix` to it so
|
|
25
|
-
the SSG build writes asset URLs pointing at the CDN directly — no HTML
|
|
26
|
-
post-processing needed. `__RELEASE_ID__` is stripped because build assets
|
|
27
|
-
are content-addressed and release-independent. Managed tenants (the
|
|
28
|
-
`/__r/__RELEASE_ID__/` default) are left untouched; the edge handler
|
|
29
|
-
rewrites relative paths to the global S3 key.
|
|
30
|
-
|
|
31
3
|
## 0.1.52-rc.7 - 2026-08-19
|
|
32
4
|
|
|
33
5
|
### Fixed
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agentic Content Infrastructure SDK
|
|
2
2
|
|
|
3
|
-
`@gradial/aci` `0.1.52-rc.
|
|
3
|
+
`@gradial/aci` `0.1.52-rc.29` is a pre-1.0 public-surface reset for the ACI
|
|
4
4
|
client SDK. Breaking changes from the old `0.1.x` accidental surface are
|
|
5
5
|
intentional: the root import is framework-neutral, framework runtime behavior is
|
|
6
6
|
behind explicit subpaths, and the first required customer path is React/Next.
|
|
@@ -8,7 +8,7 @@ behind explicit subpaths, and the first required customer path is React/Next.
|
|
|
8
8
|
## Install
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
|
-
npm install @gradial/aci@0.1.52-rc.
|
|
11
|
+
npm install @gradial/aci@0.1.52-rc.29 zod@^4
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
## Root Import
|
|
@@ -62,6 +62,13 @@ export default withAci(nextConfig, {
|
|
|
62
62
|
|
|
63
63
|
Astro public subpath is `@gradial/aci/astro`.
|
|
64
64
|
|
|
65
|
+
## Page Templates
|
|
66
|
+
|
|
67
|
+
Pages use content-authored templates for region order, defaults, composition
|
|
68
|
+
rules, and personalization locks. Start with the [page templates guide](docs/page-templates.md),
|
|
69
|
+
including the `blog_body` contract pattern for page-specific container children.
|
|
70
|
+
[Component contracts](docs/component-contracts.md) define props and nested block types.
|
|
71
|
+
|
|
65
72
|
## Block Root Identity
|
|
66
73
|
|
|
67
74
|
The renderer passes every block two props, `data-aci-block` (the authored
|
|
@@ -130,13 +137,13 @@ host bundles select and serve immutable release artifacts.
|
|
|
130
137
|
The first RC scaffold path is Next:
|
|
131
138
|
|
|
132
139
|
```bash
|
|
133
|
-
npx @gradial/aci@0.1.52-rc.
|
|
140
|
+
npx @gradial/aci@0.1.52-rc.29 init my-site --template next-starter
|
|
134
141
|
```
|
|
135
142
|
|
|
136
143
|
List available templates:
|
|
137
144
|
|
|
138
145
|
```bash
|
|
139
|
-
npx @gradial/aci@0.1.52-rc.
|
|
146
|
+
npx @gradial/aci@0.1.52-rc.29 init --list
|
|
140
147
|
```
|
|
141
148
|
|
|
142
149
|
## Personalization: The One-Tag Install
|
package/dist/astro/error-page.js
CHANGED
|
@@ -50,6 +50,7 @@ export async function getErrorPageData(astro, kind, options = {}) {
|
|
|
50
50
|
if (resolved.route) {
|
|
51
51
|
const slug = resolved.route.replace(/^\/+|\/+$/g, '');
|
|
52
52
|
const data = await getPageData({
|
|
53
|
+
isPrerendered: astro.isPrerendered,
|
|
53
54
|
params: { slug },
|
|
54
55
|
request: astro.request,
|
|
55
56
|
url: astro.url,
|
package/dist/astro/index.d.ts
CHANGED
|
@@ -4,10 +4,6 @@ import type { BlockRef } from '../types/block-ref.js';
|
|
|
4
4
|
interface AstroConfigSetupParams {
|
|
5
5
|
injectScript(stage: 'head-inline' | 'page' | 'page-ssr', content: string): void;
|
|
6
6
|
updateConfig(config: {
|
|
7
|
-
build?: {
|
|
8
|
-
assets?: string;
|
|
9
|
-
assetsPrefix?: string;
|
|
10
|
-
};
|
|
11
7
|
vite?: {
|
|
12
8
|
plugins?: unknown[];
|
|
13
9
|
};
|
package/dist/astro/index.js
CHANGED
|
@@ -83,38 +83,6 @@ export function withAci(options = {}) {
|
|
|
83
83
|
name: 'aci-astro',
|
|
84
84
|
hooks: {
|
|
85
85
|
'astro:config:setup'({ injectScript, updateConfig }) {
|
|
86
|
-
const assetPathLayout = (process.env.ACI_ASSET_PATH_LAYOUT || 'preserve').trim();
|
|
87
|
-
if (assetPathLayout !== 'preserve' && assetPathLayout !== 'flat-assets') {
|
|
88
|
-
throw new Error('ACI_ASSET_PATH_LAYOUT must be preserve or flat-assets');
|
|
89
|
-
}
|
|
90
|
-
if (assetPathLayout === 'flat-assets') {
|
|
91
|
-
updateConfig({ build: { assets: 'assets' } });
|
|
92
|
-
}
|
|
93
|
-
// Asset URL prefix for production builds. When
|
|
94
|
-
// ACI_ASSET_BASE_TPL is an absolute CDN URL, use Astro's
|
|
95
|
-
// build.assetsPrefix so the SSG build writes generated asset URLs
|
|
96
|
-
// (CSS and JS) pointing at the CDN directly. Astro owns Vite's final
|
|
97
|
-
// base value, so setting vite.base here does not survive a real build.
|
|
98
|
-
//
|
|
99
|
-
// Build assets are content-addressed (hashed filenames) and stored
|
|
100
|
-
// globally, so the URL is release-independent: strip __RELEASE_ID__.
|
|
101
|
-
// - Managed tenants (edge handler): ACI_ASSET_BASE_TPL is
|
|
102
|
-
// "/__r/__RELEASE_ID__/" or unset — do NOT set Vite base; the
|
|
103
|
-
// SSG writes relative paths (/_astro/foo.js) and the edge
|
|
104
|
-
// handler's viewer-request rewrites to the global S3 key.
|
|
105
|
-
// - External CDN tenants: "https://cdn.gradial.com/" → Vite base,
|
|
106
|
-
// Astro writes absolute CDN URLs.
|
|
107
|
-
const rawTpl = process.env.ACI_ASSET_BASE_TPL || '';
|
|
108
|
-
const assetBase = rawTpl
|
|
109
|
-
.replace('__RELEASE_ID__/', '')
|
|
110
|
-
.replace('__RELEASE_ID__', '')
|
|
111
|
-
.replace(/\/+$/, '/');
|
|
112
|
-
const isAbsoluteUrl = assetBase.startsWith('http://') || assetBase.startsWith('https://');
|
|
113
|
-
if (isAbsoluteUrl
|
|
114
|
-
&& assetBase !== '/'
|
|
115
|
-
&& process.env.NODE_ENV === 'production') {
|
|
116
|
-
updateConfig({ build: { assetsPrefix: assetBase } });
|
|
117
|
-
}
|
|
118
86
|
if (process.env.NODE_ENV === 'production') {
|
|
119
87
|
return;
|
|
120
88
|
}
|
package/dist/astro/page.d.ts
CHANGED
|
@@ -31,12 +31,13 @@
|
|
|
31
31
|
* iterate slots and render blocks via the registry.
|
|
32
32
|
*/
|
|
33
33
|
import type { KernelPage, KernelSiteConfig, KernelRouteMetadata, KernelPageBlock } from '../content/types.js';
|
|
34
|
-
import type
|
|
34
|
+
import { type LayoutEntry } from '../content/provider.js';
|
|
35
35
|
/**
|
|
36
36
|
* Minimal Astro global shape — only the properties getPageData reads.
|
|
37
37
|
* This avoids depending on Astro's own types at the SDK level.
|
|
38
38
|
*/
|
|
39
39
|
export interface AstroGlobal {
|
|
40
|
+
isPrerendered?: boolean;
|
|
40
41
|
params: {
|
|
41
42
|
slug?: string;
|
|
42
43
|
};
|
|
@@ -51,6 +52,8 @@ export interface AstroGlobal {
|
|
|
51
52
|
export interface PageData {
|
|
52
53
|
/** Route string (normalized, e.g. "/" or "/about"). */
|
|
53
54
|
route: string;
|
|
55
|
+
/** Selected render tuple, including the explicit "base" identity. */
|
|
56
|
+
variantKey: string;
|
|
54
57
|
/** Site configuration. */
|
|
55
58
|
siteConfig: KernelSiteConfig;
|
|
56
59
|
/** Page content. */
|
|
@@ -91,4 +94,4 @@ export interface GetPageDataOptions {
|
|
|
91
94
|
* directly from CloudFront in production (the CDN points at the release
|
|
92
95
|
* artifacts in S3, no runtime content fetching).
|
|
93
96
|
*/
|
|
94
|
-
export declare function getPageData(astro: AstroGlobal,
|
|
97
|
+
export declare function getPageData(astro: AstroGlobal, options?: GetPageDataOptions): Promise<PageData>;
|
package/dist/astro/page.js
CHANGED
|
@@ -30,11 +30,12 @@
|
|
|
30
30
|
* The page template receives a typed data object and only needs to
|
|
31
31
|
* iterate slots and render blocks via the registry.
|
|
32
32
|
*/
|
|
33
|
+
import { FragmentNotFoundError } from '../content/provider.js';
|
|
33
34
|
import { normalizeRoute } from '../content/routes.js';
|
|
34
35
|
import { routeMetadataForContent } from '../content/provider.js';
|
|
35
36
|
import { resolveSlots } from '../content/resolve-slots.js';
|
|
36
37
|
import { assignmentsScriptTagHTML, collectScriptTagHTML } from '../content/personalization.js';
|
|
37
|
-
import { getPendingRenderInputFromHeaders } from '../runtime/page.js';
|
|
38
|
+
import { GRADIAL_RENDER_INPUT_HEADER, getPendingRenderInputFromHeaders } from '../runtime/page.js';
|
|
38
39
|
// ---------------------------------------------------------------------------
|
|
39
40
|
// Implementation
|
|
40
41
|
// ---------------------------------------------------------------------------
|
|
@@ -52,23 +53,25 @@ import { getPendingRenderInputFromHeaders } from '../runtime/page.js';
|
|
|
52
53
|
* directly from CloudFront in production (the CDN points at the release
|
|
53
54
|
* artifacts in S3, no runtime content fetching).
|
|
54
55
|
*/
|
|
55
|
-
export async function getPageData(astro,
|
|
56
|
+
export async function getPageData(astro, options) {
|
|
56
57
|
const route = normalizeRoute(astro.params.slug ? `/${astro.params.slug}` : '/');
|
|
57
|
-
//
|
|
58
|
-
//
|
|
58
|
+
// Static Astro generation is explicit; unexpected header access errors and
|
|
59
|
+
// expired render-input IDs must not turn a selected variant into local base.
|
|
59
60
|
let runtimeInput = null;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
if (!astro.isPrerendered) {
|
|
62
|
+
const headers = astro.request.headers;
|
|
63
|
+
runtimeInput = getPendingRenderInputFromHeaders(headers);
|
|
64
|
+
if (headers.has(GRADIAL_RENDER_INPUT_HEADER) && !runtimeInput) {
|
|
65
|
+
throw new Error(`ACI Astro render input is unavailable for route ${route}; refusing to render local base content`);
|
|
66
|
+
}
|
|
66
67
|
}
|
|
67
68
|
let siteConfig;
|
|
68
69
|
let page;
|
|
69
70
|
let providerModule = null;
|
|
70
71
|
let provider = null;
|
|
72
|
+
let variantKey;
|
|
71
73
|
if (runtimeInput) {
|
|
74
|
+
variantKey = requireVariantKey(runtimeInput.variantKey, route);
|
|
72
75
|
siteConfig = runtimeInput.siteConfig;
|
|
73
76
|
page = runtimeInput.page;
|
|
74
77
|
}
|
|
@@ -76,19 +79,23 @@ export async function getPageData(astro, _options) {
|
|
|
76
79
|
// Lazy-import FileContentProvider to avoid pulling it in when
|
|
77
80
|
// preview render input is available
|
|
78
81
|
providerModule = await import('../providers/file.js');
|
|
79
|
-
provider = new providerModule.FileContentProvider();
|
|
82
|
+
provider = new providerModule.FileContentProvider(options?.compiledRoot);
|
|
80
83
|
[siteConfig, page] = await Promise.all([
|
|
81
84
|
provider.getSiteConfig(),
|
|
82
85
|
provider.getPage(route),
|
|
83
86
|
]);
|
|
87
|
+
const entry = (await provider.listRoutes()).find((entry) => normalizeRoute(entry.path) === route);
|
|
88
|
+
variantKey = requireVariantKey(entry?.variantKey, route);
|
|
84
89
|
}
|
|
85
90
|
const meta = routeMetadataForContent(siteConfig, page);
|
|
86
91
|
// Resolve layout: prefer render input, fall back to provider
|
|
87
|
-
const layout = await resolveLayout(runtimeInput, provider,
|
|
92
|
+
const layout = await resolveLayout(runtimeInput, provider, page.layout, route);
|
|
88
93
|
// Resolve slots: page regions filled, then layout fragment defaults
|
|
89
94
|
const slots = await resolveSlots(layout, page?.regions || {}, async (id) => {
|
|
90
|
-
if (runtimeInput
|
|
91
|
-
|
|
95
|
+
if (runtimeInput) {
|
|
96
|
+
if (Object.hasOwn(runtimeInput.fragments ?? {}, id))
|
|
97
|
+
return runtimeInput.fragments[id];
|
|
98
|
+
throw new FragmentNotFoundError(id, new Error('fragment is not bound by the selected page'));
|
|
92
99
|
}
|
|
93
100
|
const p = provider ?? new (await import('../providers/file.js')).FileContentProvider();
|
|
94
101
|
return await p.getFragment(id);
|
|
@@ -97,22 +104,27 @@ export async function getPageData(astro, _options) {
|
|
|
97
104
|
const isPreview = astro.url.searchParams.has('preview_token');
|
|
98
105
|
const collectTag = isPreview ? '' : collectScriptTagHTML(siteConfig);
|
|
99
106
|
const assignmentsTag = isPreview ? '' : assignmentsScriptTagHTML(page);
|
|
100
|
-
return { route, siteConfig, page, meta, layout, slots, collectTag, assignmentsTag };
|
|
107
|
+
return { route, variantKey, siteConfig, page, meta, layout, slots, collectTag, assignmentsTag };
|
|
108
|
+
}
|
|
109
|
+
function requireVariantKey(value, route) {
|
|
110
|
+
if (typeof value !== 'string' || !value.trim() || value !== value.trim()) {
|
|
111
|
+
throw new Error(`ACI Astro render variant identity is missing or invalid for route ${route}; supply an explicit variantKey ("base" for an unpersonalized render)`);
|
|
112
|
+
}
|
|
113
|
+
return value;
|
|
101
114
|
}
|
|
102
115
|
// ---------------------------------------------------------------------------
|
|
103
116
|
// Internal helpers
|
|
104
117
|
// ---------------------------------------------------------------------------
|
|
105
|
-
async function resolveLayout(runtimeInput, provider,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
118
|
+
async function resolveLayout(runtimeInput, provider, layoutName, route) {
|
|
119
|
+
if (runtimeInput) {
|
|
120
|
+
const layout = runtimeInput.layouts?.[layoutName];
|
|
121
|
+
if (!layout)
|
|
122
|
+
throw new Error(`ACI Astro render input for route ${route} is missing its selected layout ${JSON.stringify(layoutName)}`);
|
|
123
|
+
return layout;
|
|
109
124
|
}
|
|
110
125
|
// Fall back to provider
|
|
111
126
|
if (provider) {
|
|
112
127
|
return await provider.getLayout(layoutName);
|
|
113
128
|
}
|
|
114
|
-
|
|
115
|
-
const mod = providerModule ?? await import('../providers/file.js');
|
|
116
|
-
const p = new mod.FileContentProvider();
|
|
117
|
-
return await p.getLayout(layoutName);
|
|
129
|
+
throw new Error(`ACI Astro content provider is unavailable for route ${route}`);
|
|
118
130
|
}
|
|
@@ -31,13 +31,15 @@ if (isMainModule()) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
async function compileRegistry(options) {
|
|
34
|
-
|
|
34
|
+
if (options.layouts) {
|
|
35
|
+
throw new Error('--layouts is retired; author page composition under /templates instead');
|
|
36
|
+
}
|
|
37
|
+
const required = ['components', 'output'];
|
|
35
38
|
for (const key of required) {
|
|
36
39
|
if (!options[key])
|
|
37
40
|
throw new Error(`missing --${key}`);
|
|
38
41
|
}
|
|
39
42
|
const components = await loadDefaultArray(options.components, 'component registry');
|
|
40
|
-
const layouts = await loadDefaultArray(options.layouts, 'layout registry');
|
|
41
43
|
const dataModels = options.data
|
|
42
44
|
? await loadDefaultArray(options.data, 'data model registry')
|
|
43
45
|
: [];
|
|
@@ -65,8 +67,6 @@ async function compileRegistry(options) {
|
|
|
65
67
|
...(Object.keys(slots).length ? { slots } : {}),
|
|
66
68
|
});
|
|
67
69
|
}
|
|
68
|
-
const seenLayouts = new Set();
|
|
69
|
-
const layoutEntries = layouts.map((layout, index) => validateLayout(layout, index, seenLayouts));
|
|
70
70
|
const seenDataModels = new Set();
|
|
71
71
|
const dataModelEntries = [];
|
|
72
72
|
if (dataModels.length > 0) {
|
|
@@ -98,7 +98,6 @@ async function compileRegistry(options) {
|
|
|
98
98
|
manifestVersion: '1',
|
|
99
99
|
generatedAt: new Date().toISOString(),
|
|
100
100
|
components: componentEntries,
|
|
101
|
-
layouts: layoutEntries,
|
|
102
101
|
dataModels: dataModelEntries,
|
|
103
102
|
};
|
|
104
103
|
const registryPath = path.join(outDir, 'registry.json');
|
|
@@ -109,7 +108,7 @@ async function compileRegistry(options) {
|
|
|
109
108
|
}
|
|
110
109
|
async function loadDefaultArray(modulePath, label) {
|
|
111
110
|
const mod = await import(pathToFileURL(path.resolve(modulePath)).href);
|
|
112
|
-
const value = mod.default ?? mod.registry ?? mod.components ?? mod.
|
|
111
|
+
const value = mod.default ?? mod.registry ?? mod.components ?? mod.dataModels;
|
|
113
112
|
if (!Array.isArray(value))
|
|
114
113
|
throw new Error(`${label} must export an array`);
|
|
115
114
|
return value;
|
|
@@ -397,32 +396,6 @@ function unwrapSchema(schema) {
|
|
|
397
396
|
}
|
|
398
397
|
return current;
|
|
399
398
|
}
|
|
400
|
-
function validateLayout(layout, index, seen) {
|
|
401
|
-
if (!layout || typeof layout !== 'object')
|
|
402
|
-
throw new Error(`layouts[${index}] must be an object`);
|
|
403
|
-
if (!layout.name)
|
|
404
|
-
throw new Error(`layouts[${index}].name is required`);
|
|
405
|
-
if (seen.has(layout.name))
|
|
406
|
-
throw new Error(`duplicate layout name ${layout.name}`);
|
|
407
|
-
seen.add(layout.name);
|
|
408
|
-
if (!Array.isArray(layout.slots))
|
|
409
|
-
throw new Error(`layouts[${index}].slots must be an array`);
|
|
410
|
-
const slots = [];
|
|
411
|
-
const seenSlots = new Set();
|
|
412
|
-
for (const [slotIndex, slot] of layout.slots.entries()) {
|
|
413
|
-
if (!slot?.name)
|
|
414
|
-
throw new Error(`layouts[${index}].slots[${slotIndex}].name is required`);
|
|
415
|
-
if (seenSlots.has(slot.name))
|
|
416
|
-
throw new Error(`duplicate slot name ${slot.name} in layout ${layout.name}`);
|
|
417
|
-
seenSlots.add(slot.name);
|
|
418
|
-
slots.push({ name: slot.name, required: Boolean(slot.required) });
|
|
419
|
-
}
|
|
420
|
-
const entry = { name: layout.name, slots };
|
|
421
|
-
if (layout.defaults && typeof layout.defaults === 'object') {
|
|
422
|
-
entry.defaults = layout.defaults;
|
|
423
|
-
}
|
|
424
|
-
return entry;
|
|
425
|
-
}
|
|
426
399
|
function compileSchema(schema, name) {
|
|
427
400
|
const toJSONSchema = z.toJSONSchema;
|
|
428
401
|
if (typeof toJSONSchema !== 'function') {
|
|
@@ -516,13 +489,12 @@ async function generatePageSchema(schemaDir, componentEntries, outDir) {
|
|
|
516
489
|
$id: 'https://baremetal.local/schemas/page.schema.json',
|
|
517
490
|
title: 'bare-metal Page',
|
|
518
491
|
type: 'object',
|
|
519
|
-
required: ['$type', 'id', 'status', '
|
|
492
|
+
required: ['$type', 'id', 'status', 'metadata', 'regions'],
|
|
520
493
|
additionalProperties: true,
|
|
521
494
|
properties: {
|
|
522
495
|
$type: { const: 'page' },
|
|
523
496
|
id: { type: 'string', minLength: 1 },
|
|
524
497
|
status: { type: 'string', minLength: 1 },
|
|
525
|
-
layout: { type: 'string', minLength: 1 },
|
|
526
498
|
metadata: {
|
|
527
499
|
type: 'object',
|
|
528
500
|
required: ['title'],
|
package/dist/content/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from './routes.js';
|
|
|
3
3
|
export * from './types.js';
|
|
4
4
|
export * from './personalization.js';
|
|
5
5
|
export * from './cache.js';
|
|
6
|
-
export
|
|
6
|
+
export { FragmentResolutionError, MAX_EXPANDED_BLOCKS, MAX_FRAGMENT_DEPTH, fragmentToBlock, preserveFragmentScope, resolveFragmentNodes, resolveSlots, resolveSlotsSync, } from './resolve-slots.js';
|
|
7
|
+
export type { FragmentBlock, FragmentData, FragmentLoader } from './resolve-slots.js';
|
|
7
8
|
export * from './default-error-page.js';
|
|
8
9
|
export * from './error-pages.js';
|
package/dist/content/index.js
CHANGED
|
@@ -3,6 +3,9 @@ export * from './routes.js';
|
|
|
3
3
|
export * from './types.js';
|
|
4
4
|
export * from './personalization.js';
|
|
5
5
|
export * from './cache.js';
|
|
6
|
-
export
|
|
6
|
+
// Explicit list, not `export *`: fragmentRootScope is runtime-only provenance
|
|
7
|
+
// shared with mintlify/blocks.ts via a direct resolve-slots import and must
|
|
8
|
+
// never leak into the public '@gradial/aci/content' surface.
|
|
9
|
+
export { FragmentResolutionError, MAX_EXPANDED_BLOCKS, MAX_FRAGMENT_DEPTH, fragmentToBlock, preserveFragmentScope, resolveFragmentNodes, resolveSlots, resolveSlotsSync, } from './resolve-slots.js';
|
|
7
10
|
export * from './default-error-page.js';
|
|
8
11
|
export * from './error-pages.js';
|
|
@@ -2,6 +2,7 @@ import type { KernelPage, KernelRouteMetadata, KernelSiteConfig, RenderInput } f
|
|
|
2
2
|
export interface RouteEntry {
|
|
3
3
|
slug: string;
|
|
4
4
|
path: string;
|
|
5
|
+
variantKey: string;
|
|
5
6
|
payloadRef: string;
|
|
6
7
|
digest: string;
|
|
7
8
|
fragmentRefs?: string[];
|
|
@@ -25,6 +26,11 @@ export interface LayoutVariantEntry {
|
|
|
25
26
|
slots: LayoutSlot[];
|
|
26
27
|
defaults?: Record<string, FragmentRef[]>;
|
|
27
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Derived template render plan in the compiled release wire format.
|
|
31
|
+
* This is never an authorable layout: page templates own regions and defaults.
|
|
32
|
+
* The LayoutEntry name and layouts wire key remain for retained release readers.
|
|
33
|
+
*/
|
|
28
34
|
export interface LayoutEntry {
|
|
29
35
|
name: string;
|
|
30
36
|
digest?: string;
|
|
@@ -40,7 +46,6 @@ export interface LayoutSlot {
|
|
|
40
46
|
export interface FragmentRef {
|
|
41
47
|
kind: string;
|
|
42
48
|
fragmentId: string;
|
|
43
|
-
inline: boolean;
|
|
44
49
|
}
|
|
45
50
|
export interface CompiledManifest {
|
|
46
51
|
manifestVersion: string;
|
|
@@ -67,8 +72,9 @@ export declare class LayoutNotFoundError extends Error {
|
|
|
67
72
|
constructor(name: string, cause?: unknown);
|
|
68
73
|
}
|
|
69
74
|
export interface RenderInputOptions {
|
|
75
|
+
variantKey: string;
|
|
70
76
|
domain?: string;
|
|
71
77
|
locale?: string;
|
|
72
78
|
}
|
|
73
|
-
export declare function loadRenderInput(provider: ContentProvider, route
|
|
79
|
+
export declare function loadRenderInput(provider: ContentProvider, route: string | undefined, options: RenderInputOptions): Promise<RenderInput>;
|
|
74
80
|
export declare function routeMetadataForContent(siteConfig: KernelSiteConfig, page: KernelPage | null): KernelRouteMetadata;
|
package/dist/content/provider.js
CHANGED
|
@@ -24,8 +24,11 @@ export class LayoutNotFoundError extends Error {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
import { getCachedSiteConfig } from './cache.js';
|
|
27
|
-
export async function loadRenderInput(provider, route = '/', options
|
|
27
|
+
export async function loadRenderInput(provider, route = '/', options) {
|
|
28
28
|
const normalized = normalizeRoute(route);
|
|
29
|
+
if (typeof options?.variantKey !== 'string' || !options.variantKey.trim() || options.variantKey !== options.variantKey.trim()) {
|
|
30
|
+
throw new Error(`ACI render variant identity is missing or invalid for route ${normalized}; supply an explicit variantKey`);
|
|
31
|
+
}
|
|
29
32
|
const [siteConfig, page] = await Promise.all([
|
|
30
33
|
getCachedSiteConfig(provider),
|
|
31
34
|
provider.getPage(normalized).catch((error) => {
|
|
@@ -36,6 +39,7 @@ export async function loadRenderInput(provider, route = '/', options = {}) {
|
|
|
36
39
|
]);
|
|
37
40
|
return {
|
|
38
41
|
route: normalized,
|
|
42
|
+
variantKey: options.variantKey,
|
|
39
43
|
domain: options.domain || siteConfig.domain,
|
|
40
44
|
locale: (options.locale || siteConfig.defaultLocale || 'en-us').toLowerCase(),
|
|
41
45
|
siteConfig,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LayoutEntry } from './provider.js';
|
|
2
|
-
import type { KernelPageBlock } from './types.js';
|
|
2
|
+
import type { KernelPageBlock, KernelPageNode } from './types.js';
|
|
3
3
|
export interface FragmentBlock {
|
|
4
4
|
$type: 'component';
|
|
5
5
|
component: string;
|
|
@@ -13,4 +13,26 @@ export interface FragmentData {
|
|
|
13
13
|
props?: Record<string, unknown>;
|
|
14
14
|
}
|
|
15
15
|
export type FragmentLoader = (id: string) => Promise<FragmentData>;
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Shared Go/TS limit contract (sdk I-21). These values are pinned against
|
|
18
|
+
* internal/domain/content/compiler/testdata/limit-conformance.v1.json by
|
|
19
|
+
* limit-conformance.test.ts (TS) and limit_conformance_test.go (Go); the
|
|
20
|
+
* compiler's MaxFragmentDepth/MaxExpandedBlocks must match.
|
|
21
|
+
*/
|
|
22
|
+
/** Bounds a selected fragment reference chain, counting its root fragment. */
|
|
23
|
+
export declare const MAX_FRAGMENT_DEPTH = 10;
|
|
24
|
+
/** Bounds rendered component occurrences per page, including repeated placements. */
|
|
25
|
+
export declare const MAX_EXPANDED_BLOCKS = 4096;
|
|
26
|
+
/** Failures in a selected fragment are never eligible for local-content fallback. */
|
|
27
|
+
export declare class FragmentResolutionError extends Error {
|
|
28
|
+
constructor(message: string, options?: ErrorOptions);
|
|
29
|
+
}
|
|
30
|
+
export declare const fragmentRootScope: unique symbol;
|
|
31
|
+
export declare function preserveFragmentScope<T extends object>(source: object, target: T): T;
|
|
32
|
+
export declare function resolveSlots(plan: LayoutEntry, pageRegions: Record<string, KernelPageNode[]>, getFragment: FragmentLoader): Promise<Record<string, KernelPageBlock[]>>;
|
|
33
|
+
/** The config-time error seat uses the identical traversal with synchronous file reads. */
|
|
34
|
+
export declare function resolveSlotsSync(plan: LayoutEntry, pageRegions: Record<string, KernelPageNode[]>, getFragment: (id: string) => FragmentData): Record<string, KernelPageBlock[]>;
|
|
35
|
+
/** Resolve compiled references through the exact fragment bindings selected for this page. */
|
|
36
|
+
export declare function resolveFragmentNodes(nodes: readonly KernelPageNode[], getFragment: FragmentLoader): Promise<KernelPageBlock[]>;
|
|
37
|
+
/** Use the same fragment root identity for layout defaults and region placements. */
|
|
38
|
+
export declare function fragmentToBlock(fragment: FragmentData, id: string): KernelPageBlock;
|
|
@@ -1,26 +1,132 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Shared Go/TS limit contract (sdk I-21). These values are pinned against
|
|
3
|
+
* internal/domain/content/compiler/testdata/limit-conformance.v1.json by
|
|
4
|
+
* limit-conformance.test.ts (TS) and limit_conformance_test.go (Go); the
|
|
5
|
+
* compiler's MaxFragmentDepth/MaxExpandedBlocks must match.
|
|
6
|
+
*/
|
|
7
|
+
/** Bounds a selected fragment reference chain, counting its root fragment. */
|
|
8
|
+
export const MAX_FRAGMENT_DEPTH = 10;
|
|
9
|
+
/** Bounds rendered component occurrences per page, including repeated placements. */
|
|
10
|
+
export const MAX_EXPANDED_BLOCKS = 4096;
|
|
11
|
+
/** Failures in a selected fragment are never eligible for local-content fallback. */
|
|
12
|
+
export class FragmentResolutionError extends Error {
|
|
13
|
+
constructor(message, options) {
|
|
14
|
+
super(message, options);
|
|
15
|
+
this.name = 'FragmentResolutionError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// Runtime-only provenance keeps each fragment placement's authored IDs in its
|
|
19
|
+
// own validation scope. It is never emitted into compiled JSON or HTML.
|
|
20
|
+
export const fragmentRootScope = Symbol('aci.fragment-root-scope');
|
|
21
|
+
export function preserveFragmentScope(source, target) {
|
|
22
|
+
return Object.hasOwn(source, fragmentRootScope)
|
|
23
|
+
? Object.defineProperty(target, fragmentRootScope, { value: true })
|
|
24
|
+
: target;
|
|
25
|
+
}
|
|
26
|
+
export async function resolveSlots(plan, pageRegions, getFragment) {
|
|
27
|
+
return resolveAsync(walkSlots(plan, pageRegions), getFragment);
|
|
28
|
+
}
|
|
29
|
+
/** The config-time error seat uses the identical traversal with synchronous file reads. */
|
|
30
|
+
export function resolveSlotsSync(plan, pageRegions, getFragment) {
|
|
31
|
+
const walk = walkSlots(plan, pageRegions);
|
|
32
|
+
let step = walk.next();
|
|
33
|
+
while (!step.done) {
|
|
34
|
+
let fragment;
|
|
35
|
+
try {
|
|
36
|
+
fragment = getFragment(step.value);
|
|
8
37
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const blocks = [];
|
|
12
|
-
for (const ref of defaults) {
|
|
13
|
-
const fragment = await getFragment(ref.fragmentId);
|
|
14
|
-
const block = fragment.block ?? { component: fragment.component ?? '', props: fragment.props };
|
|
15
|
-
blocks.push({
|
|
16
|
-
$type: 'component',
|
|
17
|
-
id: fragment.id ?? fragment.fragmentId ?? ref.fragmentId,
|
|
18
|
-
component: block.component,
|
|
19
|
-
props: block.props || {},
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
resolved[slot.name] = blocks;
|
|
38
|
+
catch (cause) {
|
|
39
|
+
throw bindingError(step.value, cause);
|
|
23
40
|
}
|
|
41
|
+
step = walk.next(fragment);
|
|
42
|
+
}
|
|
43
|
+
return step.value;
|
|
44
|
+
}
|
|
45
|
+
/** Resolve compiled references through the exact fragment bindings selected for this page. */
|
|
46
|
+
export async function resolveFragmentNodes(nodes, getFragment) {
|
|
47
|
+
return resolveAsync(walkNodes(nodes, { blocks: 0 }), getFragment);
|
|
48
|
+
}
|
|
49
|
+
async function resolveAsync(walk, getFragment) {
|
|
50
|
+
let step = walk.next();
|
|
51
|
+
while (!step.done) {
|
|
52
|
+
let fragment;
|
|
53
|
+
try {
|
|
54
|
+
fragment = await getFragment(step.value);
|
|
55
|
+
}
|
|
56
|
+
catch (cause) {
|
|
57
|
+
throw bindingError(step.value, cause);
|
|
58
|
+
}
|
|
59
|
+
step = walk.next(fragment);
|
|
60
|
+
}
|
|
61
|
+
return step.value;
|
|
62
|
+
}
|
|
63
|
+
function bindingError(id, cause) {
|
|
64
|
+
return new FragmentResolutionError(`ACI fragment ${id} selected binding failed: ${String(cause)}`, { cause });
|
|
65
|
+
}
|
|
66
|
+
function* walkSlots(plan, pageRegions) {
|
|
67
|
+
const resolved = {};
|
|
68
|
+
const state = { blocks: 0 };
|
|
69
|
+
for (const slot of plan.slots) {
|
|
70
|
+
const pageBlocks = pageRegions[slot.name];
|
|
71
|
+
const nodes = pageBlocks?.length ? pageBlocks : plan.defaults?.[slot.name];
|
|
72
|
+
if (nodes?.length)
|
|
73
|
+
resolved[slot.name] = yield* walkNodes(nodes, state);
|
|
24
74
|
}
|
|
25
75
|
return resolved;
|
|
26
76
|
}
|
|
77
|
+
function* walkNodes(nodes, state) {
|
|
78
|
+
const blocks = [];
|
|
79
|
+
for (const node of nodes)
|
|
80
|
+
blocks.push((yield* walkValue(node, [], state)));
|
|
81
|
+
return blocks;
|
|
82
|
+
}
|
|
83
|
+
// Active ancestry belongs to each placement, while the block budget belongs to
|
|
84
|
+
// the whole rendered page. A repeated fragment in another branch is valid.
|
|
85
|
+
function* walkValue(value, ancestry, state) {
|
|
86
|
+
if (Array.isArray(value)) {
|
|
87
|
+
const result = [];
|
|
88
|
+
for (const child of value)
|
|
89
|
+
result.push(yield* walkValue(child, ancestry, state));
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
if (value === null || typeof value !== 'object')
|
|
93
|
+
return value;
|
|
94
|
+
const node = value;
|
|
95
|
+
if (node.kind === 'fragment-ref') {
|
|
96
|
+
const id = node.fragmentId;
|
|
97
|
+
if (typeof id !== 'string' || !id)
|
|
98
|
+
throw new FragmentResolutionError('ACI fragment reference has no fragmentId');
|
|
99
|
+
const chain = [...ancestry, id];
|
|
100
|
+
if (ancestry.includes(id))
|
|
101
|
+
throw new FragmentResolutionError(`ACI fragment cycle: ${chain.join(' -> ')}`);
|
|
102
|
+
if (chain.length > MAX_FRAGMENT_DEPTH)
|
|
103
|
+
throw new FragmentResolutionError(`ACI fragment depth exceeds ${MAX_FRAGMENT_DEPTH}: ${chain.join(' -> ')}`);
|
|
104
|
+
const fragment = yield id;
|
|
105
|
+
const block = (yield* walkValue(fragmentToBlock(fragment, id), chain, state));
|
|
106
|
+
return Object.defineProperty(block, fragmentRootScope, { value: true });
|
|
107
|
+
}
|
|
108
|
+
const isBlock = node.$type === 'component'
|
|
109
|
+
|| (typeof node.component === 'string' && node.component.length > 0
|
|
110
|
+
&& node.props !== null && typeof node.props === 'object' && !Array.isArray(node.props));
|
|
111
|
+
if (isBlock && ++state.blocks > MAX_EXPANDED_BLOCKS) {
|
|
112
|
+
throw new FragmentResolutionError(`ACI rendered page exceeds ${MAX_EXPANDED_BLOCKS} component blocks`);
|
|
113
|
+
}
|
|
114
|
+
const result = {};
|
|
115
|
+
for (const [key, child] of Object.entries(node))
|
|
116
|
+
result[key] = yield* walkValue(child, ancestry, state);
|
|
117
|
+
return preserveFragmentScope(node, result);
|
|
118
|
+
}
|
|
119
|
+
/** Use the same fragment root identity for layout defaults and region placements. */
|
|
120
|
+
export function fragmentToBlock(fragment, id) {
|
|
121
|
+
const block = fragment?.block ?? fragment;
|
|
122
|
+
if (!block || typeof block.component !== 'string' || !block.component
|
|
123
|
+
|| (block.props !== undefined && (typeof block.props !== 'object' || block.props === null || Array.isArray(block.props)))) {
|
|
124
|
+
throw new FragmentResolutionError(`ACI fragment ${id} has no component block`);
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
$type: 'component',
|
|
128
|
+
id: fragment.id ?? fragment.fragmentId ?? id,
|
|
129
|
+
component: block.component,
|
|
130
|
+
props: block.props || {},
|
|
131
|
+
};
|
|
132
|
+
}
|