@gradial/aci 0.1.52-rc.25 → 0.1.52-rc.28
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 -20
- package/README.md +11 -4
- package/dist/astro/index.d.ts +0 -4
- package/dist/astro/index.js +0 -31
- package/dist/astro/page.d.ts +1 -1
- package/dist/astro/page.js +5 -2
- 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 +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 +3 -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/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,25 +1,5 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.52-rc.25 - 2026-09-30
|
|
4
|
-
|
|
5
|
-
### Added
|
|
6
|
-
|
|
7
|
-
- Allow Astro builds to opt into framework-neutral `assets/` output with
|
|
8
|
-
`ACI_ASSET_PATH_LAYOUT=flat-assets`. Next.js rejects that layout until it
|
|
9
|
-
can produce the same public and stored paths safely.
|
|
10
|
-
|
|
11
|
-
## 0.1.52-rc.24 - 2026-09-18
|
|
12
|
-
|
|
13
|
-
### Added
|
|
14
|
-
|
|
15
|
-
- `withAci()` (Astro and Next.js) reads `ACI_ASSET_BASE_TPL` and, when it is
|
|
16
|
-
an absolute CDN URL, sets Vite's `base` / Next.js's `assetPrefix` to it so
|
|
17
|
-
the SSG build writes asset URLs pointing at the CDN directly — no HTML
|
|
18
|
-
post-processing needed. `__RELEASE_ID__` is stripped because build assets
|
|
19
|
-
are content-addressed and release-independent. Managed tenants (the
|
|
20
|
-
`/__r/__RELEASE_ID__/` default) are left untouched; the edge handler
|
|
21
|
-
rewrites relative paths to the global S3 key.
|
|
22
|
-
|
|
23
3
|
## 0.1.52-rc.7 - 2026-08-19
|
|
24
4
|
|
|
25
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.28` 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.28 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.28 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.28 init --list
|
|
140
147
|
```
|
|
141
148
|
|
|
142
149
|
## Personalization: The One-Tag Install
|
package/dist/astro/index.d.ts
CHANGED
|
@@ -4,12 +4,8 @@ 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
|
-
};
|
|
10
7
|
vite?: {
|
|
11
8
|
plugins?: unknown[];
|
|
12
|
-
base?: string;
|
|
13
9
|
};
|
|
14
10
|
}): void;
|
|
15
11
|
}
|
package/dist/astro/index.js
CHANGED
|
@@ -83,37 +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 it as Vite's `base`
|
|
95
|
-
// so the SSG build writes asset URLs (CSS, JS, images) pointing at
|
|
96
|
-
// the CDN directly — no post-processing needed.
|
|
97
|
-
//
|
|
98
|
-
// Build assets are content-addressed (hashed filenames) and stored
|
|
99
|
-
// globally, so the URL is release-independent: strip __RELEASE_ID__.
|
|
100
|
-
// - Managed tenants (edge handler): ACI_ASSET_BASE_TPL is
|
|
101
|
-
// "/__r/__RELEASE_ID__/" or unset — do NOT set Vite base; the
|
|
102
|
-
// SSG writes relative paths (/_astro/foo.js) and the edge
|
|
103
|
-
// handler's viewer-request rewrites to the global S3 key.
|
|
104
|
-
// - External CDN tenants: "https://cdn.gradial.com/" → Vite base,
|
|
105
|
-
// Astro writes absolute CDN URLs.
|
|
106
|
-
const rawTpl = process.env.ACI_ASSET_BASE_TPL || '';
|
|
107
|
-
const assetBase = rawTpl
|
|
108
|
-
.replace('__RELEASE_ID__/', '')
|
|
109
|
-
.replace('__RELEASE_ID__', '')
|
|
110
|
-
.replace(/\/+$/, '/');
|
|
111
|
-
const isAbsoluteUrl = assetBase.startsWith('http://') || assetBase.startsWith('https://');
|
|
112
|
-
if (isAbsoluteUrl
|
|
113
|
-
&& assetBase !== '/'
|
|
114
|
-
&& process.env.NODE_ENV === 'production') {
|
|
115
|
-
updateConfig({ vite: { base: assetBase } });
|
|
116
|
-
}
|
|
117
86
|
if (process.env.NODE_ENV === 'production') {
|
|
118
87
|
return;
|
|
119
88
|
}
|
package/dist/astro/page.d.ts
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
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.
|
package/dist/astro/page.js
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
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';
|
|
@@ -87,8 +88,10 @@ export async function getPageData(astro, _options) {
|
|
|
87
88
|
const layout = await resolveLayout(runtimeInput, provider, providerModule, page.layout);
|
|
88
89
|
// Resolve slots: page regions filled, then layout fragment defaults
|
|
89
90
|
const slots = await resolveSlots(layout, page?.regions || {}, async (id) => {
|
|
90
|
-
if (runtimeInput
|
|
91
|
-
|
|
91
|
+
if (runtimeInput) {
|
|
92
|
+
if (Object.hasOwn(runtimeInput.fragments ?? {}, id))
|
|
93
|
+
return runtimeInput.fragments[id];
|
|
94
|
+
throw new FragmentNotFoundError(id, new Error('fragment is not bound by the selected page'));
|
|
92
95
|
}
|
|
93
96
|
const p = provider ?? new (await import('../providers/file.js')).FileContentProvider();
|
|
94
97
|
return await p.getFragment(id);
|
|
@@ -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';
|
|
@@ -25,6 +25,11 @@ export interface LayoutVariantEntry {
|
|
|
25
25
|
slots: LayoutSlot[];
|
|
26
26
|
defaults?: Record<string, FragmentRef[]>;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Derived template render plan in the compiled release wire format.
|
|
30
|
+
* This is never an authorable layout: page templates own regions and defaults.
|
|
31
|
+
* The LayoutEntry name and layouts wire key remain for retained release readers.
|
|
32
|
+
*/
|
|
28
33
|
export interface LayoutEntry {
|
|
29
34
|
name: string;
|
|
30
35
|
digest?: string;
|
|
@@ -40,7 +45,6 @@ export interface LayoutSlot {
|
|
|
40
45
|
export interface FragmentRef {
|
|
41
46
|
kind: string;
|
|
42
47
|
fragmentId: string;
|
|
43
|
-
inline: boolean;
|
|
44
48
|
}
|
|
45
49
|
export interface CompiledManifest {
|
|
46
50
|
manifestVersion: string;
|
|
@@ -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
|
+
}
|
package/dist/content/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExperimentAssignmentContext, SitePersonalizationConfig } from './personalization.js';
|
|
2
|
+
import type { FragmentRefNode } from '../types/page.js';
|
|
2
3
|
export type ButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'outline' | 'ghost';
|
|
3
4
|
export interface KernelImage {
|
|
4
5
|
src: string;
|
|
@@ -47,6 +48,7 @@ export interface KernelPageBlock {
|
|
|
47
48
|
component: string;
|
|
48
49
|
props: Record<string, unknown>;
|
|
49
50
|
}
|
|
51
|
+
export type KernelPageNode = KernelPageBlock | FragmentRefNode;
|
|
50
52
|
export interface KernelAtomicBlock {
|
|
51
53
|
$type: 'component';
|
|
52
54
|
id: string;
|
|
@@ -59,7 +61,7 @@ export interface KernelPage {
|
|
|
59
61
|
status: string;
|
|
60
62
|
layout: string;
|
|
61
63
|
metadata: KernelPageSeo;
|
|
62
|
-
regions: Record<string,
|
|
64
|
+
regions: Record<string, KernelPageNode[]>;
|
|
63
65
|
/**
|
|
64
66
|
* Build-time experiment assignment context (personalization-reporting
|
|
65
67
|
* design/04); present only on experiment variant payloads. The starters
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export { defineComponentContract } from './define-component.js';
|
|
2
|
-
export { defineLayoutContract, slot, fragmentDefault } from './define-layout.js';
|
|
3
2
|
export { createRegistry, registryLookup } from './registry.js';
|
|
4
3
|
export type { Registry, RegistryEntry, AnyComponent } from './registry.js';
|
|
5
4
|
export { blockRef, blockRefArray } from './types/block-ref.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export { defineComponentContract } from './define-component.js';
|
|
2
|
-
export { defineLayoutContract, slot, fragmentDefault } from './define-layout.js';
|
|
3
2
|
export { createRegistry, registryLookup } from './registry.js';
|
|
4
3
|
export { blockRef, blockRefArray } from './types/block-ref.js';
|
|
5
4
|
export { ImageSchema, imageAttrs, primaryImageSource, } from './types/image.js';
|
package/dist/mintlify/blocks.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fragmentRootScope, preserveFragmentScope } from '../content/resolve-slots.js';
|
|
1
2
|
import { cleanFilePath, isMintlifyJSONValue, isObject, nonEmptyString } from './json.js';
|
|
2
3
|
const GENERATED_MARKER = '{/* Generated by @gradial/aci; edit .content instead. */}';
|
|
3
4
|
const SUPPORTED_COMPONENTS = new Set([
|
|
@@ -92,6 +93,8 @@ export function validateBlock(value, label, ids) {
|
|
|
92
93
|
if (!isObject(value) || value.$type !== 'component' || !nonEmptyString(value.id) || !nonEmptyString(value.component) || !isObject(value.props)) {
|
|
93
94
|
throw new Error(`ACI Mintlify block is invalid: ${label}`);
|
|
94
95
|
}
|
|
96
|
+
if (Object.hasOwn(value, fragmentRootScope))
|
|
97
|
+
ids = new Set();
|
|
95
98
|
if (ids.has(value.id))
|
|
96
99
|
throw new Error(`ACI Mintlify block ID is duplicated: ${value.id}`);
|
|
97
100
|
ids.add(value.id);
|
|
@@ -112,7 +115,7 @@ export function validateBlock(value, label, ids) {
|
|
|
112
115
|
}
|
|
113
116
|
}
|
|
114
117
|
validateComponentProps(value.component, props, label);
|
|
115
|
-
return { $type: 'component', id: value.id, component: value.component, props };
|
|
118
|
+
return preserveFragmentScope(value, { $type: 'component', id: value.id, component: value.component, props });
|
|
116
119
|
}
|
|
117
120
|
function validateComponentProps(component, props, label) {
|
|
118
121
|
if (component === 'mintlify.markdown' || component === 'mintlify.mdx') {
|
package/dist/mintlify/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { materializeMintlifySite, loadMintlifySource, loadCompiledMintlifySource, } from './materialize.js';
|
|
1
|
+
export { materializeMintlifySite, resolveMintlifyDocument, loadMintlifySource, loadCompiledMintlifySource, } from './materialize.js';
|
|
2
2
|
export { renderDocument, renderSnippet } from './blocks.js';
|
|
3
3
|
export type { MintlifyAPISpec, MintlifyBlock, MintlifyConfigFragment, MintlifyDocument, MintlifyJSONValue, MintlifyNavigationGroup, MintlifySite, MintlifySnippet, MintlifySource, } from './types.js';
|
package/dist/mintlify/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { materializeMintlifySite, loadMintlifySource, loadCompiledMintlifySource, } from './materialize.js';
|
|
1
|
+
export { materializeMintlifySite, resolveMintlifyDocument, loadMintlifySource, loadCompiledMintlifySource, } from './materialize.js';
|
|
2
2
|
export { renderDocument, renderSnippet } from './blocks.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MaterializeMintlifySiteOptions, MintlifySource } from './types.js';
|
|
1
|
+
import type { MaterializeMintlifySiteOptions, MintlifyDocument, MintlifySource } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Reads a site and its documents from ACI's portable JSON content tree.
|
|
4
4
|
* The source shape is standard ACI content: `config/mintlify.json`, resources
|
|
@@ -10,3 +10,5 @@ export declare function loadMintlifySource(sourceDir: string): Promise<MintlifyS
|
|
|
10
10
|
export declare function loadCompiledMintlifySource(sourceDir: string, compiledDir: string): Promise<MintlifySource>;
|
|
11
11
|
/** Materializes one exact Mintlify site from ACI source content. */
|
|
12
12
|
export declare function materializeMintlifySite(options: MaterializeMintlifySiteOptions): Promise<void>;
|
|
13
|
+
/** Project a runtime page using only its selected, immutable fragment bindings. */
|
|
14
|
+
export declare function resolveMintlifyDocument(value: unknown, fragments: Record<string, unknown>, documentPath: string, layout: unknown): Promise<MintlifyDocument>;
|