@mintlify/previewing 4.0.1215 → 4.0.1217

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.
@@ -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 routes = new Set(['/']);
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 res = await fetch(`${baseUrl}${route}`);
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 bundleStaticAssets(tmpDir) {
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 ${routes.length} pages...` }));
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}`, routes, exportDir);
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,2 @@
1
+ import type { DecoratedNavigationConfig } from '@mintlify/validation';
2
+ export declare function getGroupFilteredRoutes(generatedNav: DecoratedNavigationConfig, groups: string[]): string[];
@@ -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
+ }