@mintlify/previewing 4.0.1195 → 4.0.1196
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 +1 -0
- package/dist/local-preview/index.js +37 -2
- package/dist/local-preview/listener/index.d.ts +1 -1
- package/dist/local-preview/listener/index.js +12 -1
- package/dist/local-preview/on-demand-page.d.ts +11 -0
- package/dist/local-preview/on-demand-page.js +126 -0
- package/dist/local-preview/run.js +24 -2
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
|
@@ -1,13 +1,32 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { prebuild } from '@mintlify/prebuild';
|
|
2
|
+
import { getFileListSync, prebuild } from '@mintlify/prebuild';
|
|
3
3
|
import fse, { pathExists } from 'fs-extra';
|
|
4
4
|
import isOnline from 'is-online';
|
|
5
|
+
import pathUtil from 'path';
|
|
5
6
|
import { CLIENT_PATH, DOT_MINTLIFY, CMD_EXEC_PATH, VERSION_PATH, NEXT_PUBLIC_PATH, NEXT_PROPS_PATH, } from '../constants.js';
|
|
6
7
|
import { addLog, clearLogs } from '../logging-state.js';
|
|
7
8
|
import { ErrorLog, SpinnerLog } from '../logs.js';
|
|
8
9
|
import { timeDev, timeDevSync } from './dev-timing.js';
|
|
9
10
|
import { run } from './run.js';
|
|
10
11
|
import { silentUpdateClient } from './update.js';
|
|
12
|
+
const PRUNE_EXEMPT_PREFIXES = ['favicons/', '.mint-content-source'];
|
|
13
|
+
const prunePublicOrphans = () => {
|
|
14
|
+
let publicFiles;
|
|
15
|
+
try {
|
|
16
|
+
publicFiles = getFileListSync(NEXT_PUBLIC_PATH);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
for (const relativePath of publicFiles) {
|
|
22
|
+
if (PRUNE_EXEMPT_PREFIXES.some((prefix) => relativePath.startsWith(prefix))) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (!fse.existsSync(pathUtil.join(CMD_EXEC_PATH, relativePath))) {
|
|
26
|
+
fse.removeSync(pathUtil.join(NEXT_PUBLIC_PATH, relativePath));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
11
30
|
const dev = async (argv) => {
|
|
12
31
|
const hasInternet = await timeDev('check internet', () => isOnline());
|
|
13
32
|
const localSchema = argv['local-schema'];
|
|
@@ -42,8 +61,23 @@ const dev = async (argv) => {
|
|
|
42
61
|
}
|
|
43
62
|
// clear preexisting prebuild files
|
|
44
63
|
timeDevSync('clear prebuild output', () => {
|
|
45
|
-
fse.emptyDirSync(NEXT_PUBLIC_PATH);
|
|
46
64
|
fse.emptyDirSync(NEXT_PROPS_PATH);
|
|
65
|
+
// only clear public/ when previewing a different docs repo, so unchanged
|
|
66
|
+
// static files can be skipped on warm starts; on warm starts, prune files
|
|
67
|
+
// whose source no longer exists so deleted assets stop being served
|
|
68
|
+
const markerPath = pathUtil.join(NEXT_PUBLIC_PATH, '.mint-content-source');
|
|
69
|
+
let previousSource;
|
|
70
|
+
try {
|
|
71
|
+
previousSource = fse.readFileSync(markerPath, 'utf8');
|
|
72
|
+
}
|
|
73
|
+
catch { }
|
|
74
|
+
if (previousSource !== CMD_EXEC_PATH) {
|
|
75
|
+
fse.emptyDirSync(NEXT_PUBLIC_PATH);
|
|
76
|
+
fse.outputFileSync(markerPath, CMD_EXEC_PATH);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
prunePublicOrphans();
|
|
80
|
+
}
|
|
47
81
|
});
|
|
48
82
|
timeDevSync('switch to local client directory', () => {
|
|
49
83
|
process.chdir(CLIENT_PATH);
|
|
@@ -55,6 +89,7 @@ const dev = async (argv) => {
|
|
|
55
89
|
groups,
|
|
56
90
|
disableOpenApi,
|
|
57
91
|
allowSourceRefs: true,
|
|
92
|
+
lazyPages: true,
|
|
58
93
|
}));
|
|
59
94
|
fileImportsMap = result?.fileImportsMap;
|
|
60
95
|
}
|
|
@@ -4,7 +4,7 @@ type LocalPreviewOptions = {
|
|
|
4
4
|
groups?: string[];
|
|
5
5
|
allowSourceRefs?: boolean;
|
|
6
6
|
};
|
|
7
|
-
declare const listener: (triggerRefresh: () => void, options?: LocalPreviewOptions) => void;
|
|
7
|
+
declare const listener: (triggerRefresh: () => void, options?: LocalPreviewOptions, gate?: Promise<void>) => void;
|
|
8
8
|
declare const initializeFrontmatterHashCache: () => Promise<void>;
|
|
9
9
|
declare const initializeDocsConfigHashCache: () => Promise<void>;
|
|
10
10
|
export { initializeDocsConfigHashCache, initializeFrontmatterHashCache, initializeImportCache };
|
|
@@ -81,7 +81,9 @@ const addUpdateTimingLog = (filename, category, durationMs) => {
|
|
|
81
81
|
const categoryLabel = category == null ? 'uncategorized' : category;
|
|
82
82
|
addChangeLog(_jsx(InfoLog, { message: `processed ${filename} in ${formatElapsedSeconds(durationMs)} (${categoryLabel})` }));
|
|
83
83
|
};
|
|
84
|
-
|
|
84
|
+
let watcherGate;
|
|
85
|
+
const listener = (triggerRefresh, options = {}, gate) => {
|
|
86
|
+
watcherGate = gate;
|
|
85
87
|
const previewOptions = { ...options, allowSourceRefs: true };
|
|
86
88
|
const mintIgnoreGlobs = getMintIgnoreGlobs();
|
|
87
89
|
chokidar
|
|
@@ -103,6 +105,9 @@ const listener = (triggerRefresh, options = {}) => {
|
|
|
103
105
|
.on('unlink', (filename) => onUnlinkEvent(filename, triggerRefresh, previewOptions));
|
|
104
106
|
};
|
|
105
107
|
const onAddEvent = async (filename, triggerRefresh, options) => {
|
|
108
|
+
if (watcherGate) {
|
|
109
|
+
await watcherGate;
|
|
110
|
+
}
|
|
106
111
|
if (isMintIgnored(filename, getMintIgnoreGlobs())) {
|
|
107
112
|
return;
|
|
108
113
|
}
|
|
@@ -119,6 +124,9 @@ const onAddEvent = async (filename, triggerRefresh, options) => {
|
|
|
119
124
|
}
|
|
120
125
|
};
|
|
121
126
|
const onChangeEvent = async (filename, triggerRefresh, options) => {
|
|
127
|
+
if (watcherGate) {
|
|
128
|
+
await watcherGate;
|
|
129
|
+
}
|
|
122
130
|
if (isMintIgnored(filename, getMintIgnoreGlobs())) {
|
|
123
131
|
return;
|
|
124
132
|
}
|
|
@@ -135,6 +143,9 @@ const onChangeEvent = async (filename, triggerRefresh, options) => {
|
|
|
135
143
|
}
|
|
136
144
|
};
|
|
137
145
|
const onUnlinkEvent = async (filename, triggerRefresh, options) => {
|
|
146
|
+
if (watcherGate) {
|
|
147
|
+
await watcherGate;
|
|
148
|
+
}
|
|
138
149
|
if (isMintIgnored(filename, getMintIgnoreGlobs())) {
|
|
139
150
|
return;
|
|
140
151
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const markOnDemandPagesReady: () => void;
|
|
2
|
+
export type CompileEvent = {
|
|
3
|
+
path: string;
|
|
4
|
+
active: boolean;
|
|
5
|
+
};
|
|
6
|
+
type CompileListener = (event: CompileEvent) => void;
|
|
7
|
+
export declare const setCompileListener: (listener: CompileListener) => void;
|
|
8
|
+
export declare const ensurePageCompiled: (pathname: string, options?: {
|
|
9
|
+
silent?: boolean;
|
|
10
|
+
}) => Promise<void>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { findAndRemoveImports, getFileCategory, hasImports, isMintIgnored, stringifyTree, } from '@mintlify/common';
|
|
2
|
+
import { createPage, preparseMdxTree } from '@mintlify/prebuild';
|
|
3
|
+
import { promises as _promises } from 'fs';
|
|
4
|
+
import fse from 'fs-extra';
|
|
5
|
+
import pathUtil from 'path';
|
|
6
|
+
import { CMD_EXEC_PATH, NEXT_PROPS_PATH } from '../constants.js';
|
|
7
|
+
import { startDevTiming } from './dev-timing.js';
|
|
8
|
+
import { getImportedFilesFromCache } from './listener/importCache.js';
|
|
9
|
+
import { upsertPageMetadataCacheEntry } from './listener/page-metadata-cache.js';
|
|
10
|
+
import { resolvePageImports } from './listener/resolve-page-imports.js';
|
|
11
|
+
import { getMintIgnoreGlobs, handleParseError, suppressParseError } from './listener/utils.js';
|
|
12
|
+
const { readFile } = _promises;
|
|
13
|
+
let resolveReady = () => { };
|
|
14
|
+
const ready = new Promise((resolve) => {
|
|
15
|
+
resolveReady = resolve;
|
|
16
|
+
});
|
|
17
|
+
export const markOnDemandPagesReady = () => {
|
|
18
|
+
resolveReady();
|
|
19
|
+
};
|
|
20
|
+
let compileListener;
|
|
21
|
+
export const setCompileListener = (listener) => {
|
|
22
|
+
compileListener = listener;
|
|
23
|
+
};
|
|
24
|
+
const pagePathForFilename = (filename) => {
|
|
25
|
+
return `/${filename.replace(/\.mdx?$/, '')}`;
|
|
26
|
+
};
|
|
27
|
+
const trackCompile = async (filename, task) => {
|
|
28
|
+
const path = pagePathForFilename(filename);
|
|
29
|
+
compileListener?.({ path, active: true });
|
|
30
|
+
try {
|
|
31
|
+
return await task();
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
compileListener?.({ path, active: false });
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const inflight = new Map();
|
|
38
|
+
const SKIPPED_PREFIXES = ['_next/', '_mintlify/', 'socket.io/', 'favicon'];
|
|
39
|
+
const decodePathname = (pathname) => {
|
|
40
|
+
try {
|
|
41
|
+
return decodeURIComponent(pathname);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return pathname;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const slugFromPathname = (pathname) => {
|
|
48
|
+
let slug = decodePathname(pathname).replace(/^\/+/, '').replace(/\/+$/, '');
|
|
49
|
+
if (slug.startsWith('_markdown/')) {
|
|
50
|
+
slug = slug.slice('_markdown/'.length);
|
|
51
|
+
}
|
|
52
|
+
if (slug === '') {
|
|
53
|
+
return 'index';
|
|
54
|
+
}
|
|
55
|
+
if (slug.includes('\0') ||
|
|
56
|
+
slug.includes('\\') ||
|
|
57
|
+
slug.split('/').some((segment) => segment === '..')) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
if (SKIPPED_PREFIXES.some((prefix) => slug.startsWith(prefix))) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
return slug;
|
|
64
|
+
};
|
|
65
|
+
const candidateFilenames = (slug) => [
|
|
66
|
+
`${slug}.mdx`,
|
|
67
|
+
`${slug}.md`,
|
|
68
|
+
`${slug}/index.mdx`,
|
|
69
|
+
`${slug}/index.md`,
|
|
70
|
+
];
|
|
71
|
+
const failedCompiles = new Map();
|
|
72
|
+
const compilePage = async (filename, sourcePath, targetPath, sourceMtimeMs) => {
|
|
73
|
+
const finish = startDevTiming(`compile ${filename}`);
|
|
74
|
+
try {
|
|
75
|
+
let contentStr = (await readFile(sourcePath)).toString();
|
|
76
|
+
const tree = await preparseMdxTree(contentStr, CMD_EXEC_PATH, sourcePath, suppressParseError);
|
|
77
|
+
const importsResponse = await findAndRemoveImports(tree);
|
|
78
|
+
if (hasImports(importsResponse)) {
|
|
79
|
+
contentStr = stringifyTree(await resolvePageImports({ ...importsResponse, filename }));
|
|
80
|
+
}
|
|
81
|
+
const { pageContent, pageMetadata, slug } = await createPage(filename, contentStr, CMD_EXEC_PATH, [], [], suppressParseError);
|
|
82
|
+
await fse.outputFile(targetPath, pageContent, { flag: 'w' });
|
|
83
|
+
upsertPageMetadataCacheEntry(slug, pageMetadata);
|
|
84
|
+
failedCompiles.delete(filename);
|
|
85
|
+
finish();
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
failedCompiles.set(filename, sourceMtimeMs);
|
|
89
|
+
handleParseError(`Failed to compile ${filename}: ${error instanceof Error ? error.message : String(error)}`);
|
|
90
|
+
finish('failed');
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
export const ensurePageCompiled = async (pathname, options = {}) => {
|
|
94
|
+
const slug = slugFromPathname(pathname);
|
|
95
|
+
if (slug === undefined) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
for (const filename of candidateFilenames(slug)) {
|
|
99
|
+
const sourcePath = pathUtil.join(CMD_EXEC_PATH, filename);
|
|
100
|
+
if (!(await fse.pathExists(sourcePath))) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (isMintIgnored(filename, getMintIgnoreGlobs())) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const sourceStat = await fse.stat(sourcePath);
|
|
107
|
+
const targetPath = pathUtil.join(NEXT_PROPS_PATH, filename);
|
|
108
|
+
const targetStat = await fse.stat(targetPath).catch(() => undefined);
|
|
109
|
+
if (targetStat !== undefined && targetStat.mtimeMs >= sourceStat.mtimeMs) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
await ready;
|
|
113
|
+
if (getFileCategory(filename, { importedFiles: getImportedFilesFromCache() }) !== 'page') {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (failedCompiles.get(filename) === sourceStat.mtimeMs) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const pending = inflight.get(filename) ??
|
|
120
|
+
compilePage(filename, sourcePath, targetPath, sourceStat.mtimeMs).finally(() => {
|
|
121
|
+
inflight.delete(filename);
|
|
122
|
+
});
|
|
123
|
+
inflight.set(filename, pending);
|
|
124
|
+
return options.silent ? pending : trackCompile(filename, () => pending);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
@@ -11,6 +11,7 @@ import { addDevTimingLog, startDevTiming, timeDev } from './dev-timing.js';
|
|
|
11
11
|
import listener, { initializeDocsConfigHashCache, initializeFrontmatterHashCache, initializeImportCache, } from './listener/index.js';
|
|
12
12
|
import { refreshTrackedReferencedFiles } from './listener/referencedFiles.js';
|
|
13
13
|
import { getLocalNetworkIp } from './network.js';
|
|
14
|
+
import { ensurePageCompiled, markOnDemandPagesReady, setCompileListener } from './on-demand-page.js';
|
|
14
15
|
import { setupNext } from './setupNext.js';
|
|
15
16
|
const PROXY_RESPONSE_HEADER_DENYLIST = new Set([
|
|
16
17
|
'connection',
|
|
@@ -60,6 +61,16 @@ export const run = async (argv) => {
|
|
|
60
61
|
process.env.MINTLIFY_CLI_SUBDOMAIN = subdomain;
|
|
61
62
|
// next-server is bugged, public files added after starting aren't served
|
|
62
63
|
app.use('/', express.static(NEXT_PUBLIC_PATH));
|
|
64
|
+
app.use((req, _res, next) => {
|
|
65
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
66
|
+
next();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const silent = req.headers['next-router-prefetch'] !== undefined ||
|
|
70
|
+
req.headers['purpose'] === 'prefetch' ||
|
|
71
|
+
req.headers['sec-purpose'] === 'prefetch';
|
|
72
|
+
ensurePageCompiled(req.path, { silent }).then(() => next(), () => next());
|
|
73
|
+
});
|
|
63
74
|
app.post('/_mintlify/api-public/search/:subdomain', express.json(), async (req, res) => {
|
|
64
75
|
const targetSubdomain = subdomain ?? req.params.subdomain;
|
|
65
76
|
try {
|
|
@@ -121,6 +132,9 @@ export const run = async (argv) => {
|
|
|
121
132
|
const onChange = () => {
|
|
122
133
|
io.emit('reload');
|
|
123
134
|
};
|
|
135
|
+
setCompileListener((event) => {
|
|
136
|
+
io.emit('compiling', event);
|
|
137
|
+
});
|
|
124
138
|
const finishServerListenTiming = startDevTiming('server ready');
|
|
125
139
|
server.listen(currentPort, () => {
|
|
126
140
|
finishServerListenTiming();
|
|
@@ -147,10 +161,18 @@ export const run = async (argv) => {
|
|
|
147
161
|
process.on('SIGINT', onExit);
|
|
148
162
|
process.on('SIGTERM', onExit);
|
|
149
163
|
});
|
|
164
|
+
// start watching immediately so edits made during cache initialization are
|
|
165
|
+
// not lost; handlers wait on the gate until the caches they rely on exist
|
|
166
|
+
let releaseWatcherGate = () => { };
|
|
167
|
+
const watcherGate = new Promise((resolve) => {
|
|
168
|
+
releaseWatcherGate = resolve;
|
|
169
|
+
});
|
|
170
|
+
addDevTimingLog('watching for file changes');
|
|
171
|
+
listener(onChange, {}, watcherGate);
|
|
150
172
|
await timeDev('initialize import cache', () => initializeImportCache(CMD_EXEC_PATH, argv.fileImportsMap));
|
|
173
|
+
markOnDemandPagesReady();
|
|
151
174
|
await timeDev('initialize frontmatter cache', () => initializeFrontmatterHashCache());
|
|
152
175
|
await timeDev('initialize docs config cache', () => initializeDocsConfigHashCache());
|
|
153
176
|
await timeDev('refresh referenced files', () => refreshTrackedReferencedFiles());
|
|
154
|
-
|
|
155
|
-
listener(onChange);
|
|
177
|
+
releaseWatcherGate();
|
|
156
178
|
};
|