@mintlify/previewing 4.0.1588 → 4.0.1590
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__/dev.test.js +3 -3
- package/dist/__tests__/export-assets.test.js +9 -5
- package/dist/__tests__/export-vite.test.d.ts +1 -0
- package/dist/__tests__/export-vite.test.js +101 -0
- package/dist/__tests__/export.test.js +2 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +2 -2
- package/dist/local-preview/export-assets.d.ts +4 -1
- package/dist/local-preview/export-assets.js +6 -6
- package/dist/local-preview/export-next.d.ts +3 -0
- package/dist/local-preview/export-next.js +68 -0
- package/dist/local-preview/export-pages.d.ts +16 -0
- package/dist/local-preview/export-pages.js +36 -0
- package/dist/local-preview/export-vite.d.ts +9 -0
- package/dist/local-preview/export-vite.js +90 -0
- package/dist/local-preview/export.d.ts +0 -1
- package/dist/local-preview/export.js +24 -89
- package/dist/local-preview/index.js +1 -8
- package/dist/local-preview/setupVite.d.ts +9 -0
- package/dist/local-preview/setupVite.js +15 -2
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/export-scripts/README.classic.md +15 -0
- package/export-scripts/README.md +2 -3
- package/package.json +5 -5
|
@@ -67,7 +67,7 @@ describe('dev', () => {
|
|
|
67
67
|
process.chdir = originalChdir;
|
|
68
68
|
});
|
|
69
69
|
it('happy path', async () => {
|
|
70
|
-
await dev(defaultYargs);
|
|
70
|
+
await dev({ ...defaultYargs, classic: true });
|
|
71
71
|
expect(addLogSpy).toHaveBeenCalledWith(expect.objectContaining({ props: { message: 'preparing local preview...' } }));
|
|
72
72
|
expect(seedPreviewFromSharedMock).toHaveBeenCalled();
|
|
73
73
|
expect(silentUpdateClientMock).toHaveBeenCalled();
|
|
@@ -75,12 +75,12 @@ describe('dev', () => {
|
|
|
75
75
|
expect(runMock).toHaveBeenCalled();
|
|
76
76
|
});
|
|
77
77
|
it('does not seed from shared when a specific client version is requested', async () => {
|
|
78
|
-
await dev({ ...defaultYargs, 'client-version': '1.2.3' });
|
|
78
|
+
await dev({ ...defaultYargs, classic: true, 'client-version': '1.2.3' });
|
|
79
79
|
expect(seedPreviewFromSharedMock).not.toHaveBeenCalled();
|
|
80
80
|
expect(silentUpdateClientMock).toHaveBeenCalled();
|
|
81
81
|
});
|
|
82
82
|
it('downloads the vite client without seeding it from the next workspace', async () => {
|
|
83
|
-
await dev(
|
|
83
|
+
await dev(defaultYargs);
|
|
84
84
|
expect(seedPreviewFromSharedMock).not.toHaveBeenCalled();
|
|
85
85
|
expect(silentUpdateClientMock).toHaveBeenCalled();
|
|
86
86
|
expect(runMock).toHaveBeenCalled();
|
|
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'url';
|
|
|
6
6
|
import { collectAssetPaths, localizeAssets, rewriteCssUrls, } from '../local-preview/export-assets.js';
|
|
7
7
|
import { fetchPublicAsset } from '../local-preview/export-fetch.js';
|
|
8
8
|
import { generateExportLlmsTxt } from '../local-preview/export-llms.js';
|
|
9
|
+
const NEXT_STATIC = path.join('_next', 'static');
|
|
9
10
|
const ICON_URL = 'https://d3gk2c5xim1je2.cloudfront.net/fontawesome/v7.2.0/regular/house.svg';
|
|
10
11
|
const GOOGLE_CSS_URL = 'https://fonts.googleapis.com/css2?family=Open+Sans:wght@400&display=swap';
|
|
11
12
|
const asset = (body, contentType) => ({
|
|
@@ -58,7 +59,7 @@ describe('export assets', () => {
|
|
|
58
59
|
return asset(new Uint8Array([1, 2, 3]));
|
|
59
60
|
return undefined;
|
|
60
61
|
});
|
|
61
|
-
const result = await localizeAssets(exportDir, fetchAsset);
|
|
62
|
+
const result = await localizeAssets(exportDir, { stylesheetDir: NEXT_STATIC, fetchAsset });
|
|
62
63
|
expect(result.failed).toEqual(['https://d3gk2c5xim1je2.cloudfront.net/missing.svg']);
|
|
63
64
|
await expect(fse.readFile(path.join(exportDir, iconPath), 'utf8')).resolves.toBe('<svg/>');
|
|
64
65
|
await expect(fse.readFile(path.join(exportDir, cssPath), 'utf8')).resolves.toBe('@font-face{src:url(../../../fonts.gstatic.com/s/a.woff2)}');
|
|
@@ -68,7 +69,10 @@ describe('export assets', () => {
|
|
|
68
69
|
});
|
|
69
70
|
it('does not report missing language icons as failed', async () => {
|
|
70
71
|
await fse.outputFile(path.join(exportDir, 'index.html'), '<pre language="sql"></pre><pre language="python"></pre>');
|
|
71
|
-
const result = await localizeAssets(exportDir,
|
|
72
|
+
const result = await localizeAssets(exportDir, {
|
|
73
|
+
stylesheetDir: NEXT_STATIC,
|
|
74
|
+
fetchAsset: async () => undefined,
|
|
75
|
+
});
|
|
72
76
|
expect(result.failed).toEqual([]);
|
|
73
77
|
});
|
|
74
78
|
});
|
|
@@ -87,7 +91,7 @@ describe('localizeAssets path safety', () => {
|
|
|
87
91
|
const fetchAsset = vi.fn(async (url) => url === cssUrl
|
|
88
92
|
? asset('@font-face{src:url(https://evil.example/%2e%2e%2f%2e%2e%2f%2e%2e%2fserve.js)}', 'text/css')
|
|
89
93
|
: asset('malicious launcher'));
|
|
90
|
-
const result = await localizeAssets(exportDir, fetchAsset);
|
|
94
|
+
const result = await localizeAssets(exportDir, { stylesheetDir: NEXT_STATIC, fetchAsset });
|
|
91
95
|
await expect(fse.readFile(path.join(exportDir, 'serve.js'), 'utf8')).resolves.toBe('original launcher');
|
|
92
96
|
expect(result.failed).toContain('/_mintlify/export-assets/evil.example/%2e%2e%2f%2e%2e%2f%2e%2e%2fserve.js');
|
|
93
97
|
});
|
|
@@ -106,7 +110,7 @@ describe('localizeAssets private destinations', () => {
|
|
|
106
110
|
const fetchAsset = vi.fn(async (url) => url === cssUrl
|
|
107
111
|
? asset('.a{background:url(http://127.0.0.1:9000/secret.png)}', 'text/css')
|
|
108
112
|
: fetchPublicAsset(url));
|
|
109
|
-
const result = await localizeAssets(exportDir, fetchAsset);
|
|
113
|
+
const result = await localizeAssets(exportDir, { stylesheetDir: NEXT_STATIC, fetchAsset });
|
|
110
114
|
expect(result.failed).toEqual(['http://127.0.0.1:9000/secret.png']);
|
|
111
115
|
await expect(fse.pathExists(path.join(exportDir, '_mintlify/export-assets/_http/127.0.0.1~9000/secret.png'))).resolves.toBe(false);
|
|
112
116
|
});
|
|
@@ -123,7 +127,7 @@ describe('localizeAssets stylesheets', () => {
|
|
|
123
127
|
const cssFile = path.join(exportDir, '_next', 'static', 'chunks', 'app.css');
|
|
124
128
|
await fse.outputFile(cssFile, '@font-face{src:url(https://cdn.jsdelivr.net/gh/x/latex-css/fonts/LM-regular.woff2)}@font-face{src:url(//fonts.cdnfonts.com/s/1/cmuntt.woff)}@font-face{src:url(../media/inter.woff2)}');
|
|
125
129
|
const fetchAsset = vi.fn(async () => asset(new Uint8Array([7])));
|
|
126
|
-
const result = await localizeAssets(exportDir, fetchAsset);
|
|
130
|
+
const result = await localizeAssets(exportDir, { stylesheetDir: NEXT_STATIC, fetchAsset });
|
|
127
131
|
expect(result.failed).toEqual([]);
|
|
128
132
|
await expect(fse.readFile(cssFile, 'utf8')).resolves.toBe('@font-face{src:url(../../../_mintlify/export-assets/cdn.jsdelivr.net/gh/x/latex-css/fonts/LM-regular.woff2)}@font-face{src:url(../../../_mintlify/export-assets/fonts.cdnfonts.com/s/1/cmuntt.woff)}@font-face{src:url(../media/inter.woff2)}');
|
|
129
133
|
await expect(fse.pathExists(path.join(exportDir, '_mintlify/export-assets/fonts.cdnfonts.com/s/1/cmuntt.woff'))).resolves.toBe(true);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import fse from 'fs-extra';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { bundleServerAssets, generateVitePages } from '../local-preview/export-vite.js';
|
|
5
|
+
const html = (body, status = 200) => new Response(body, { status, headers: { 'content-type': 'text/html' } });
|
|
6
|
+
describe('generateVitePages', () => {
|
|
7
|
+
let outputDir;
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
outputDir = await fse.mkdtemp(path.join(os.tmpdir(), 'mint-export-vite-test-'));
|
|
10
|
+
});
|
|
11
|
+
afterEach(async () => {
|
|
12
|
+
await fse.remove(outputDir);
|
|
13
|
+
});
|
|
14
|
+
it('writes one html document per route', async () => {
|
|
15
|
+
const fetchPage = vi.fn(async (req) => html(new URL(req.url).pathname));
|
|
16
|
+
await expect(generateVitePages(fetchPage, ['/', '/guides/math'], outputDir, '/introduction')).resolves.toBe(0);
|
|
17
|
+
await expect(fse.readFile(path.join(outputDir, 'index.html'), 'utf8')).resolves.toBe('/introduction');
|
|
18
|
+
await expect(fse.readFile(path.join(outputDir, 'guides', 'math', 'index.html'), 'utf8')).resolves.toBe('/guides/math');
|
|
19
|
+
expect(await fse.pathExists(path.join(outputDir, 'guides', 'math', 'index.txt'))).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
it('follows local redirects, like a root that redirects to the first page', async () => {
|
|
22
|
+
const fetchPage = vi.fn(async (req) => new URL(req.url).pathname === '/'
|
|
23
|
+
? new Response(null, { status: 307, headers: { location: '/introduction' } })
|
|
24
|
+
: html(new URL(req.url).pathname));
|
|
25
|
+
await expect(generateVitePages(fetchPage, ['/'], outputDir)).resolves.toBe(0);
|
|
26
|
+
await expect(fse.readFile(path.join(outputDir, 'index.html'), 'utf8')).resolves.toBe('/introduction');
|
|
27
|
+
});
|
|
28
|
+
it('does not follow redirects off the local origin', async () => {
|
|
29
|
+
const fetchPage = vi.fn(async () => new Response(null, { status: 307, headers: { location: 'https://example.com/' } }));
|
|
30
|
+
await expect(generateVitePages(fetchPage, ['/'], outputDir)).resolves.toBe(1);
|
|
31
|
+
expect(fetchPage).toHaveBeenCalledTimes(1);
|
|
32
|
+
});
|
|
33
|
+
it('counts failed and non-html responses', async () => {
|
|
34
|
+
const fetchPage = vi.fn(async (req) => req.url.endsWith('/missing')
|
|
35
|
+
? html('', 404)
|
|
36
|
+
: new Response('{}', { headers: { 'content-type': 'application/json' } }));
|
|
37
|
+
await expect(generateVitePages(fetchPage, ['/missing', '/json'], outputDir)).resolves.toBe(2);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
const ASSET_PREFIXES = ['/_mintlify/custom/', '/_mintlify/assets/fonts/google/'];
|
|
41
|
+
describe('bundleServerAssets', () => {
|
|
42
|
+
let outputDir;
|
|
43
|
+
beforeEach(async () => {
|
|
44
|
+
outputDir = await fse.mkdtemp(path.join(os.tmpdir(), 'mint-export-vite-test-'));
|
|
45
|
+
});
|
|
46
|
+
afterEach(async () => {
|
|
47
|
+
await fse.remove(outputDir);
|
|
48
|
+
});
|
|
49
|
+
it('writes the server assets the pages reference and reports failures', async () => {
|
|
50
|
+
await fse.outputFile(path.join(outputDir, 'index.html'), '<style>src:url(/_mintlify/assets/fonts/google/s/inter/v1/a.woff2) url(/_mintlify/assets/fonts/google/s/inter/v1/b.woff2)</style><script src="/_mintlify/custom/localhost/abc/script.js"></script>');
|
|
51
|
+
const fetchAsset = vi.fn(async (req) => req.url.endsWith('b.woff2') ? new Response(null, { status: 502 }) : new Response('ok'));
|
|
52
|
+
await expect(bundleServerAssets(fetchAsset, outputDir, ASSET_PREFIXES)).resolves.toEqual([
|
|
53
|
+
'/_mintlify/assets/fonts/google/s/inter/v1/b.woff2',
|
|
54
|
+
]);
|
|
55
|
+
await expect(fse.readFile(path.join(outputDir, '_mintlify/assets/fonts/google/s/inter/v1/a.woff2'), 'utf8')).resolves.toBe('ok');
|
|
56
|
+
await expect(fse.readFile(path.join(outputDir, '_mintlify/custom/localhost/abc/script.js'), 'utf8')).resolves.toBe('ok');
|
|
57
|
+
});
|
|
58
|
+
it('fetches server assets referenced by fetched stylesheets', async () => {
|
|
59
|
+
await fse.outputFile(path.join(outputDir, 'index.html'), '<link rel="stylesheet" href="/_mintlify/custom/localhost/abc/style.css">');
|
|
60
|
+
const fetchAsset = vi.fn(async (req) => req.url.endsWith('style.css')
|
|
61
|
+
? new Response('@font-face{src:url(/_mintlify/assets/fonts/google/s/geist/v1/c.woff2)}', {
|
|
62
|
+
headers: { 'content-type': 'text/css' },
|
|
63
|
+
})
|
|
64
|
+
: new Response('font'));
|
|
65
|
+
await expect(bundleServerAssets(fetchAsset, outputDir, ASSET_PREFIXES)).resolves.toEqual([]);
|
|
66
|
+
await expect(fse.readFile(path.join(outputDir, '_mintlify/assets/fonts/google/s/geist/v1/c.woff2'), 'utf8')).resolves.toBe('font');
|
|
67
|
+
});
|
|
68
|
+
it('writes percent-encoded custom filenames decoded', async () => {
|
|
69
|
+
await fse.outputFile(path.join(outputDir, 'index.html'), '<script src="/_mintlify/custom/localhost/abc/custom%281%29%21%27.js"></script>');
|
|
70
|
+
const fetchAsset = vi.fn(async () => new Response('ok'));
|
|
71
|
+
await expect(bundleServerAssets(fetchAsset, outputDir, ASSET_PREFIXES)).resolves.toEqual([]);
|
|
72
|
+
await expect(fse.readFile(path.join(outputDir, "_mintlify/custom/localhost/abc/custom(1)!'.js"), 'utf8')).resolves.toBe('ok');
|
|
73
|
+
});
|
|
74
|
+
it('collects assets under a base path', async () => {
|
|
75
|
+
await fse.outputFile(path.join(outputDir, 'index.html'), '<link href="/docs/_mintlify/custom/localhost/abc/style.css"><style>src:url(/docs/_mintlify/assets/fonts/google/s/a.woff2)</style>');
|
|
76
|
+
const fetchAsset = vi.fn(async (_req) => new Response('ok'));
|
|
77
|
+
await expect(bundleServerAssets(fetchAsset, outputDir, ASSET_PREFIXES)).resolves.toEqual([]);
|
|
78
|
+
expect(fetchAsset.mock.calls.map(([req]) => new URL(req.url).pathname).sort()).toEqual([
|
|
79
|
+
'/_mintlify/assets/fonts/google/s/a.woff2',
|
|
80
|
+
'/_mintlify/custom/localhost/abc/style.css',
|
|
81
|
+
]);
|
|
82
|
+
});
|
|
83
|
+
it('strips quotes from css urls', async () => {
|
|
84
|
+
await fse.outputFile(path.join(outputDir, 'index.html'), `<style>src:url("/_mintlify/assets/fonts/google/s/a.woff2") url('/_mintlify/custom/localhost/abc/b.png')</style>`);
|
|
85
|
+
const fetchAsset = vi.fn(async (_req) => new Response('ok'));
|
|
86
|
+
await expect(bundleServerAssets(fetchAsset, outputDir, ASSET_PREFIXES)).resolves.toEqual([]);
|
|
87
|
+
expect(fetchAsset.mock.calls.map(([req]) => new URL(req.url).pathname).sort()).toEqual([
|
|
88
|
+
'/_mintlify/assets/fonts/google/s/a.woff2',
|
|
89
|
+
'/_mintlify/custom/localhost/abc/b.png',
|
|
90
|
+
]);
|
|
91
|
+
});
|
|
92
|
+
it('never writes encoded traversal paths outside the export', async () => {
|
|
93
|
+
await fse.outputFile(path.join(outputDir, 'index.html'), '<script src="/_mintlify/custom/%2e%2e/%2e%2e/%2e%2e/escape.js"></script>');
|
|
94
|
+
const fetchAsset = vi.fn(async () => new Response('x'));
|
|
95
|
+
await expect(bundleServerAssets(fetchAsset, outputDir, ASSET_PREFIXES)).resolves.toEqual([
|
|
96
|
+
'/_mintlify/custom/%2e%2e/%2e%2e/%2e%2e/escape.js',
|
|
97
|
+
]);
|
|
98
|
+
expect(fetchAsset).not.toHaveBeenCalled();
|
|
99
|
+
expect(await fse.pathExists(path.join(path.dirname(outputDir), 'escape.js'))).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fse from 'fs-extra';
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import { generatePages
|
|
4
|
+
import { generatePages } from '../local-preview/export-next.js';
|
|
5
|
+
import { withIndexRouteAliases, writeStaticUserInfo } from '../local-preview/export.js';
|
|
5
6
|
import { getGroupFilteredRoutes } from '../local-preview/getGroupFilteredRoutes.js';
|
|
6
7
|
describe('getGroupFilteredRoutes', () => {
|
|
7
8
|
it('prunes routes outside the selected groups', () => {
|
package/dist/constants.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export declare let TAR_PATH: string;
|
|
|
16
16
|
export type ClientFramework = 'next' | 'vite';
|
|
17
17
|
export declare let CLIENT_FRAMEWORK: ClientFramework;
|
|
18
18
|
export declare const configurePreviewPath: (port?: string | number, framework?: ClientFramework) => void;
|
|
19
|
-
export declare const configureSharedPreviewPath: () => void;
|
|
19
|
+
export declare const configureSharedPreviewPath: (framework?: ClientFramework) => void;
|
|
20
20
|
export declare const MINT_CLIENT_RELEASES_CLOUDFRONT_URL = "https://releases.mintlify.com";
|
|
21
21
|
export declare const TARGET_MINT_VERSION_URL = "https://releases.mintlify.com/mint-version.txt";
|
|
22
22
|
export declare const MINT_VERSION_MAP_URL = "https://releases.mintlify.com/mint-version-map.json";
|
package/dist/constants.js
CHANGED
|
@@ -43,8 +43,8 @@ export const configurePreviewPath = (port = 3000, framework = 'next') => {
|
|
|
43
43
|
}
|
|
44
44
|
setPreviewPath(String(numericPort), framework);
|
|
45
45
|
};
|
|
46
|
-
export const configureSharedPreviewPath = () => {
|
|
47
|
-
setPreviewPath('shared');
|
|
46
|
+
export const configureSharedPreviewPath = (framework = 'next') => {
|
|
47
|
+
setPreviewPath('shared', framework);
|
|
48
48
|
};
|
|
49
49
|
configureSharedPreviewPath();
|
|
50
50
|
export const MINT_CLIENT_RELEASES_CLOUDFRONT_URL = 'https://releases.mintlify.com';
|
|
@@ -6,4 +6,7 @@ export type LocalizeAssetsResult = {
|
|
|
6
6
|
export declare function walkFiles(dir: string): AsyncGenerator<string>;
|
|
7
7
|
export declare function collectAssetPaths(exportDir: string): Promise<Set<string>>;
|
|
8
8
|
export declare function rewriteCssUrls(css: string, cssUrl: string | undefined, cssAssetPath: string, onAsset: (assetPath: string) => void): string;
|
|
9
|
-
export declare function localizeAssets(exportDir: string, fetchAsset
|
|
9
|
+
export declare function localizeAssets(exportDir: string, { stylesheetDir, fetchAsset, }: {
|
|
10
|
+
stylesheetDir: string;
|
|
11
|
+
fetchAsset?: FetchAsset;
|
|
12
|
+
}): Promise<LocalizeAssetsResult>;
|
|
@@ -73,11 +73,11 @@ export function rewriteCssUrls(css, cssUrl, cssAssetPath, onAsset) {
|
|
|
73
73
|
return `url(${quote}${relativePath}${quote})`;
|
|
74
74
|
});
|
|
75
75
|
}
|
|
76
|
-
async function localizeStylesheets(exportDir, onAsset) {
|
|
77
|
-
const
|
|
78
|
-
if (!(await fse.pathExists(
|
|
76
|
+
async function localizeStylesheets(exportDir, stylesheetDir, onAsset) {
|
|
77
|
+
const dir = path.join(exportDir, stylesheetDir);
|
|
78
|
+
if (!(await fse.pathExists(dir)))
|
|
79
79
|
return;
|
|
80
|
-
for await (const file of walkFiles(
|
|
80
|
+
for await (const file of walkFiles(dir)) {
|
|
81
81
|
if (!file.endsWith('.css'))
|
|
82
82
|
continue;
|
|
83
83
|
const css = await fse.readFile(file, 'utf8');
|
|
@@ -87,7 +87,7 @@ async function localizeStylesheets(exportDir, onAsset) {
|
|
|
87
87
|
await fse.writeFile(file, rewritten, 'utf8');
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
-
export async function localizeAssets(exportDir, fetchAsset = fetchPublicAsset) {
|
|
90
|
+
export async function localizeAssets(exportDir, { stylesheetDir, fetchAsset = fetchPublicAsset, }) {
|
|
91
91
|
const queue = [...(await collectAssetPaths(exportDir))];
|
|
92
92
|
const seen = new Set(queue);
|
|
93
93
|
const downloaded = [];
|
|
@@ -98,7 +98,7 @@ export async function localizeAssets(exportDir, fetchAsset = fetchPublicAsset) {
|
|
|
98
98
|
seen.add(assetPath);
|
|
99
99
|
queue.push(assetPath);
|
|
100
100
|
};
|
|
101
|
-
await localizeStylesheets(exportDir, enqueue);
|
|
101
|
+
await localizeStylesheets(exportDir, stylesheetDir, enqueue);
|
|
102
102
|
const localize = async (assetPath) => {
|
|
103
103
|
const url = fromMintExportAssetPath(assetPath);
|
|
104
104
|
const outputPath = resolveOutputPath(exportDir, assetPath);
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type ExportRenderer } from './export-pages.js';
|
|
2
|
+
export declare const generatePages: (baseUrl: string, routes: string[], outputDir: string, rootRoute?: string) => Promise<number>;
|
|
3
|
+
export declare function createNextRenderer(groups: string[] | undefined, basePath: string): Promise<ExportRenderer>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import fse, { pathExists } from 'fs-extra';
|
|
4
|
+
import { createServer } from 'http';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { CLIENT_PATH, NEXT_PUBLIC_PATH } from '../constants.js';
|
|
7
|
+
import { addLog } from '../logging-state.js';
|
|
8
|
+
import { SpinnerLog } from '../logs.js';
|
|
9
|
+
import { applyBasePathToExport } from './export-base-path.js';
|
|
10
|
+
import { writePages } from './export-pages.js';
|
|
11
|
+
import { setupNext } from './setupNext.js';
|
|
12
|
+
const RSC_CONTENT_TYPE = 'text/x-component';
|
|
13
|
+
const RSC_PAYLOAD_FILE = 'index.txt';
|
|
14
|
+
export const generatePages = (baseUrl, routes, outputDir, rootRoute) => writePages(routes, outputDir, rootRoute, async (requestRoute) => {
|
|
15
|
+
const [res, rscRes] = await Promise.all([
|
|
16
|
+
fetch(`${baseUrl}${requestRoute}`),
|
|
17
|
+
fetch(`${baseUrl}${requestRoute}`, { headers: { RSC: '1' } }),
|
|
18
|
+
]);
|
|
19
|
+
const isRscPayload = rscRes.headers.get('content-type')?.startsWith(RSC_CONTENT_TYPE);
|
|
20
|
+
if (!res.ok || !rscRes.ok || !isRscPayload)
|
|
21
|
+
return { status: res.ok ? rscRes.status : res.status };
|
|
22
|
+
return { files: { 'index.html': await res.text(), [RSC_PAYLOAD_FILE]: await rscRes.text() } };
|
|
23
|
+
});
|
|
24
|
+
async function startServer(basePath) {
|
|
25
|
+
const app = express();
|
|
26
|
+
const requestHandler = await setupNext(basePath ? { assetPrefix: basePath } : undefined);
|
|
27
|
+
app.use('/', express.static(NEXT_PUBLIC_PATH));
|
|
28
|
+
app.all('*', (req, res) => requestHandler(req, res));
|
|
29
|
+
const server = createServer(app);
|
|
30
|
+
const port = await new Promise((resolve, reject) => {
|
|
31
|
+
server.once('error', reject);
|
|
32
|
+
server.listen(0, () => {
|
|
33
|
+
const addr = server.address();
|
|
34
|
+
if (!addr || typeof addr === 'string') {
|
|
35
|
+
reject(new Error('failed to determine listening port'));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
resolve(addr.port);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
return { server, port };
|
|
42
|
+
}
|
|
43
|
+
async function closeServer(server) {
|
|
44
|
+
await new Promise((resolve) => {
|
|
45
|
+
server.close(() => resolve());
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
export async function createNextRenderer(groups, basePath) {
|
|
49
|
+
addLog(_jsx(SpinnerLog, { message: "starting local server..." }));
|
|
50
|
+
process.env.MINT_EXPORT = JSON.stringify({ groups, basePath: basePath || undefined });
|
|
51
|
+
const { server, port } = await startServer(basePath);
|
|
52
|
+
return {
|
|
53
|
+
readme: 'README.classic.md',
|
|
54
|
+
stylesheetDir: path.join('_next', 'static'),
|
|
55
|
+
generatePages: (routes, outputDir, rootRoute) => generatePages(`http://localhost:${port}`, routes, outputDir, rootRoute),
|
|
56
|
+
copyAssets: async (exportDir) => {
|
|
57
|
+
const nextStaticSrc = path.join(CLIENT_PATH, '.next', 'static');
|
|
58
|
+
if (await pathExists(nextStaticSrc)) {
|
|
59
|
+
await fse.copy(nextStaticSrc, path.join(exportDir, '_next', 'static'));
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
bundleAssets: async (exportDir) => {
|
|
63
|
+
await applyBasePathToExport(exportDir, basePath);
|
|
64
|
+
return [];
|
|
65
|
+
},
|
|
66
|
+
close: () => closeServer(server),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type ExportRenderer = {
|
|
2
|
+
readme: string;
|
|
3
|
+
stylesheetDir: string;
|
|
4
|
+
generatePages: (routes: string[], outputDir: string, rootRoute?: string) => Promise<number>;
|
|
5
|
+
copyAssets: (exportDir: string) => Promise<void>;
|
|
6
|
+
bundleAssets: (exportDir: string) => Promise<string[]>;
|
|
7
|
+
close: () => Promise<void>;
|
|
8
|
+
};
|
|
9
|
+
export declare const resolveRequestRoute: (route: string, rootRoute: string | undefined) => string;
|
|
10
|
+
export declare function forEachConcurrent<T>(items: T[], task: (item: T) => Promise<void>): Promise<void>;
|
|
11
|
+
export type RenderedPage = {
|
|
12
|
+
files: Record<string, string>;
|
|
13
|
+
} | {
|
|
14
|
+
status: number;
|
|
15
|
+
};
|
|
16
|
+
export declare function writePages(routes: string[], outputDir: string, rootRoute: string | undefined, render: (requestRoute: string) => Promise<RenderedPage>): Promise<number>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import fse from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { addLog } from '../logging-state.js';
|
|
5
|
+
import { ErrorLog } from '../logs.js';
|
|
6
|
+
const CONCURRENCY = 10;
|
|
7
|
+
export const resolveRequestRoute = (route, rootRoute) => route === '/' && rootRoute ? rootRoute : route;
|
|
8
|
+
export async function forEachConcurrent(items, task) {
|
|
9
|
+
const queue = [...items];
|
|
10
|
+
const worker = async () => {
|
|
11
|
+
for (let item = queue.shift(); item !== undefined; item = queue.shift())
|
|
12
|
+
await task(item);
|
|
13
|
+
};
|
|
14
|
+
await Promise.all(Array.from({ length: Math.min(CONCURRENCY, queue.length) }, worker));
|
|
15
|
+
}
|
|
16
|
+
export async function writePages(routes, outputDir, rootRoute, render) {
|
|
17
|
+
let failedPageCount = 0;
|
|
18
|
+
const fail = (message) => {
|
|
19
|
+
failedPageCount += 1;
|
|
20
|
+
addLog(_jsx(ErrorLog, { message: message }));
|
|
21
|
+
};
|
|
22
|
+
await forEachConcurrent(routes, async (route) => {
|
|
23
|
+
try {
|
|
24
|
+
const page = await render(resolveRequestRoute(route, rootRoute));
|
|
25
|
+
if ('status' in page)
|
|
26
|
+
return fail(`failed to generate ${route} (status ${page.status})`);
|
|
27
|
+
const pageDir = route === '/' ? outputDir : path.join(outputDir, route);
|
|
28
|
+
await fse.ensureDir(pageDir);
|
|
29
|
+
await Promise.all(Object.entries(page.files).map(([name, body]) => fse.writeFile(path.join(pageDir, name), body, 'utf8')));
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
fail(`failed to generate ${route}: ${err instanceof Error ? err.message : 'unknown error'}`);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
return failedPageCount;
|
|
36
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ExportRenderer } from './export-pages.js';
|
|
2
|
+
import { type ExportFetch } from './setupVite.js';
|
|
3
|
+
export declare const generateVitePages: (fetchPage: ExportFetch, routes: string[], outputDir: string, rootRoute?: string) => Promise<number>;
|
|
4
|
+
export declare function copyViteAssets(exportDir: string): Promise<void>;
|
|
5
|
+
export declare function bundleServerAssets(fetchAsset: ExportFetch, exportDir: string, assetPrefixes: string[]): Promise<string[]>;
|
|
6
|
+
export declare function createViteRenderer(mintExport: {
|
|
7
|
+
groups?: string[];
|
|
8
|
+
basePath?: string;
|
|
9
|
+
}): Promise<ExportRenderer>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import fse, { pathExists } from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { CLIENT_PATH } from '../constants.js';
|
|
4
|
+
import { walkFiles } from './export-assets.js';
|
|
5
|
+
import { forEachConcurrent, writePages } from './export-pages.js';
|
|
6
|
+
import { setupViteExport } from './setupVite.js';
|
|
7
|
+
const MAX_REDIRECTS = 5;
|
|
8
|
+
const EXPORT_ORIGIN = 'http://localhost';
|
|
9
|
+
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
10
|
+
const serverAssetPattern = (prefixes) => new RegExp(`(?:${prefixes.map(escapeRegExp).join('|')})[^"'()\\s]+`, 'g');
|
|
11
|
+
const exportRequest = (route) => new Request(`${EXPORT_ORIGIN}${route}`, { headers: { accept: 'text/html' } });
|
|
12
|
+
async function fetchFollowingRedirects(fetchExport, route) {
|
|
13
|
+
let res = await fetchExport(exportRequest(route));
|
|
14
|
+
for (let hops = 0; hops < MAX_REDIRECTS && res.status >= 300 && res.status < 400; hops++) {
|
|
15
|
+
const location = res.headers.get('location');
|
|
16
|
+
if (!location)
|
|
17
|
+
break;
|
|
18
|
+
const target = new URL(location, `${EXPORT_ORIGIN}${route}`);
|
|
19
|
+
if (target.origin !== EXPORT_ORIGIN)
|
|
20
|
+
break;
|
|
21
|
+
route = `${target.pathname}${target.search}`;
|
|
22
|
+
res = await fetchExport(exportRequest(route));
|
|
23
|
+
}
|
|
24
|
+
return res;
|
|
25
|
+
}
|
|
26
|
+
export const generateVitePages = (fetchPage, routes, outputDir, rootRoute) => writePages(routes, outputDir, rootRoute, async (requestRoute) => {
|
|
27
|
+
const res = await fetchFollowingRedirects(fetchPage, requestRoute);
|
|
28
|
+
if (!res.ok || !res.headers.get('content-type')?.includes('text/html')) {
|
|
29
|
+
return { status: res.status };
|
|
30
|
+
}
|
|
31
|
+
return { files: { 'index.html': await res.text() } };
|
|
32
|
+
});
|
|
33
|
+
export async function copyViteAssets(exportDir) {
|
|
34
|
+
const assetsDir = path.join(CLIENT_PATH, 'dist', 'client', '_mintlify');
|
|
35
|
+
if (!(await pathExists(assetsDir)))
|
|
36
|
+
throw new Error('client assets not found, rerun online to fetch the latest client');
|
|
37
|
+
await fse.copy(assetsDir, path.join(exportDir, '_mintlify'));
|
|
38
|
+
}
|
|
39
|
+
export async function bundleServerAssets(fetchAsset, exportDir, assetPrefixes) {
|
|
40
|
+
const pattern = serverAssetPattern(assetPrefixes);
|
|
41
|
+
const seen = new Set();
|
|
42
|
+
let pending = [];
|
|
43
|
+
const collect = (source) => {
|
|
44
|
+
for (const [assetPath] of source.matchAll(pattern)) {
|
|
45
|
+
if (seen.has(assetPath))
|
|
46
|
+
continue;
|
|
47
|
+
seen.add(assetPath);
|
|
48
|
+
pending.push(assetPath);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
for await (const file of walkFiles(exportDir)) {
|
|
52
|
+
if (file.endsWith('.html'))
|
|
53
|
+
collect(await fse.readFile(file, 'utf8'));
|
|
54
|
+
}
|
|
55
|
+
const failed = [];
|
|
56
|
+
while (pending.length > 0) {
|
|
57
|
+
const batch = pending;
|
|
58
|
+
pending = [];
|
|
59
|
+
await forEachConcurrent(batch, async (assetPath) => {
|
|
60
|
+
try {
|
|
61
|
+
const outputPath = path.join(exportDir, decodeURIComponent(assetPath));
|
|
62
|
+
if (!outputPath.startsWith(exportDir + path.sep))
|
|
63
|
+
throw new Error('outside export');
|
|
64
|
+
const res = await fetchAsset(new Request(`${EXPORT_ORIGIN}${assetPath}`));
|
|
65
|
+
if (!res.ok)
|
|
66
|
+
throw new Error(`status ${res.status}`);
|
|
67
|
+
const body = Buffer.from(await res.arrayBuffer());
|
|
68
|
+
if (res.headers.get('content-type')?.includes('text/css') || assetPath.endsWith('.css')) {
|
|
69
|
+
collect(body.toString('utf8'));
|
|
70
|
+
}
|
|
71
|
+
await fse.outputFile(outputPath, body);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
failed.push(assetPath);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return failed;
|
|
79
|
+
}
|
|
80
|
+
export async function createViteRenderer(mintExport) {
|
|
81
|
+
const { fetchPage, assetPrefixes } = await setupViteExport(mintExport);
|
|
82
|
+
return {
|
|
83
|
+
readme: 'README.md',
|
|
84
|
+
stylesheetDir: path.join('_mintlify', 'assets'),
|
|
85
|
+
generatePages: (routes, outputDir, rootRoute) => generateVitePages(fetchPage, routes, outputDir, rootRoute),
|
|
86
|
+
copyAssets: copyViteAssets,
|
|
87
|
+
bundleAssets: (exportDir) => bundleServerAssets(fetchPage, exportDir, assetPrefixes),
|
|
88
|
+
close: async () => { },
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { ArgumentsCamelCase } from 'yargs';
|
|
2
2
|
export declare const withIndexRouteAliases: (routes: string[]) => string[];
|
|
3
|
-
export declare function generatePages(baseUrl: string, routes: string[], outputDir: string, rootRoute?: string): Promise<number>;
|
|
4
3
|
export declare function writeStaticUserInfo(tmpDir: string, groups: string[] | undefined): Promise<void>;
|
|
5
4
|
export declare function writeLlmsTxt(tmpDir: string, routes: string[], basePath?: string): Promise<void>;
|
|
6
5
|
export declare const exportSite: (argv: ArgumentsCamelCase) => Promise<void>;
|