UNPKG

@mintlify/previewing

Version:

Preview Mintlify docs locally

164 lines (163 loc) • 6 kB
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); } };