@gradial/aci 0.1.52-rc.21 → 0.1.52-rc.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -4
- package/dist/cli/mintlify-render.d.mts +1 -0
- package/dist/cli/mintlify-render.mjs +8 -0
- package/dist/mintlify/blocks.d.ts +6 -0
- package/dist/mintlify/blocks.js +198 -0
- package/dist/mintlify/config.d.ts +3 -0
- package/dist/mintlify/config.js +77 -0
- package/dist/mintlify/index.d.ts +3 -0
- package/dist/mintlify/index.js +2 -0
- package/dist/mintlify/json.d.ts +8 -0
- package/dist/mintlify/json.js +37 -0
- package/dist/mintlify/materialize.d.ts +12 -0
- package/dist/mintlify/materialize.js +159 -0
- package/dist/mintlify/types.d.ts +69 -0
- package/dist/mintlify/types.js +1 -0
- package/dist/mintlify/validate.d.ts +7 -0
- package/dist/mintlify/validate.js +252 -0
- package/dist/react/VideoPlayer.d.ts +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/testing/index.js +2 -2
- package/package.json +11 -7
- package/src/cli/mintlify-render.mjs +9 -0
- package/templates/astro/starter-content/src/fixtures/sections.ts +1 -1
- package/templates/common/.agents/skills/aci/SKILL.md +50 -0
- package/templates/common/.agents/skills/authoring/SKILL.md +4 -3
- package/templates/common/.agents/skills/migration/references/selector-map.md +1 -1
- package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000101}/asset.json +1 -1
- package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000102}/asset.json +1 -1
- package/templates/common/starter-content/.content/pages/404/_index.json +1 -1
- package/templates/common/starter-content/.content/pages/{home/_index.json → _index.json} +2 -2
- package/templates/common/starter-content/.content/pages/about/_index.json +1 -1
- package/templates/common/starter-content/.content/pages/error/_index.json +1 -1
- package/templates/common/starter-content/.content/patterns/components/button.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/container.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/feature_card.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/feature_grid.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/home_hero.json +2 -2
- 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/common/starter-content/.content/patterns/layouts/default.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/personalization_choice.json +0 -42
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/editorial.json +0 -0
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000101}/governance.json +0 -0
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/versions/v0001.json +0 -0
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/editorial.json +0 -0
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000102}/governance.json +0 -0
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/versions/v0001.json +0 -0
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.22` 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.22 zod@^4
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
## Root Import
|
|
@@ -62,6 +62,36 @@ export default withAci(nextConfig, {
|
|
|
62
62
|
|
|
63
63
|
Astro public subpath is `@gradial/aci/astro`.
|
|
64
64
|
|
|
65
|
+
## Mintlify
|
|
66
|
+
|
|
67
|
+
`@gradial/aci/mintlify` is the Mintlify adapter used automatically by the ACI
|
|
68
|
+
CLI. Mintlify remains the renderer; ACI owns the standard `.content` tree.
|
|
69
|
+
Documents use stable ACI component IDs and an
|
|
70
|
+
ordered recursive block tree: Markdown slices for prose, typed nested Mintlify
|
|
71
|
+
components and stable accessibility-safe anchors for common structures, and
|
|
72
|
+
explicit `mintlify.mdx` nodes for native syntax that has not been promoted to a
|
|
73
|
+
typed contract.
|
|
74
|
+
|
|
75
|
+
The source tree may also contain independently governed snippets, recursive
|
|
76
|
+
`docs.json` `$ref` fragments, and OpenAPI, AsyncAPI, or GraphQL descriptions:
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
.content/
|
|
80
|
+
config/site.json
|
|
81
|
+
config/mintlify.json
|
|
82
|
+
pages/**/_index.json
|
|
83
|
+
fragments/**/_index.json
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
aci content compile
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
See `starters/mintlify/` for the smallest runnable site. `.aci.yaml` selects
|
|
91
|
+
`framework: mintlify`; no renderer or validation flag is needed. The command
|
|
92
|
+
validates the graph and writes the native project to
|
|
93
|
+
`.aci/compiled/renderers/mintlify/` beside the ordinary compiled manifest.
|
|
94
|
+
|
|
65
95
|
## Build-Time Content Provider
|
|
66
96
|
|
|
67
97
|
The file provider reads compiler output while generating complete static pages:
|
|
@@ -78,13 +108,13 @@ host bundles select and serve immutable release artifacts.
|
|
|
78
108
|
The first RC scaffold path is Next:
|
|
79
109
|
|
|
80
110
|
```bash
|
|
81
|
-
npx @gradial/aci@0.1.52-rc.
|
|
111
|
+
npx @gradial/aci@0.1.52-rc.22 init my-site --template next-starter
|
|
82
112
|
```
|
|
83
113
|
|
|
84
114
|
List available templates:
|
|
85
115
|
|
|
86
116
|
```bash
|
|
87
|
-
npx @gradial/aci@0.1.52-rc.
|
|
117
|
+
npx @gradial/aci@0.1.52-rc.22 init --list
|
|
88
118
|
```
|
|
89
119
|
|
|
90
120
|
## Personalization: The One-Tag Install
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { loadCompiledMintlifySource, materializeMintlifySite } from '../../dist/mintlify/index.js';
|
|
3
|
+
const [sourceArg, compiledArg, outputArg] = process.argv.slice(2);
|
|
4
|
+
if (!sourceArg || !compiledArg || !outputArg) {
|
|
5
|
+
throw new Error('Mintlify renderer requires content, compiled, and output directories');
|
|
6
|
+
}
|
|
7
|
+
const source = await loadCompiledMintlifySource(path.resolve(sourceArg), path.resolve(compiledArg));
|
|
8
|
+
await materializeMintlifySite({ ...source, outputDir: path.resolve(outputArg) });
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { MintlifyBlock, MintlifyDocument, MintlifySnippet } from './types.js';
|
|
2
|
+
export declare function renderDocument(document: MintlifyDocument): string;
|
|
3
|
+
export declare function renderSnippet(snippet: MintlifySnippet): string;
|
|
4
|
+
export declare function validateBlock(value: unknown, label: string, ids: Set<string>): MintlifyBlock;
|
|
5
|
+
export declare function visitBlocks(blocks: MintlifyBlock[], visit: (block: MintlifyBlock) => void): void;
|
|
6
|
+
export declare function validateSnippetReferences(documents: MintlifyDocument[], snippets: MintlifySnippet[]): void;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { cleanFilePath, isMintlifyJSONValue, isObject, nonEmptyString } from './json.js';
|
|
2
|
+
const GENERATED_MARKER = '{/* Generated by @gradial/aci; edit .content instead. */}';
|
|
3
|
+
const SUPPORTED_COMPONENTS = new Set([
|
|
4
|
+
'mintlify.markdown', 'mintlify.mdx', 'mintlify.anchor', 'mintlify.steps', 'mintlify.step',
|
|
5
|
+
'mintlify.card-group', 'mintlify.card', 'mintlify.accordion-group',
|
|
6
|
+
'mintlify.accordion', 'mintlify.callout', 'mintlify.tabs', 'mintlify.tab',
|
|
7
|
+
'mintlify.snippet-ref',
|
|
8
|
+
]);
|
|
9
|
+
export function renderDocument(document) {
|
|
10
|
+
const frontmatter = [`title: ${JSON.stringify(document.metadata.title)}`];
|
|
11
|
+
if (document.metadata.description)
|
|
12
|
+
frontmatter.push(`description: ${JSON.stringify(document.metadata.description)}`);
|
|
13
|
+
if (document.metadata.mintlify?.sidebarTitle)
|
|
14
|
+
frontmatter.push(`sidebarTitle: ${JSON.stringify(document.metadata.mintlify.sidebarTitle)}`);
|
|
15
|
+
if (document.metadata.mintlify?.icon)
|
|
16
|
+
frontmatter.push(`icon: ${JSON.stringify(document.metadata.mintlify.icon)}`);
|
|
17
|
+
for (const [key, value] of Object.entries(document.metadata.mintlify?.frontmatter ?? {}).sort(([left], [right]) => left.localeCompare(right))) {
|
|
18
|
+
frontmatter.push(`${key}: ${JSON.stringify(value)}`);
|
|
19
|
+
}
|
|
20
|
+
return `---\n${frontmatter.join('\n')}\n---\n\n${GENERATED_MARKER}\n\n${renderContent(document.regions.main)}`;
|
|
21
|
+
}
|
|
22
|
+
export function renderSnippet(snippet) {
|
|
23
|
+
return `${GENERATED_MARKER}\n\n${renderContent([snippet.block])}`;
|
|
24
|
+
}
|
|
25
|
+
function renderContent(blocks) {
|
|
26
|
+
const aliases = snippetAliases(blocks);
|
|
27
|
+
const imports = [...aliases].map(([snippetPath, alias]) => `import ${alias} from "/${snippetPath}";`).join('\n');
|
|
28
|
+
const body = renderBlocks(blocks, 0, aliases).replace(/\n*$/, '\n');
|
|
29
|
+
return imports ? `${imports}\n\n${body}` : body;
|
|
30
|
+
}
|
|
31
|
+
function renderBlocks(blocks, indentation, aliases) {
|
|
32
|
+
return blocks.map((block) => renderBlock(block, indentation, aliases)).join('\n\n');
|
|
33
|
+
}
|
|
34
|
+
function renderBlock(block, indentation, aliases) {
|
|
35
|
+
const prefix = ' '.repeat(indentation);
|
|
36
|
+
if (block.component === 'mintlify.markdown' || block.component === 'mintlify.mdx')
|
|
37
|
+
return indentSource(block.props.source, indentation);
|
|
38
|
+
if (block.component === 'mintlify.anchor')
|
|
39
|
+
return `${prefix}<span id="${escapeAttribute(block.props.name)}"></span>`;
|
|
40
|
+
if (block.component === 'mintlify.snippet-ref') {
|
|
41
|
+
const snippetPath = block.props.path;
|
|
42
|
+
const alias = aliases.get(snippetPath);
|
|
43
|
+
const values = block.props.values;
|
|
44
|
+
const attributes = Object.entries(values).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => renderAttribute(key, value)).join('');
|
|
45
|
+
return `${prefix}<${alias}${attributes} />`;
|
|
46
|
+
}
|
|
47
|
+
const children = block.props.children ?? [];
|
|
48
|
+
const tag = nativeComponentName(block);
|
|
49
|
+
const attributes = Object.entries(block.props)
|
|
50
|
+
.filter(([key]) => key !== 'children' && key !== 'variant')
|
|
51
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
52
|
+
.map(([key, value]) => renderAttribute(key, value))
|
|
53
|
+
.join('');
|
|
54
|
+
if (children.length === 0)
|
|
55
|
+
return `${prefix}<${tag}${attributes}>\n${prefix}</${tag}>`;
|
|
56
|
+
return `${prefix}<${tag}${attributes}>\n${renderBlocks(children, indentation + 2, aliases)}\n${prefix}</${tag}>`;
|
|
57
|
+
}
|
|
58
|
+
function snippetAliases(blocks) {
|
|
59
|
+
const aliases = new Map();
|
|
60
|
+
visitBlocks(blocks, (block) => {
|
|
61
|
+
if (block.component !== 'mintlify.snippet-ref')
|
|
62
|
+
return;
|
|
63
|
+
const snippetPath = block.props.path;
|
|
64
|
+
if (!aliases.has(snippetPath))
|
|
65
|
+
aliases.set(snippetPath, `AciSnippet${aliases.size + 1}`);
|
|
66
|
+
});
|
|
67
|
+
return aliases;
|
|
68
|
+
}
|
|
69
|
+
function nativeComponentName(block) {
|
|
70
|
+
if (block.component === 'mintlify.callout') {
|
|
71
|
+
const variant = block.props.variant;
|
|
72
|
+
return variant[0].toUpperCase() + variant.slice(1);
|
|
73
|
+
}
|
|
74
|
+
return block.component.slice('mintlify.'.length).split('-').map((part) => part[0].toUpperCase() + part.slice(1)).join('');
|
|
75
|
+
}
|
|
76
|
+
function renderAttribute(key, value) {
|
|
77
|
+
if (value === true)
|
|
78
|
+
return ` ${key}`;
|
|
79
|
+
if (typeof value === 'string')
|
|
80
|
+
return ` ${key}="${escapeAttribute(value)}"`;
|
|
81
|
+
return ` ${key}={${JSON.stringify(value)}}`;
|
|
82
|
+
}
|
|
83
|
+
function escapeAttribute(value) {
|
|
84
|
+
return value.replaceAll('&', '&').replaceAll('"', '"').replaceAll('<', '<').replaceAll('>', '>');
|
|
85
|
+
}
|
|
86
|
+
function indentSource(source, indentation) {
|
|
87
|
+
const normalized = source.replace(/\r\n/g, '\n').replace(/^\n+|\n+$/g, '');
|
|
88
|
+
const prefix = ' '.repeat(indentation);
|
|
89
|
+
return normalized.split('\n').map((line) => line.length > 0 ? `${prefix}${line}` : '').join('\n');
|
|
90
|
+
}
|
|
91
|
+
export function validateBlock(value, label, ids) {
|
|
92
|
+
if (!isObject(value) || value.$type !== 'component' || !nonEmptyString(value.id) || !nonEmptyString(value.component) || !isObject(value.props)) {
|
|
93
|
+
throw new Error(`ACI Mintlify block is invalid: ${label}`);
|
|
94
|
+
}
|
|
95
|
+
if (ids.has(value.id))
|
|
96
|
+
throw new Error(`ACI Mintlify block ID is duplicated: ${value.id}`);
|
|
97
|
+
ids.add(value.id);
|
|
98
|
+
if (!SUPPORTED_COMPONENTS.has(value.component))
|
|
99
|
+
throw new Error(`ACI Mintlify component is unsupported: ${value.component}`);
|
|
100
|
+
const props = {};
|
|
101
|
+
for (const [key, entry] of Object.entries(value.props)) {
|
|
102
|
+
if (key === 'children') {
|
|
103
|
+
if (!Array.isArray(entry))
|
|
104
|
+
throw new Error(`ACI Mintlify block children are invalid: ${label}`);
|
|
105
|
+
props.children = entry.map((child, index) => validateBlock(child, `${label}.props.children[${index}]`, ids));
|
|
106
|
+
}
|
|
107
|
+
else if (!isMintlifyJSONValue(entry)) {
|
|
108
|
+
throw new Error(`ACI Mintlify block property is invalid: ${label}.${key}`);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
props[key] = entry;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
validateComponentProps(value.component, props, label);
|
|
115
|
+
return { $type: 'component', id: value.id, component: value.component, props };
|
|
116
|
+
}
|
|
117
|
+
function validateComponentProps(component, props, label) {
|
|
118
|
+
if (component === 'mintlify.markdown' || component === 'mintlify.mdx') {
|
|
119
|
+
if (typeof props.source !== 'string' || props.children !== undefined)
|
|
120
|
+
throw new Error(`ACI Mintlify source block is invalid: ${label}`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (component === 'mintlify.anchor') {
|
|
124
|
+
if (typeof props.name !== 'string' || !/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(props.name) || Object.keys(props).some((key) => key !== 'name')) {
|
|
125
|
+
throw new Error(`ACI Mintlify anchor is invalid: ${label}`);
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (component === 'mintlify.snippet-ref') {
|
|
130
|
+
if (!nonEmptyString(props.path) || cleanFilePath(props.path, '.mdx') !== props.path || !isObject(props.values) || !isMintlifyJSONValue(props.values) || props.children !== undefined) {
|
|
131
|
+
throw new Error(`ACI Mintlify snippet reference is invalid: ${label}`);
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (!Array.isArray(props.children))
|
|
136
|
+
throw new Error(`ACI Mintlify component children are required: ${label}`);
|
|
137
|
+
if (component === 'mintlify.callout' && !['info', 'warning', 'note', 'tip', 'check'].includes(String(props.variant))) {
|
|
138
|
+
throw new Error(`ACI Mintlify callout variant is invalid: ${label}`);
|
|
139
|
+
}
|
|
140
|
+
if (['mintlify.step', 'mintlify.card', 'mintlify.accordion', 'mintlify.tab'].includes(component) && !nonEmptyString(props.title)) {
|
|
141
|
+
throw new Error(`ACI Mintlify component title is required: ${label}`);
|
|
142
|
+
}
|
|
143
|
+
const expectedChild = component === 'mintlify.steps' ? 'mintlify.step'
|
|
144
|
+
: component === 'mintlify.card-group' ? 'mintlify.card'
|
|
145
|
+
: component === 'mintlify.accordion-group' ? 'mintlify.accordion'
|
|
146
|
+
: component === 'mintlify.tabs' ? 'mintlify.tab'
|
|
147
|
+
: undefined;
|
|
148
|
+
if (expectedChild && props.children?.some((child) => child.component !== expectedChild))
|
|
149
|
+
throw new Error(`ACI Mintlify ${component} child is invalid: ${label}`);
|
|
150
|
+
}
|
|
151
|
+
export function visitBlocks(blocks, visit) {
|
|
152
|
+
for (const block of blocks) {
|
|
153
|
+
visit(block);
|
|
154
|
+
if (block.props.children)
|
|
155
|
+
visitBlocks(block.props.children, visit);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function snippetReferences(blocks) {
|
|
159
|
+
const references = [];
|
|
160
|
+
visitBlocks(blocks, (block) => {
|
|
161
|
+
if (block.component === 'mintlify.snippet-ref')
|
|
162
|
+
references.push(block.props.path);
|
|
163
|
+
});
|
|
164
|
+
return references;
|
|
165
|
+
}
|
|
166
|
+
export function validateSnippetReferences(documents, snippets) {
|
|
167
|
+
const byPath = new Map();
|
|
168
|
+
for (const snippet of snippets) {
|
|
169
|
+
if (byPath.has(snippet.path))
|
|
170
|
+
throw new Error(`ACI Mintlify snippet path is duplicated: ${snippet.path}`);
|
|
171
|
+
byPath.set(snippet.path, snippet);
|
|
172
|
+
}
|
|
173
|
+
for (const document of documents) {
|
|
174
|
+
for (const reference of snippetReferences(document.regions.main)) {
|
|
175
|
+
if (!byPath.has(reference))
|
|
176
|
+
throw new Error(`ACI Mintlify snippet reference is missing: ${reference}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const visited = new Set();
|
|
180
|
+
const visiting = new Set();
|
|
181
|
+
const walk = (snippetPath) => {
|
|
182
|
+
if (visited.has(snippetPath))
|
|
183
|
+
return;
|
|
184
|
+
if (visiting.has(snippetPath))
|
|
185
|
+
throw new Error(`ACI Mintlify snippet reference cycle includes: ${snippetPath}`);
|
|
186
|
+
visiting.add(snippetPath);
|
|
187
|
+
const snippet = byPath.get(snippetPath);
|
|
188
|
+
for (const reference of snippetReferences([snippet.block])) {
|
|
189
|
+
if (!byPath.has(reference))
|
|
190
|
+
throw new Error(`ACI Mintlify snippet reference is missing: ${reference}`);
|
|
191
|
+
walk(reference);
|
|
192
|
+
}
|
|
193
|
+
visiting.delete(snippetPath);
|
|
194
|
+
visited.add(snippetPath);
|
|
195
|
+
};
|
|
196
|
+
for (const snippetPath of byPath.keys())
|
|
197
|
+
walk(snippetPath);
|
|
198
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { MintlifyConfigFragment, MintlifyJSONValue, MintlifySite } from './types.js';
|
|
2
|
+
export declare function toDocsJSON(site: MintlifySite): Record<string, unknown>;
|
|
3
|
+
export declare function resolveConfigReferences(configuration: MintlifyJSONValue, fragments: MintlifyConfigFragment[]): MintlifyJSONValue;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { canonicalize, isExternalReference, isObject } from './json.js';
|
|
3
|
+
export function toDocsJSON(site) {
|
|
4
|
+
const { $schema: _schema, ...configuration } = site;
|
|
5
|
+
return canonicalize({
|
|
6
|
+
$schema: 'https://mintlify.com/docs.json',
|
|
7
|
+
...(typeof configuration.theme === 'string' ? {} : { theme: 'mint' }),
|
|
8
|
+
...configuration,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
export function resolveConfigReferences(configuration, fragments) {
|
|
12
|
+
const byPath = new Map();
|
|
13
|
+
for (const fragment of fragments) {
|
|
14
|
+
if (byPath.has(fragment.path))
|
|
15
|
+
throw new Error(`ACI Mintlify config fragment path is duplicated: ${fragment.path}`);
|
|
16
|
+
byPath.set(fragment.path, fragment.value);
|
|
17
|
+
}
|
|
18
|
+
return resolveConfigValue(configuration, 'docs.json', byPath, new Set());
|
|
19
|
+
}
|
|
20
|
+
function resolveConfigValue(value, currentPath, fragments, stack) {
|
|
21
|
+
if (Array.isArray(value))
|
|
22
|
+
return value.map((entry) => resolveConfigValue(entry, currentPath, fragments, stack));
|
|
23
|
+
if (!isObject(value))
|
|
24
|
+
return value;
|
|
25
|
+
if (typeof value.$ref === 'string' && !isExternalReference(value.$ref)) {
|
|
26
|
+
const reference = resolveConfigReferencePath(currentPath, value.$ref);
|
|
27
|
+
const target = fragments.get(reference.path);
|
|
28
|
+
if (target === undefined)
|
|
29
|
+
throw new Error(`ACI Mintlify config reference is missing: ${value.$ref} from ${currentPath}`);
|
|
30
|
+
const cycleKey = `${reference.path}#${reference.pointer.join('/')}`;
|
|
31
|
+
if (stack.has(cycleKey))
|
|
32
|
+
throw new Error(`ACI Mintlify config reference cycle includes: ${cycleKey}`);
|
|
33
|
+
const nextStack = new Set(stack).add(cycleKey);
|
|
34
|
+
const selected = resolveJSONPointer(target, reference.pointer, value.$ref);
|
|
35
|
+
const resolvedTarget = resolveConfigValue(selected, reference.path, fragments, nextStack);
|
|
36
|
+
const siblings = Object.fromEntries(Object.entries(value)
|
|
37
|
+
.filter(([key]) => key !== '$ref')
|
|
38
|
+
.map(([key, child]) => [key, resolveConfigValue(child, currentPath, fragments, stack)]));
|
|
39
|
+
if (Object.keys(siblings).length === 0)
|
|
40
|
+
return resolvedTarget;
|
|
41
|
+
if (!isObject(resolvedTarget))
|
|
42
|
+
throw new Error(`ACI Mintlify config reference with sibling keys must resolve to an object: ${value.$ref}`);
|
|
43
|
+
return { ...resolvedTarget, ...siblings };
|
|
44
|
+
}
|
|
45
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, resolveConfigValue(child, currentPath, fragments, stack)]));
|
|
46
|
+
}
|
|
47
|
+
function resolveConfigReferencePath(currentPath, reference) {
|
|
48
|
+
const [rawPath, rawPointer = ''] = reference.split('#', 2);
|
|
49
|
+
if (!rawPath)
|
|
50
|
+
throw new Error(`ACI Mintlify local config reference requires a file path: ${reference}`);
|
|
51
|
+
const resolved = path.posix.normalize(path.posix.join(path.posix.dirname(currentPath), rawPath));
|
|
52
|
+
if (resolved.startsWith('../') || resolved.startsWith('/') || !resolved.endsWith('.json')) {
|
|
53
|
+
throw new Error(`ACI Mintlify config reference path is invalid: ${reference}`);
|
|
54
|
+
}
|
|
55
|
+
const pointer = rawPointer === '' ? [] : rawPointer.startsWith('/')
|
|
56
|
+
? rawPointer.slice(1).split('/').map((part) => part.replaceAll('~1', '/').replaceAll('~0', '~'))
|
|
57
|
+
: (() => { throw new Error(`ACI Mintlify config reference pointer is invalid: ${reference}`); })();
|
|
58
|
+
return { path: resolved, pointer };
|
|
59
|
+
}
|
|
60
|
+
function resolveJSONPointer(value, pointer, reference) {
|
|
61
|
+
let current = value;
|
|
62
|
+
for (const part of pointer) {
|
|
63
|
+
if (Array.isArray(current)) {
|
|
64
|
+
const index = Number(part);
|
|
65
|
+
if (!Number.isInteger(index) || index < 0 || index >= current.length)
|
|
66
|
+
throw new Error(`ACI Mintlify config reference pointer is missing: ${reference}`);
|
|
67
|
+
current = current[index];
|
|
68
|
+
}
|
|
69
|
+
else if (isObject(current) && Object.hasOwn(current, part)) {
|
|
70
|
+
current = current[part];
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
throw new Error(`ACI Mintlify config reference pointer is missing: ${reference}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return current;
|
|
77
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { materializeMintlifySite, loadMintlifySource, loadCompiledMintlifySource, } from './materialize.js';
|
|
2
|
+
export { renderDocument, renderSnippet } from './blocks.js';
|
|
3
|
+
export type { MintlifyAPISpec, MintlifyBlock, MintlifyConfigFragment, MintlifyDocument, MintlifyJSONValue, MintlifyNavigationGroup, MintlifySite, MintlifySnippet, MintlifySource, } from './types.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { MintlifyJSONValue } from './types.js';
|
|
2
|
+
export declare function isObject(value: unknown): value is Record<string, unknown>;
|
|
3
|
+
export declare function nonEmptyString(value: unknown): value is string;
|
|
4
|
+
export declare function isMintlifyJSONValue(value: unknown): value is MintlifyJSONValue;
|
|
5
|
+
export declare function canonicalize(value: MintlifyJSONValue): MintlifyJSONValue;
|
|
6
|
+
export declare function isExternalReference(value: string): boolean;
|
|
7
|
+
export declare function cleanDocumentPath(value: string): string;
|
|
8
|
+
export declare function cleanFilePath(value: string, extension: string): string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export function isObject(value) {
|
|
2
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
export function nonEmptyString(value) {
|
|
5
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
6
|
+
}
|
|
7
|
+
export function isMintlifyJSONValue(value) {
|
|
8
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
9
|
+
return true;
|
|
10
|
+
if (typeof value === 'number')
|
|
11
|
+
return Number.isFinite(value);
|
|
12
|
+
if (Array.isArray(value))
|
|
13
|
+
return value.every(isMintlifyJSONValue);
|
|
14
|
+
return isObject(value) && Object.values(value).every(isMintlifyJSONValue);
|
|
15
|
+
}
|
|
16
|
+
export function canonicalize(value) {
|
|
17
|
+
if (Array.isArray(value))
|
|
18
|
+
return value.map(canonicalize);
|
|
19
|
+
if (!isObject(value))
|
|
20
|
+
return value;
|
|
21
|
+
return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, child]) => [key, canonicalize(child)]));
|
|
22
|
+
}
|
|
23
|
+
export function isExternalReference(value) {
|
|
24
|
+
return /^(?:https?:)?\/\//i.test(value) || value.startsWith('mailto:');
|
|
25
|
+
}
|
|
26
|
+
export function cleanDocumentPath(value) {
|
|
27
|
+
const normalized = value.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
|
28
|
+
if (!normalized || normalized.endsWith('.md') || normalized.endsWith('.mdx') || normalized.split('/').some((part) => !part || part === '.' || part === '..'))
|
|
29
|
+
return '';
|
|
30
|
+
return normalized;
|
|
31
|
+
}
|
|
32
|
+
export function cleanFilePath(value, extension) {
|
|
33
|
+
const normalized = value.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
|
34
|
+
if (!normalized.endsWith(extension) || normalized.split('/').some((part) => !part || part === '.' || part === '..'))
|
|
35
|
+
return '';
|
|
36
|
+
return normalized;
|
|
37
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { MaterializeMintlifySiteOptions, MintlifySource } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Reads a site and its documents from ACI's portable JSON content tree.
|
|
4
|
+
* The source shape is standard ACI content: `config/mintlify.json`, resources
|
|
5
|
+
* below `pages/` and `fragments/`; the generated Mintlify tree is not
|
|
6
|
+
* source content and must live outside this directory.
|
|
7
|
+
*/
|
|
8
|
+
export declare function loadMintlifySource(sourceDir: string): Promise<MintlifySource>;
|
|
9
|
+
/** Loads resolved pages/fragments from ACI compiler output for framework projection. */
|
|
10
|
+
export declare function loadCompiledMintlifySource(sourceDir: string, compiledDir: string): Promise<MintlifySource>;
|
|
11
|
+
/** Materializes one exact Mintlify site from ACI source content. */
|
|
12
|
+
export declare function materializeMintlifySite(options: MaterializeMintlifySiteOptions): Promise<void>;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, writeFile, } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { resolveConfigReferences, toDocsJSON } from './config.js';
|
|
4
|
+
import { renderDocument, renderSnippet } from './blocks.js';
|
|
5
|
+
import { canonicalize, isObject, } from './json.js';
|
|
6
|
+
import { validateAPISpec, validateConfigFragment, validateDocument, validateSite, validateSnippet, validateSource, } from './validate.js';
|
|
7
|
+
/**
|
|
8
|
+
* Reads a site and its documents from ACI's portable JSON content tree.
|
|
9
|
+
* The source shape is standard ACI content: `config/mintlify.json`, resources
|
|
10
|
+
* below `pages/` and `fragments/`; the generated Mintlify tree is not
|
|
11
|
+
* source content and must live outside this directory.
|
|
12
|
+
*/
|
|
13
|
+
export async function loadMintlifySource(sourceDir) {
|
|
14
|
+
const sitePath = path.join(sourceDir, 'config', 'mintlify.json');
|
|
15
|
+
const pagesDir = path.join(sourceDir, 'pages');
|
|
16
|
+
const [site, pageResources, fragmentResources, configFragments, apiSpecs] = await Promise.all([
|
|
17
|
+
readJSON(sitePath),
|
|
18
|
+
readIndexResources(pagesDir),
|
|
19
|
+
readOptionalIndexResources(path.join(sourceDir, 'fragments')),
|
|
20
|
+
readOptionalJSONResources(path.join(sourceDir, 'config', 'mintlify')),
|
|
21
|
+
readOptionalJSONResources(path.join(sourceDir, 'data', 'mintlify-api')),
|
|
22
|
+
]);
|
|
23
|
+
return {
|
|
24
|
+
site: validateSite(site, sitePath),
|
|
25
|
+
documents: pageResources.map(({ value, file, relative }) => validateDocument(value, file, relative === '_index.json' ? 'index' : relative.slice(0, -'/_index.json'.length))),
|
|
26
|
+
snippets: fragmentResources.map(({ value, file, relative }) => validateSnippet(value, file, `${relative.slice(0, -'/_index.json'.length)}.mdx`)),
|
|
27
|
+
configFragments: configFragments.map(({ value, file }) => validateConfigFragment(value, file)),
|
|
28
|
+
apiSpecs: apiSpecs.map(({ value, file }) => validateAPISpec(value, file)),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Loads resolved pages/fragments from ACI compiler output for framework projection. */
|
|
32
|
+
export async function loadCompiledMintlifySource(sourceDir, compiledDir) {
|
|
33
|
+
const sitePath = path.join(sourceDir, 'config', 'mintlify.json');
|
|
34
|
+
const manifestPath = path.join(compiledDir, 'manifest.json');
|
|
35
|
+
const [siteValue, manifestValue, configFragments, apiSpecs] = await Promise.all([
|
|
36
|
+
readJSON(sitePath), readJSON(manifestPath),
|
|
37
|
+
readOptionalJSONResources(path.join(sourceDir, 'config', 'mintlify')),
|
|
38
|
+
readOptionalJSONResources(path.join(sourceDir, 'data', 'mintlify-api')),
|
|
39
|
+
]);
|
|
40
|
+
if (!isObject(manifestValue) || !isObject(manifestValue.routes) || !isObject(manifestValue.fragments)) {
|
|
41
|
+
throw new Error(`ACI compiled manifest is invalid: ${manifestPath}`);
|
|
42
|
+
}
|
|
43
|
+
const documents = await Promise.all(Object.entries(manifestValue.routes).map(async ([slug, entry]) => {
|
|
44
|
+
if (!isObject(entry) || typeof entry.payloadRef !== 'string')
|
|
45
|
+
throw new Error(`ACI compiled route is invalid: ${slug}`);
|
|
46
|
+
return validateDocument(await readJSON(safeOutputPath(compiledDir, entry.payloadRef)), entry.payloadRef, slug || 'index');
|
|
47
|
+
}));
|
|
48
|
+
const snippets = await Promise.all(Object.entries(manifestValue.fragments).map(async ([id, entry]) => {
|
|
49
|
+
if (!isObject(entry) || typeof entry.payloadRef !== 'string')
|
|
50
|
+
throw new Error(`ACI compiled fragment is invalid: ${id}`);
|
|
51
|
+
return validateSnippet(await readJSON(safeOutputPath(compiledDir, entry.payloadRef)), entry.payloadRef, `${id}.mdx`);
|
|
52
|
+
}));
|
|
53
|
+
return {
|
|
54
|
+
site: validateSite(siteValue, sitePath), documents, snippets,
|
|
55
|
+
configFragments: configFragments.map(({ value, file }) => validateConfigFragment(value, file)),
|
|
56
|
+
apiSpecs: apiSpecs.map(({ value, file }) => validateAPISpec(value, file)),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** Materializes one exact Mintlify site from ACI source content. */
|
|
60
|
+
export async function materializeMintlifySite(options) {
|
|
61
|
+
const outputDir = path.resolve(options.outputDir);
|
|
62
|
+
const site = validateSite(options.site, 'site');
|
|
63
|
+
const documents = options.documents.map((document, index) => validateDocument(document, `documents[${index}]`, document.path));
|
|
64
|
+
const snippets = (options.snippets ?? []).map((snippet, index) => validateSnippet(snippet, `snippets[${index}]`, snippet.path));
|
|
65
|
+
const configFragments = (options.configFragments ?? []).map((fragment, index) => validateConfigFragment(fragment, `configFragments[${index}]`));
|
|
66
|
+
const apiSpecs = (options.apiSpecs ?? []).map((spec, index) => validateAPISpec(spec, `apiSpecs[${index}]`));
|
|
67
|
+
const nativeConfig = toDocsJSON(site);
|
|
68
|
+
const resolvedConfig = resolveConfigReferences(nativeConfig, configFragments);
|
|
69
|
+
validateSource(resolvedConfig, documents, snippets, apiSpecs);
|
|
70
|
+
const generated = new Map();
|
|
71
|
+
addGenerated(generated, 'docs.json', `${JSON.stringify(nativeConfig, null, 2)}\n`);
|
|
72
|
+
for (const fragment of configFragments) {
|
|
73
|
+
addGenerated(generated, fragment.path, `${JSON.stringify(canonicalize(fragment.value), null, 2)}\n`);
|
|
74
|
+
}
|
|
75
|
+
for (const snippet of snippets) {
|
|
76
|
+
addGenerated(generated, snippet.path, renderSnippet(snippet));
|
|
77
|
+
}
|
|
78
|
+
for (const spec of apiSpecs) {
|
|
79
|
+
addGenerated(generated, spec.path, spec.source.replace(/\r\n/g, '\n').replace(/\n*$/, '\n'));
|
|
80
|
+
}
|
|
81
|
+
for (const document of documents) {
|
|
82
|
+
addGenerated(generated, `${document.path}.mdx`, renderDocument(document));
|
|
83
|
+
}
|
|
84
|
+
await mkdir(outputDir, { recursive: true });
|
|
85
|
+
for (const [relativePath, contents] of generated) {
|
|
86
|
+
const destination = safeOutputPath(outputDir, relativePath);
|
|
87
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
88
|
+
await writeFile(destination, contents, 'utf8');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function addGenerated(generated, outputPath, contents) {
|
|
92
|
+
if (generated.has(outputPath))
|
|
93
|
+
throw new Error(`ACI Mintlify generated output path is duplicated: ${outputPath}`);
|
|
94
|
+
generated.set(outputPath, contents);
|
|
95
|
+
}
|
|
96
|
+
async function readDocuments(directory) {
|
|
97
|
+
let entries;
|
|
98
|
+
try {
|
|
99
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
throw new Error(`ACI Mintlify documents directory is required: ${directory}`, { cause: error });
|
|
103
|
+
}
|
|
104
|
+
const results = [];
|
|
105
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
106
|
+
const file = path.join(directory, entry.name);
|
|
107
|
+
if (entry.isDirectory())
|
|
108
|
+
results.push(...await readDocuments(file));
|
|
109
|
+
else if (entry.isFile() && entry.name.endsWith('.json'))
|
|
110
|
+
results.push({ file, value: await readJSON(file) });
|
|
111
|
+
}
|
|
112
|
+
return results;
|
|
113
|
+
}
|
|
114
|
+
async function readIndexResources(directory, root = directory) {
|
|
115
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
116
|
+
const results = [];
|
|
117
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
118
|
+
const file = path.join(directory, entry.name);
|
|
119
|
+
if (entry.isDirectory())
|
|
120
|
+
results.push(...await readIndexResources(file, root));
|
|
121
|
+
else if (entry.isFile() && entry.name === '_index.json')
|
|
122
|
+
results.push({ file, relative: path.relative(root, file).split(path.sep).join('/'), value: await readJSON(file) });
|
|
123
|
+
}
|
|
124
|
+
return results;
|
|
125
|
+
}
|
|
126
|
+
async function readOptionalIndexResources(directory) {
|
|
127
|
+
try {
|
|
128
|
+
return await readIndexResources(directory);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (error.code === 'ENOENT')
|
|
132
|
+
return [];
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async function readOptionalJSONResources(directory) {
|
|
137
|
+
try {
|
|
138
|
+
return await readDocuments(directory);
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
if (error.cause && error.cause.code === 'ENOENT')
|
|
142
|
+
return [];
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async function readJSON(file) {
|
|
147
|
+
try {
|
|
148
|
+
return JSON.parse(await readFile(file, 'utf8'));
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
throw new Error(`ACI Mintlify source is not valid JSON: ${file}`, { cause: error });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function safeOutputPath(root, relativePath) {
|
|
155
|
+
const target = path.resolve(root, relativePath);
|
|
156
|
+
if (target === root || !target.startsWith(`${root}${path.sep}`))
|
|
157
|
+
throw new Error(`ACI Mintlify generated path escapes output directory: ${relativePath}`);
|
|
158
|
+
return target;
|
|
159
|
+
}
|