@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.
Files changed (48) hide show
  1. package/README.md +34 -4
  2. package/dist/cli/mintlify-render.d.mts +1 -0
  3. package/dist/cli/mintlify-render.mjs +8 -0
  4. package/dist/mintlify/blocks.d.ts +6 -0
  5. package/dist/mintlify/blocks.js +198 -0
  6. package/dist/mintlify/config.d.ts +3 -0
  7. package/dist/mintlify/config.js +77 -0
  8. package/dist/mintlify/index.d.ts +3 -0
  9. package/dist/mintlify/index.js +2 -0
  10. package/dist/mintlify/json.d.ts +8 -0
  11. package/dist/mintlify/json.js +37 -0
  12. package/dist/mintlify/materialize.d.ts +12 -0
  13. package/dist/mintlify/materialize.js +159 -0
  14. package/dist/mintlify/types.d.ts +69 -0
  15. package/dist/mintlify/types.js +1 -0
  16. package/dist/mintlify/validate.d.ts +7 -0
  17. package/dist/mintlify/validate.js +252 -0
  18. package/dist/react/VideoPlayer.d.ts +1 -1
  19. package/dist/react/index.d.ts +1 -1
  20. package/dist/testing/index.js +2 -2
  21. package/package.json +11 -7
  22. package/src/cli/mintlify-render.mjs +9 -0
  23. package/templates/astro/starter-content/src/fixtures/sections.ts +1 -1
  24. package/templates/common/.agents/skills/aci/SKILL.md +50 -0
  25. package/templates/common/.agents/skills/authoring/SKILL.md +4 -3
  26. package/templates/common/.agents/skills/migration/references/selector-map.md +1 -1
  27. package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000101}/asset.json +1 -1
  28. package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000102}/asset.json +1 -1
  29. package/templates/common/starter-content/.content/pages/404/_index.json +1 -1
  30. package/templates/common/starter-content/.content/pages/{home/_index.json → _index.json} +2 -2
  31. package/templates/common/starter-content/.content/pages/about/_index.json +1 -1
  32. package/templates/common/starter-content/.content/pages/error/_index.json +1 -1
  33. package/templates/common/starter-content/.content/patterns/components/button.json +2 -2
  34. package/templates/common/starter-content/.content/patterns/components/container.json +2 -2
  35. package/templates/common/starter-content/.content/patterns/components/feature_card.json +2 -2
  36. package/templates/common/starter-content/.content/patterns/components/feature_grid.json +2 -2
  37. package/templates/common/starter-content/.content/patterns/components/home_hero.json +2 -2
  38. package/templates/common/starter-content/.content/patterns/components/site-footer.json +2 -2
  39. package/templates/common/starter-content/.content/patterns/components/site-navigation.json +2 -2
  40. package/templates/common/starter-content/.content/patterns/components/stats_bar.json +2 -2
  41. package/templates/common/starter-content/.content/patterns/layouts/default.json +2 -2
  42. package/templates/common/starter-content/.content/patterns/components/personalization_choice.json +0 -42
  43. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/editorial.json +0 -0
  44. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000101}/governance.json +0 -0
  45. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/versions/v0001.json +0 -0
  46. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/editorial.json +0 -0
  47. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000102}/governance.json +0 -0
  48. /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.21` is a pre-1.0 public-surface reset for the ACI
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.21 zod@^4
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.21 init my-site --template next-starter
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.21 init --list
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('&', '&amp;').replaceAll('"', '&quot;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
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,2 @@
1
+ export { materializeMintlifySite, loadMintlifySource, loadCompiledMintlifySource, } from './materialize.js';
2
+ export { renderDocument, renderSnippet } from './blocks.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
+ }