@mintlify/prebuild 1.0.951 → 1.0.953

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/dist/index.d.ts CHANGED
@@ -4,3 +4,5 @@ export * from './utils.js';
4
4
  export * from './prebuild/index.js';
5
5
  export { getFaviconsConfig } from './prebuild/generateFavicons.js';
6
6
  export * from './prebuild/update/rss/index.js';
7
+ export { resolveRefs, resolveFileRefs, RefNotFoundError, CircularRefError, PathTraversalError, RefInvalidJsonError, } from './prebuild/resolveRefs.js';
8
+ export type { ResolveRefsResult } from './prebuild/resolveRefs.js';
package/dist/index.js CHANGED
@@ -4,3 +4,4 @@ export * from './utils.js';
4
4
  export * from './prebuild/index.js';
5
5
  export { getFaviconsConfig } from './prebuild/generateFavicons.js';
6
6
  export * from './prebuild/update/rss/index.js';
7
+ export { resolveRefs, resolveFileRefs, RefNotFoundError, CircularRefError, PathTraversalError, RefInvalidJsonError, } from './prebuild/resolveRefs.js';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,163 @@
1
+ import { mkdtemp, writeFile, mkdir, rm } from 'fs/promises';
2
+ import { readFile } from 'fs/promises';
3
+ import { tmpdir } from 'os';
4
+ import path from 'path';
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
+ import { resolveRefs, RefNotFoundError, CircularRefError, PathTraversalError, RefInvalidJsonError, } from '../resolveRefs.js';
7
+ describe('resolveRefs', () => {
8
+ let tempDir;
9
+ let reader;
10
+ beforeEach(async () => {
11
+ tempDir = await mkdtemp(path.join(tmpdir(), 'resolve-refs-'));
12
+ reader = (relPath) => readFile(path.join(tempDir, relPath), 'utf-8');
13
+ });
14
+ afterEach(async () => {
15
+ await rm(tempDir, { recursive: true, force: true });
16
+ });
17
+ const writeJson = async (relativePath, value) => {
18
+ await writeFile(path.join(tempDir, relativePath), JSON.stringify(value));
19
+ };
20
+ it('passes through values with no refs unchanged', async () => {
21
+ const input = { name: 'My Docs', navigation: [{ group: 'Guide', pages: ['intro'] }] };
22
+ const { resolved, referencedFiles } = await resolveRefs(input, reader);
23
+ expect(resolved).toEqual(input);
24
+ expect(referencedFiles).toEqual([]);
25
+ });
26
+ it('resolves a basic single $ref', async () => {
27
+ await writeJson('nav.json', ['intro', 'quickstart']);
28
+ const input = { name: 'Docs', pages: { $ref: './nav.json' } };
29
+ const { resolved, referencedFiles } = await resolveRefs(input, reader);
30
+ expect(resolved).toEqual({ name: 'Docs', pages: ['intro', 'quickstart'] });
31
+ expect(referencedFiles).toEqual(['nav.json']);
32
+ });
33
+ it('resolves nested refs (A → B → C)', async () => {
34
+ await mkdir(path.join(tempDir, 'nav'));
35
+ await writeJson('nav/tabs.json', { items: { $ref: './items.json' } });
36
+ await writeJson('nav/items.json', ['page1', 'page2']);
37
+ const input = { tabs: { $ref: './nav/tabs.json' } };
38
+ const { resolved, referencedFiles } = await resolveRefs(input, reader);
39
+ expect(resolved).toEqual({ tabs: { items: ['page1', 'page2'] } });
40
+ expect(referencedFiles).toContain(path.join('nav', 'tabs.json'));
41
+ expect(referencedFiles).toContain(path.join('nav', 'items.json'));
42
+ });
43
+ it('resolves $ref inside array elements', async () => {
44
+ await writeJson('group1.json', { group: 'API', pages: ['api/get', 'api/post'] });
45
+ await writeJson('group2.json', { group: 'Guides', pages: ['guide/start'] });
46
+ const input = { navigation: [{ $ref: './group1.json' }, { $ref: './group2.json' }] };
47
+ const { resolved } = await resolveRefs(input, reader);
48
+ expect(resolved).toEqual({
49
+ navigation: [
50
+ { group: 'API', pages: ['api/get', 'api/post'] },
51
+ { group: 'Guides', pages: ['guide/start'] },
52
+ ],
53
+ });
54
+ });
55
+ it('handles mixed refs and inline values', async () => {
56
+ await writeJson('api-nav.json', ['api/list', 'api/create']);
57
+ const input = {
58
+ name: 'Docs',
59
+ navigation: [
60
+ { group: 'Getting Started', pages: ['intro'] },
61
+ { group: 'API', pages: { $ref: './api-nav.json' } },
62
+ ],
63
+ };
64
+ const { resolved } = await resolveRefs(input, reader);
65
+ expect(resolved).toEqual({
66
+ name: 'Docs',
67
+ navigation: [
68
+ { group: 'Getting Started', pages: ['intro'] },
69
+ { group: 'API', pages: ['api/list', 'api/create'] },
70
+ ],
71
+ });
72
+ });
73
+ it('merges sibling keys on top of $ref when resolved value is an object', async () => {
74
+ await writeJson('group.json', {
75
+ group: 'Authentication',
76
+ pages: ['api/login', 'api/logout'],
77
+ });
78
+ const input = {
79
+ navigation: [
80
+ {
81
+ $ref: './group.json',
82
+ icon: 'lock',
83
+ tag: 'Beta',
84
+ },
85
+ ],
86
+ };
87
+ const { resolved } = await resolveRefs(input, reader);
88
+ expect(resolved).toEqual({
89
+ navigation: [
90
+ {
91
+ group: 'Authentication',
92
+ pages: ['api/login', 'api/logout'],
93
+ icon: 'lock',
94
+ tag: 'Beta',
95
+ },
96
+ ],
97
+ });
98
+ });
99
+ it('sibling keys override matching keys from $ref', async () => {
100
+ await writeJson('group.json', {
101
+ group: 'Auth',
102
+ pages: ['api/login', 'api/logout'],
103
+ icon: 'key',
104
+ });
105
+ const input = {
106
+ navigation: [
107
+ {
108
+ $ref: './group.json',
109
+ icon: 'lock',
110
+ },
111
+ ],
112
+ };
113
+ const { resolved } = await resolveRefs(input, reader);
114
+ expect(resolved).toEqual({
115
+ navigation: [
116
+ {
117
+ group: 'Auth',
118
+ pages: ['api/login', 'api/logout'],
119
+ icon: 'lock',
120
+ },
121
+ ],
122
+ });
123
+ });
124
+ it('detects circular references (A → B → A)', async () => {
125
+ await writeJson('a.json', { next: { $ref: './b.json' } });
126
+ await writeJson('b.json', { next: { $ref: './a.json' } });
127
+ const input = { data: { $ref: './a.json' } };
128
+ await expect(resolveRefs(input, reader)).rejects.toThrow(CircularRefError);
129
+ await expect(resolveRefs(input, reader)).rejects.toThrow(/Circular reference detected/);
130
+ });
131
+ it('throws RefNotFoundError for missing files', async () => {
132
+ const input = { data: { $ref: './nonexistent.json' } };
133
+ await expect(resolveRefs(input, reader)).rejects.toThrow(RefNotFoundError);
134
+ await expect(resolveRefs(input, reader)).rejects.toThrow(/does not exist/);
135
+ });
136
+ it('throws RefInvalidJsonError for invalid JSON in referenced file', async () => {
137
+ await writeFile(path.join(tempDir, 'bad.json'), '{ invalid json }');
138
+ const input = { data: { $ref: './bad.json' } };
139
+ await expect(resolveRefs(input, reader)).rejects.toThrow(RefInvalidJsonError);
140
+ await expect(resolveRefs(input, reader)).rejects.toThrow(/contains invalid JSON/);
141
+ });
142
+ it('throws PathTraversalError for paths outside content root', async () => {
143
+ const input = { data: { $ref: '../../etc/passwd' } };
144
+ await expect(resolveRefs(input, reader)).rejects.toThrow(PathTraversalError);
145
+ await expect(resolveRefs(input, reader)).rejects.toThrow(/outside the project directory/);
146
+ });
147
+ it('allows diamond dependencies (same file referenced from two places)', async () => {
148
+ await writeJson('shared.json', { shared: true });
149
+ const input = {
150
+ a: { $ref: './shared.json' },
151
+ b: { $ref: './shared.json' },
152
+ };
153
+ const { resolved, referencedFiles } = await resolveRefs(input, reader);
154
+ expect(resolved).toEqual({ a: { shared: true }, b: { shared: true } });
155
+ expect(referencedFiles.filter((f) => f.endsWith('shared.json'))).toHaveLength(2);
156
+ });
157
+ it('ignores sibling keys when the referenced value is not an object', async () => {
158
+ await writeJson('pages.json', ['intro', 'quickstart']);
159
+ const input = { $ref: './pages.json', extra: 'value' };
160
+ const { resolved } = await resolveRefs(input, reader);
161
+ expect(resolved).toEqual(['intro', 'quickstart']);
162
+ });
163
+ });
@@ -0,0 +1,18 @@
1
+ export declare class RefNotFoundError extends Error {
2
+ constructor(refPath: string, referencedFrom: string);
3
+ }
4
+ export declare class CircularRefError extends Error {
5
+ constructor(chain: string[]);
6
+ }
7
+ export declare class PathTraversalError extends Error {
8
+ constructor(refPath: string);
9
+ }
10
+ export declare class RefInvalidJsonError extends Error {
11
+ constructor(refPath: string, details: string);
12
+ }
13
+ export interface ResolveRefsResult {
14
+ resolved: unknown;
15
+ referencedFiles: string[];
16
+ }
17
+ export declare function resolveRefs(value: unknown, readFile: (relativePath: string) => Promise<string>): Promise<ResolveRefsResult>;
18
+ export declare function resolveFileRefs(value: unknown, contentDirectory: string): Promise<ResolveRefsResult>;
@@ -0,0 +1,95 @@
1
+ import { readFile } from 'fs/promises';
2
+ import path from 'path';
3
+ export class RefNotFoundError extends Error {
4
+ constructor(refPath, referencedFrom) {
5
+ super(`Referenced config file '${refPath}' does not exist (referenced from ${referencedFrom})`);
6
+ this.name = 'RefNotFoundError';
7
+ }
8
+ }
9
+ export class CircularRefError extends Error {
10
+ constructor(chain) {
11
+ super(`Circular reference detected: ${chain.join(' → ')}`);
12
+ this.name = 'CircularRefError';
13
+ }
14
+ }
15
+ export class PathTraversalError extends Error {
16
+ constructor(refPath) {
17
+ super(`Referenced config file '${refPath}' is outside the project directory`);
18
+ this.name = 'PathTraversalError';
19
+ }
20
+ }
21
+ export class RefInvalidJsonError extends Error {
22
+ constructor(refPath, details) {
23
+ super(`Referenced config file '${refPath}' contains invalid JSON: ${details}`);
24
+ this.name = 'RefInvalidJsonError';
25
+ }
26
+ }
27
+ function isJsonObject(value) {
28
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
29
+ }
30
+ function getRefPath(value) {
31
+ if (!isJsonObject(value) || typeof value.$ref !== 'string')
32
+ return undefined;
33
+ return value.$ref;
34
+ }
35
+ function resolveRefPath(refPath, fromFile) {
36
+ if (fromFile)
37
+ return path.normalize(path.join(path.dirname(fromFile), refPath));
38
+ return path.normalize(refPath);
39
+ }
40
+ async function loadRefFile(refPath, fullPath, readFile, fromFile) {
41
+ let content;
42
+ try {
43
+ content = await readFile(fullPath);
44
+ }
45
+ catch {
46
+ throw new RefNotFoundError(refPath, fromFile ?? 'project root');
47
+ }
48
+ try {
49
+ return JSON.parse(content);
50
+ }
51
+ catch (e) {
52
+ throw new RefInvalidJsonError(refPath, e instanceof Error ? e.message : String(e));
53
+ }
54
+ }
55
+ export async function resolveRefs(value, readFile) {
56
+ const referencedFiles = [];
57
+ async function resolve(value, visited, fromFile) {
58
+ const refPath = getRefPath(value);
59
+ if (refPath != null) {
60
+ const fullPath = resolveRefPath(refPath, fromFile);
61
+ if (fullPath.startsWith('..'))
62
+ throw new PathTraversalError(refPath);
63
+ if (visited.has(fullPath))
64
+ throw new CircularRefError([...visited, fullPath]);
65
+ const parsed = await loadRefFile(refPath, fullPath, readFile, fromFile);
66
+ referencedFiles.push(fullPath);
67
+ const resolvedRef = await resolve(parsed, new Set([...visited, fullPath]), fullPath);
68
+ if (isJsonObject(resolvedRef)) {
69
+ const siblings = Object.entries(value).filter(([key]) => key !== '$ref');
70
+ const result = { ...resolvedRef };
71
+ for (const [key, val] of siblings) {
72
+ result[key] = await resolve(val, visited, fromFile);
73
+ }
74
+ return result;
75
+ }
76
+ return resolvedRef;
77
+ }
78
+ if (Array.isArray(value)) {
79
+ return Promise.all(value.map((item) => resolve(item, visited, fromFile)));
80
+ }
81
+ if (isJsonObject(value)) {
82
+ const result = {};
83
+ for (const [key, val] of Object.entries(value)) {
84
+ result[key] = await resolve(val, visited, fromFile);
85
+ }
86
+ return result;
87
+ }
88
+ return value;
89
+ }
90
+ const resolved = await resolve(value, new Set());
91
+ return { resolved, referencedFiles };
92
+ }
93
+ export async function resolveFileRefs(value, contentDirectory) {
94
+ return resolveRefs(value, (relativePath) => readFile(path.join(contentDirectory, relativePath), 'utf-8'));
95
+ }
@@ -3,7 +3,7 @@ export declare class ConfigUpdater<T> {
3
3
  private type;
4
4
  constructor(type: ConfigType);
5
5
  getConfigType(): "mint" | "docs";
6
- getConfig(configPath: string, strict?: boolean, onError?: (message: string) => void): Promise<T>;
6
+ getConfig(configPath: string, strict?: boolean, contentDirectoryPath?: string, onError?: (message: string) => void): Promise<T>;
7
7
  validateConfigJsonString: (configContents: string, strict?: boolean, onError?: (message: string) => void) => Promise<{
8
8
  data: T;
9
9
  warnings: import("zod").ZodIssue[];
@@ -15,6 +15,7 @@ export declare class ConfigUpdater<T> {
15
15
  success: true;
16
16
  error?: never;
17
17
  }>;
18
+ private validateConfigObj;
18
19
  private readConfigFile;
19
20
  writeConfigFile: (config: T, targetDir?: string) => Promise<void>;
20
21
  private parseConfigJson;
@@ -3,11 +3,15 @@ import Chalk from 'chalk';
3
3
  import { promises as _promises } from 'fs';
4
4
  import { outputFile } from 'fs-extra';
5
5
  import { join } from 'path';
6
+ import { resolveFileRefs } from '../resolveRefs.js';
6
7
  const { readFile } = _promises;
7
8
  export class ConfigUpdater {
8
9
  constructor(type) {
9
10
  this.validateConfigJsonString = async (configContents, strict, onError) => {
10
11
  const configObj = this.parseConfigJson(configContents, onError);
12
+ return this.validateConfigObj(configObj, strict, onError);
13
+ };
14
+ this.validateConfigObj = async (configObj, strict, onError) => {
11
15
  const validationResults = this.type === 'mint' ? validateMintConfig(configObj) : validateDocsConfig(configObj);
12
16
  if (!validationResults.success) {
13
17
  const errorMsg = `🚨 Invalid ${this.type}.json:`;
@@ -94,10 +98,25 @@ export class ConfigUpdater {
94
98
  getConfigType() {
95
99
  return this.type;
96
100
  }
97
- async getConfig(configPath, strict, onError) {
101
+ async getConfig(configPath, strict, contentDirectoryPath, onError) {
98
102
  const configContents = await this.readConfigFile(configPath);
99
- const validationResults = await this.validateConfigJsonString(configContents, strict, onError);
100
- return validationResults.data;
103
+ const configObj = this.parseConfigJson(configContents, onError);
104
+ let resolvedObj = configObj;
105
+ if (contentDirectoryPath) {
106
+ try {
107
+ const { resolved } = await resolveFileRefs(configObj, contentDirectoryPath);
108
+ resolvedObj = resolved;
109
+ }
110
+ catch (e) {
111
+ const msg = `Error resolving $ref in ${this.type}.json: ${e instanceof Error ? e.message : String(e)}`;
112
+ if (onError) {
113
+ onError(msg);
114
+ }
115
+ throw Error(msg);
116
+ }
117
+ }
118
+ const { data: config } = await this.validateConfigObj(resolvedObj, strict, onError);
119
+ return config;
101
120
  }
102
121
  }
103
122
  export const MintConfigUpdater = new ConfigUpdater('mint');
@@ -10,7 +10,7 @@ export async function updateDocsConfigFile({ contentDirectoryPath, openApiFiles,
10
10
  throw Error(NOT_CORRECT_PATH_ERROR);
11
11
  }
12
12
  if (docsConfig == null && configPath) {
13
- docsConfig = await DocsConfigUpdater.getConfig(configPath, strict);
13
+ docsConfig = await DocsConfigUpdater.getConfig(configPath, strict, contentDirectoryPath);
14
14
  }
15
15
  if (docsConfig == null) {
16
16
  throw Error(NOT_CORRECT_PATH_ERROR);
@@ -6,7 +6,7 @@ export async function updateMintConfigFile(contentDirectoryPath, openApiFiles, l
6
6
  if (configPath == null) {
7
7
  return null;
8
8
  }
9
- const mintConfig = await MintConfigUpdater.getConfig(configPath, strict);
9
+ const mintConfig = await MintConfigUpdater.getConfig(configPath, strict, contentDirectoryPath);
10
10
  const { mintConfig: newMintConfig, pagesAcc, openApiFiles: newOpenApiFiles, } = await generateOpenApiAnchorsOrTabs(mintConfig, openApiFiles, undefined, localSchema);
11
11
  await MintConfigUpdater.writeConfigFile(newMintConfig);
12
12
  return {