@mintlify/previewing 4.0.1215 → 4.0.1216
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/__tests__/export.test.d.ts +1 -0
- package/dist/__tests__/export.test.js +60 -0
- package/dist/local-preview/export.d.ts +2 -0
- package/dist/local-preview/export.js +45 -12
- package/dist/local-preview/getGroupFilteredRoutes.d.ts +2 -0
- package/dist/local-preview/getGroupFilteredRoutes.js +34 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fse from 'fs-extra';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { generatePages, writeStaticUserInfo } from '../local-preview/export.js';
|
|
5
|
+
import { getGroupFilteredRoutes } from '../local-preview/getGroupFilteredRoutes.js';
|
|
6
|
+
describe('getGroupFilteredRoutes', () => {
|
|
7
|
+
it('prunes routes outside the selected groups', () => {
|
|
8
|
+
const routes = getGroupFilteredRoutes({
|
|
9
|
+
groups: [
|
|
10
|
+
{
|
|
11
|
+
group: 'Guides',
|
|
12
|
+
pages: [
|
|
13
|
+
{ href: '/index', title: 'Introduction' },
|
|
14
|
+
{ href: '/quickstart', title: 'Quickstart', groups: ['admin'] },
|
|
15
|
+
{ href: '/development', title: 'Development', groups: ['developer'] },
|
|
16
|
+
{ href: '/authenticated', title: 'Authenticated', groups: ['*'] },
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
}, ['admin']);
|
|
21
|
+
expect(routes).toEqual(['/', '/index', '/quickstart', '/authenticated']);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
describe('generatePages', () => {
|
|
25
|
+
let outputDir;
|
|
26
|
+
beforeEach(async () => {
|
|
27
|
+
outputDir = await fse.mkdtemp(path.join(os.tmpdir(), 'mint-export-test-'));
|
|
28
|
+
});
|
|
29
|
+
afterEach(async () => {
|
|
30
|
+
vi.unstubAllGlobals();
|
|
31
|
+
await fse.remove(outputDir);
|
|
32
|
+
});
|
|
33
|
+
it('uses the first accessible route for the root page', async () => {
|
|
34
|
+
const fetchMock = vi.fn().mockResolvedValue(new Response('<html>Introduction</html>'));
|
|
35
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
36
|
+
const failedPageCount = await generatePages('http://localhost:3000', ['/'], outputDir, '/introduction');
|
|
37
|
+
expect(failedPageCount).toBe(0);
|
|
38
|
+
expect(fetchMock).toHaveBeenCalledWith('http://localhost:3000/introduction');
|
|
39
|
+
await expect(fse.readFile(path.join(outputDir, 'index.html'), 'utf8')).resolves.toBe('<html>Introduction</html>');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe('writeStaticUserInfo', () => {
|
|
43
|
+
let outputDir;
|
|
44
|
+
beforeEach(async () => {
|
|
45
|
+
outputDir = await fse.mkdtemp(path.join(os.tmpdir(), 'mint-export-test-'));
|
|
46
|
+
});
|
|
47
|
+
afterEach(async () => {
|
|
48
|
+
await fse.remove(outputDir);
|
|
49
|
+
});
|
|
50
|
+
it('writes mock groups to the static user endpoint', async () => {
|
|
51
|
+
await writeStaticUserInfo(outputDir, ['admin', 'user']);
|
|
52
|
+
const userInfo = await fse.readJson(path.join(outputDir, '_mintlify', 'api', 'user', 'index.html'));
|
|
53
|
+
expect(userInfo).toEqual({ user: { groups: ['admin', 'user'] } });
|
|
54
|
+
});
|
|
55
|
+
it('does not write the endpoint without groups', async () => {
|
|
56
|
+
await writeStaticUserInfo(outputDir, undefined);
|
|
57
|
+
const endpointExists = await fse.pathExists(path.join(outputDir, '_mintlify', 'api', 'user', 'index.html'));
|
|
58
|
+
expect(endpointExists).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
import { ArgumentsCamelCase } from 'yargs';
|
|
2
|
+
export declare function generatePages(baseUrl: string, routes: string[], outputDir: string, rootRoute?: string): Promise<number>;
|
|
3
|
+
export declare function writeStaticUserInfo(tmpDir: string, groups: string[] | undefined): Promise<void>;
|
|
2
4
|
export declare const exportSite: (argv: ArgumentsCamelCase) => Promise<void>;
|
|
@@ -12,10 +12,27 @@ import { fileURLToPath } from 'url';
|
|
|
12
12
|
import { CLIENT_PATH, DOT_MINTLIFY, CMD_EXEC_PATH, VERSION_PATH, NEXT_PUBLIC_PATH, NEXT_PROPS_PATH, } from '../constants.js';
|
|
13
13
|
import { addLog, clearLogs } from '../logging-state.js';
|
|
14
14
|
import { ErrorLog, SpinnerLog, SuccessLog } from '../logs.js';
|
|
15
|
+
import { getGroupFilteredRoutes } from './getGroupFilteredRoutes.js';
|
|
15
16
|
import { setupNext } from './setupNext.js';
|
|
16
17
|
import { silentUpdateClient } from './update.js';
|
|
17
18
|
const CONCURRENCY = 10;
|
|
18
19
|
const EXPORT_SCRIPTS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'export-scripts');
|
|
20
|
+
function resolveRequestRoute(route, rootRoute) {
|
|
21
|
+
if (route === '/' && rootRoute)
|
|
22
|
+
return rootRoute;
|
|
23
|
+
return route;
|
|
24
|
+
}
|
|
25
|
+
function prepareRoutesForExport(routes, groups) {
|
|
26
|
+
if (!groups?.length || routes.includes('/'))
|
|
27
|
+
return { routes };
|
|
28
|
+
const firstAccessibleRoute = routes.find((route) => route !== '/');
|
|
29
|
+
if (!firstAccessibleRoute)
|
|
30
|
+
return { routes };
|
|
31
|
+
return {
|
|
32
|
+
routes: ['/', ...routes],
|
|
33
|
+
rootRoute: firstAccessibleRoute,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
19
36
|
async function withExportConsoleSilenced(task) {
|
|
20
37
|
const originalConsole = {
|
|
21
38
|
log: console.log,
|
|
@@ -38,14 +55,15 @@ async function fatal(message) {
|
|
|
38
55
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
39
56
|
process.exit(1);
|
|
40
57
|
}
|
|
41
|
-
async function collectRoutes() {
|
|
42
|
-
const
|
|
58
|
+
async function collectRoutes(groups) {
|
|
59
|
+
const shouldFilterByGroups = groups !== undefined && groups.length > 0;
|
|
60
|
+
const routes = new Set(shouldFilterByGroups ? [] : ['/']);
|
|
43
61
|
const docsConfigPath = path.join(CMD_EXEC_PATH, 'docs.json');
|
|
44
62
|
if (!(await pathExists(docsConfigPath))) {
|
|
45
63
|
return [...routes];
|
|
46
64
|
}
|
|
47
65
|
const config = await fse.readJson(docsConfigPath);
|
|
48
|
-
if (config.navigation) {
|
|
66
|
+
if (!shouldFilterByGroups && config.navigation) {
|
|
49
67
|
for (const navPath of getAllPathsInDocsNav(config.navigation)) {
|
|
50
68
|
routes.add(`/${navPath}`);
|
|
51
69
|
}
|
|
@@ -55,20 +73,26 @@ async function collectRoutes() {
|
|
|
55
73
|
const generatedNavPath = path.join(NEXT_PROPS_PATH, 'generatedDocsNav.json');
|
|
56
74
|
if (await pathExists(generatedNavPath)) {
|
|
57
75
|
const generatedNav = await fse.readJson(generatedNavPath);
|
|
76
|
+
if (shouldFilterByGroups)
|
|
77
|
+
return getGroupFilteredRoutes(generatedNav, groups);
|
|
58
78
|
for (const navPath of getAllPathsInDecoratedNav(generatedNav)) {
|
|
59
79
|
routes.add(`/${navPath}`);
|
|
60
80
|
}
|
|
61
81
|
}
|
|
82
|
+
else if (shouldFilterByGroups) {
|
|
83
|
+
throw new Error('failed to collect group-filtered routes');
|
|
84
|
+
}
|
|
62
85
|
return [...routes];
|
|
63
86
|
}
|
|
64
|
-
async function generatePages(baseUrl, routes, outputDir) {
|
|
87
|
+
export async function generatePages(baseUrl, routes, outputDir, rootRoute) {
|
|
65
88
|
const queue = [...routes];
|
|
66
89
|
let failedPageCount = 0;
|
|
67
90
|
async function worker() {
|
|
68
91
|
while (queue.length > 0) {
|
|
69
92
|
const route = queue.shift();
|
|
70
93
|
try {
|
|
71
|
-
const
|
|
94
|
+
const requestRoute = resolveRequestRoute(route, rootRoute);
|
|
95
|
+
const res = await fetch(`${baseUrl}${requestRoute}`);
|
|
72
96
|
if (!res.ok) {
|
|
73
97
|
failedPageCount += 1;
|
|
74
98
|
addLog(_jsx(ErrorLog, { message: `failed to generate ${route} (status ${res.status})` }));
|
|
@@ -108,7 +132,14 @@ async function startServer() {
|
|
|
108
132
|
});
|
|
109
133
|
return { server, port };
|
|
110
134
|
}
|
|
111
|
-
async function
|
|
135
|
+
export async function writeStaticUserInfo(tmpDir, groups) {
|
|
136
|
+
if (!groups?.length)
|
|
137
|
+
return;
|
|
138
|
+
await fse.outputJson(path.join(tmpDir, '_mintlify', 'api', 'user', 'index.html'), {
|
|
139
|
+
user: { groups },
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
async function bundleStaticAssets(tmpDir, groups) {
|
|
112
143
|
const nextStaticSrc = path.join(CLIENT_PATH, '.next', 'static');
|
|
113
144
|
if (await pathExists(nextStaticSrc)) {
|
|
114
145
|
await fse.copy(nextStaticSrc, path.join(tmpDir, '_next', 'static'));
|
|
@@ -121,6 +152,7 @@ async function bundleStaticAssets(tmpDir) {
|
|
|
121
152
|
});
|
|
122
153
|
}
|
|
123
154
|
}
|
|
155
|
+
await writeStaticUserInfo(tmpDir, groups);
|
|
124
156
|
await fse.copy(path.join(EXPORT_SCRIPTS_DIR, 'serve.js'), path.join(tmpDir, 'serve.js'));
|
|
125
157
|
await fse.copy(path.join(EXPORT_SCRIPTS_DIR, 'start-docs.sh'), path.join(tmpDir, 'Start Docs.command'));
|
|
126
158
|
await fse.copy(path.join(EXPORT_SCRIPTS_DIR, 'start-docs.bat'), path.join(tmpDir, 'Start Docs.bat'));
|
|
@@ -130,22 +162,23 @@ async function closeServer(server) {
|
|
|
130
162
|
server.close(() => resolve());
|
|
131
163
|
});
|
|
132
164
|
}
|
|
133
|
-
async function generateExportArchive(routes, outputPath) {
|
|
165
|
+
async function generateExportArchive(routes, outputPath, groups) {
|
|
134
166
|
const { server, port } = await startServer();
|
|
167
|
+
const { routes: routesToGenerate, rootRoute } = prepareRoutesForExport(routes, groups);
|
|
135
168
|
let tmpDir;
|
|
136
169
|
try {
|
|
137
170
|
clearLogs();
|
|
138
|
-
addLog(_jsx(SpinnerLog, { message: `generating ${
|
|
171
|
+
addLog(_jsx(SpinnerLog, { message: `generating ${routesToGenerate.length} pages...` }));
|
|
139
172
|
tmpDir = await fse.mkdtemp(path.join(os.tmpdir(), 'mint-export-'));
|
|
140
173
|
const exportDir = tmpDir;
|
|
141
174
|
const failedPageCount = await withExportConsoleSilenced(async () => {
|
|
142
|
-
return generatePages(`http://localhost:${port}`,
|
|
175
|
+
return generatePages(`http://localhost:${port}`, routesToGenerate, exportDir, rootRoute);
|
|
143
176
|
});
|
|
144
177
|
if (failedPageCount > 0) {
|
|
145
178
|
throw new Error(`failed to export: ${failedPageCount} page(s) could not be generated.`);
|
|
146
179
|
}
|
|
147
180
|
addLog(_jsx(SpinnerLog, { message: "collecting static assets..." }));
|
|
148
|
-
await bundleStaticAssets(exportDir);
|
|
181
|
+
await bundleStaticAssets(exportDir, groups);
|
|
149
182
|
addLog(_jsx(SpinnerLog, { message: "creating zip archive..." }));
|
|
150
183
|
const zip = new AdmZip();
|
|
151
184
|
zip.addLocalFolder(exportDir);
|
|
@@ -202,14 +235,14 @@ export const exportSite = async (argv) => {
|
|
|
202
235
|
}
|
|
203
236
|
let routes = [];
|
|
204
237
|
try {
|
|
205
|
-
routes = await collectRoutes();
|
|
238
|
+
routes = await collectRoutes(groups);
|
|
206
239
|
}
|
|
207
240
|
catch (err) {
|
|
208
241
|
await fatal(err instanceof Error && err.message ? err.message : 'failed to collect routes');
|
|
209
242
|
}
|
|
210
243
|
addLog(_jsx(SpinnerLog, { message: "starting local server..." }));
|
|
211
244
|
try {
|
|
212
|
-
await generateExportArchive(routes, outputPath);
|
|
245
|
+
await generateExportArchive(routes, outputPath, groups);
|
|
213
246
|
}
|
|
214
247
|
catch (err) {
|
|
215
248
|
await fatal(err instanceof Error && err.message ? err.message : 'export failed');
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { checkNavAccess, isAbsoluteUrl, isPage, readObjectArrayProperty } from '@mintlify/common';
|
|
2
|
+
import { divisions } from '@mintlify/validation';
|
|
3
|
+
export function getGroupFilteredRoutes(generatedNav, groups) {
|
|
4
|
+
const routes = new Set();
|
|
5
|
+
collectRoutes(generatedNav, new Set([...groups, '*']), routes);
|
|
6
|
+
return [...routes];
|
|
7
|
+
}
|
|
8
|
+
function collectRoutes(entry, userGroups, routes) {
|
|
9
|
+
const navEntry = entry;
|
|
10
|
+
if (isPage(navEntry)) {
|
|
11
|
+
if (!checkNavAccess(navEntry, userGroups, false, false))
|
|
12
|
+
return;
|
|
13
|
+
const { href } = navEntry;
|
|
14
|
+
if (!href || href.startsWith('#') || href.startsWith('//') || isAbsoluteUrl(href))
|
|
15
|
+
return;
|
|
16
|
+
const route = href.startsWith('/') ? href : `/${href}`;
|
|
17
|
+
if (route === '/index')
|
|
18
|
+
routes.add('/');
|
|
19
|
+
routes.add(route);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (entry.root != null && typeof entry.root === 'object') {
|
|
23
|
+
collectRoutes(entry.root, userGroups, routes);
|
|
24
|
+
}
|
|
25
|
+
const pages = readObjectArrayProperty(entry, 'pages');
|
|
26
|
+
if (pages) {
|
|
27
|
+
pages.forEach((page) => collectRoutes(page, userGroups, routes));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
for (const division of ['groups', ...divisions]) {
|
|
31
|
+
const items = readObjectArrayProperty(entry, division);
|
|
32
|
+
items?.forEach((item) => collectRoutes(item, userGroups, routes));
|
|
33
|
+
}
|
|
34
|
+
}
|