@mintlify/previewing
Version:
Preview Mintlify docs locally
164 lines (163 loc) • 6 kB
JavaScript
import { findAndRemoveImports, getFileCategory, hasImports, isMintIgnored, stringifyTree, } from '@mintlify/common';
import { createPage, preparseMdxTree } from '@mintlify/prebuild';
import { promises as _promises } from 'fs';
import fse from 'fs-extra';
import pathUtil from 'path';
import { CMD_EXEC_PATH, NEXT_PROPS_PATH } from '../constants.js';
import { startDevTiming } from './dev-timing.js';
import { getImportedFilesFromCache } from './listener/importCache.js';
import { upsertPageMetadataCacheEntry } from './listener/page-metadata-cache.js';
import { resolvePageImports } from './listener/resolve-page-imports.js';
import { getMintIgnoreGlobs, handleParseError } from './listener/utils.js';
const { readFile } = _promises;
let resolveReady = () => { };
const ready = new Promise((resolve) => {
resolveReady = resolve;
});
export const markOnDemandPagesReady = () => {
resolveReady();
};
let compileListener;
export const setCompileListener = (listener) => {
compileListener = listener;
};
const pagePathForFilename = (filename) => {
return `/${filename.replace(/\.mdx?$/, '')}`;
};
const trackCompile = async (filename, task) => {
const path = pagePathForFilename(filename);
compileListener?.({ path, active: true });
let failed = false;
try {
failed = await task();
}
finally {
compileListener?.({ path, active: false, error: failed || undefined });
}
};
const inflight = new Map();
const SKIPPED_PREFIXES = ['_next/', '_mintlify/', 'socket.io/', 'favicon'];
const decodePathname = (pathname) => {
try {
return decodeURIComponent(pathname);
}
catch {
return pathname;
}
};
const slugFromPathname = (pathname) => {
let slug = decodePathname(pathname).replace(/^\/+/, '').replace(/\/+$/, '');
if (slug.startsWith('_markdown/')) {
slug = slug.slice('_markdown/'.length);
}
if (slug === '') {
return 'index';
}
if (slug.includes('\0') ||
slug.includes('\\') ||
slug.split('/').some((segment) => segment === '..')) {
return undefined;
}
if (SKIPPED_PREFIXES.some((prefix) => slug.startsWith(prefix))) {
return undefined;
}
return slug;
};
const candidateFilenames = (slug) => [
`${slug}.mdx`,
`${slug}.md`,
`${slug}/index.mdx`,
`${slug}/index.md`,
];
const failedCompiles = new Map();
export const getFailedCompileEvents = () => Array.from(failedCompiles.keys()).map((filename) => ({
path: pagePathForFilename(filename),
active: false,
error: true,
}));
export const reportPageCompileSuccess = (filename) => {
if (!failedCompiles.delete(filename))
return;
compileListener?.({ path: pagePathForFilename(filename), active: false });
};
export const reportPageCompileError = (filename) => {
failedCompiles.set(filename, 0);
compileListener?.({ path: pagePathForFilename(filename), active: false, error: true });
};
const compilePage = async (filename, sourcePath, targetPath, sourceMtimeMs) => {
const finish = startDevTiming(`compile ${filename}`);
let hasParseError = false;
const captureParseError = () => {
hasParseError = true;
};
try {
let contentStr = (await readFile(sourcePath)).toString();
const tree = await preparseMdxTree(contentStr, CMD_EXEC_PATH, sourcePath, captureParseError);
const importsResponse = await findAndRemoveImports(tree);
if (hasImports(importsResponse)) {
contentStr = stringifyTree(await resolvePageImports({ ...importsResponse, filename }));
}
const { pageContent, pageMetadata, slug } = await createPage(filename, contentStr, CMD_EXEC_PATH, [], [], captureParseError);
await fse.outputFile(targetPath, pageContent, { flag: 'w' });
upsertPageMetadataCacheEntry(slug, pageMetadata);
if (hasParseError) {
failedCompiles.set(filename, sourceMtimeMs);
finish('failed');
return true;
}
failedCompiles.delete(filename);
finish();
return false;
}
catch (error) {
failedCompiles.set(filename, sourceMtimeMs);
handleParseError(`Failed to compile ${filename}: ${error instanceof Error ? error.message : String(error)}`);
finish('failed');
return true;
}
};
export const ensurePageCompiled = async (pathname, options = {}) => {
const slug = slugFromPathname(pathname);
if (slug === undefined) {
return;
}
for (const filename of candidateFilenames(slug)) {
const sourcePath = pathUtil.join(CMD_EXEC_PATH, filename);
if (!(await fse.pathExists(sourcePath))) {
continue;
}
if (isMintIgnored(filename, getMintIgnoreGlobs())) {
continue;
}
const sourceStat = await fse.stat(sourcePath);
const targetPath = pathUtil.join(NEXT_PROPS_PATH, filename);
const targetStat = await fse.stat(targetPath).catch(() => undefined);
if (failedCompiles.get(filename) === sourceStat.mtimeMs) {
if (!options.silent) {
compileListener?.({
path: pagePathForFilename(filename),
active: false,
error: true,
});
}
return;
}
if (targetStat !== undefined && targetStat.mtimeMs >= sourceStat.mtimeMs) {
return;
}
await ready;
if (getFileCategory(filename, { importedFiles: getImportedFilesFromCache() }) !== 'page') {
continue;
}
const pending = inflight.get(filename) ??
compilePage(filename, sourcePath, targetPath, sourceStat.mtimeMs).finally(() => {
inflight.delete(filename);
});
inflight.set(filename, pending);
if (options.silent) {
await pending;
return;
}
return trackCompile(filename, () => pending);
}
};