UNPKG

@mintlify/previewing

Version:

Preview Mintlify docs locally

285 lines (284 loc) 11.7 kB
import { jsx as _jsx } from "react/jsx-runtime"; import { findAndRemoveImports, hasImports, getFileCategory, openApiCheck, stringifyTree, processMintIgnoreString, isMintIgnored, DEFAULT_MINT_IGNORES, } from '@mintlify/common'; import { createPage, MintConfigUpdater, DocsConfigUpdater, preparseMdxTree, prebuild, } from '@mintlify/prebuild'; import Chalk from 'chalk'; import chokidar from 'chokidar'; import { promises as _promises } from 'fs'; import fse from 'fs-extra'; import fs from 'fs/promises'; import yaml from 'js-yaml'; import pathUtil from 'path'; import { CMD_EXEC_PATH, NEXT_PROPS_PATH, NEXT_PUBLIC_PATH, CLIENT_PATH } from '../../constants.js'; import { addChangeLog } from '../../logging-state.js'; import { AddedLog, DeletedLog, EditedLog, WarningLog, InfoLog } from '../../logs.js'; import { generateDependentSnippets } from './generateDependentSnippets.js'; import { generatePagesWithImports } from './generatePagesWithImports.js'; import { getDocsState } from './getDocsState.js'; import { resolveAllImports } from './resolveAllImports.js'; import { updateCustomLanguages, updateGeneratedNav, updateOpenApiFiles, upsertOpenApiFile, } from './update.js'; import { isFileSizeValid, shouldRegenerateNavForPage } from './utils.js'; const { readFile } = _promises; const frontmatterHashes = new Map(); const listener = (callback, options = {}) => { chokidar .watch(CMD_EXEC_PATH, { ignoreInitial: true, ignored: DEFAULT_MINT_IGNORES, cwd: CMD_EXEC_PATH, }) .on('add', (filename) => onAddEvent(filename, callback, options)) .on('change', (filename) => onChangeEvent(filename, callback, options)) .on('unlink', (filename) => onUnlinkEvent(filename, options)); }; const getMintIgnoreGlobs = () => { const mintIgnorePath = pathUtil.join(CMD_EXEC_PATH, '.mintignore'); if (fse.existsSync(mintIgnorePath)) { const content = fse.readFileSync(mintIgnorePath, 'utf8'); return processMintIgnoreString(content); } return []; }; const onAddEvent = async (filename, callback, options) => { if (isMintIgnored(filename, getMintIgnoreGlobs())) { return; } try { await onUpdateEvent(filename, callback, options); addChangeLog(_jsx(AddedLog, { filename: filename })); } catch (error) { console.error(error.message); } }; const onChangeEvent = async (filename, callback, options) => { if (isMintIgnored(filename, getMintIgnoreGlobs())) { return; } try { await onUpdateEvent(filename, callback, options); addChangeLog(_jsx(EditedLog, { filename: filename })); } catch (error) { console.error(error.message); } }; const onUnlinkEvent = async (filename, options) => { if (isMintIgnored(filename, getMintIgnoreGlobs())) { return; } try { const potentialCategory = getFileCategory(filename); const targetPath = getTargetPath(potentialCategory, filename); if (potentialCategory === 'page' || potentialCategory === 'snippet' || potentialCategory === 'mintConfig' || potentialCategory === 'docsConfig' || potentialCategory === 'staticFile' || potentialCategory === 'snippet-v2' || potentialCategory === 'css' || potentialCategory === 'js' || potentialCategory === 'generatedStaticFile') { await fse.remove(targetPath); } switch (potentialCategory) { case 'mintConfig': console.error('mint.json has been deleted.'); await validateConfigFiles(); break; case 'docsConfig': console.error('docs.json has been deleted.'); await validateConfigFiles(); break; case 'mintIgnore': addChangeLog(_jsx(WarningLog, { message: ".mintignore has been deleted. Rebuilding..." })); try { await fse.emptyDir(NEXT_PUBLIC_PATH); await fse.emptyDir(NEXT_PROPS_PATH); await prebuild(CMD_EXEC_PATH, options); } catch (err) { console.error('Error rebuilding after .mintignore deletion:', err); } break; } addChangeLog(_jsx(DeletedLog, { filename: filename })); } catch (error) { console.error(error.message); } }; const getTargetPath = (potentialCategory, filePath) => { switch (potentialCategory) { case 'page': return pathUtil.join(NEXT_PROPS_PATH, filePath); case 'mintConfig': return pathUtil.join(NEXT_PROPS_PATH, 'mint.json'); case 'docsConfig': return pathUtil.join(NEXT_PROPS_PATH, 'docs.json'); case 'potentialYamlOpenApiSpec': case 'potentialJsonOpenApiSpec': return pathUtil.join(NEXT_PROPS_PATH, 'openApiFiles.json'); case 'generatedStaticFile': return pathUtil.join(NEXT_PUBLIC_PATH, filePath); case 'mintIgnore': return pathUtil.join(NEXT_PROPS_PATH, 'mint-ignore.json'); case 'snippet': case 'staticFile': case 'snippet-v2': case 'css': case 'js': return pathUtil.join(NEXT_PUBLIC_PATH, filePath); default: throw new Error('Invalid category'); } }; const validateConfigFiles = async () => { try { const mintConfigPath = pathUtil.join(CMD_EXEC_PATH, 'mint.json'); const docsConfigPath = pathUtil.join(CMD_EXEC_PATH, 'docs.json'); const mintConfigExists = await fse.pathExists(mintConfigPath); const docsConfigExists = await fse.pathExists(docsConfigPath); if (!mintConfigExists && !docsConfigExists) { console.error('⚠️ Error: Neither mint.json nor docs.json found in the directory'); process.exit(1); } } catch (error) { console.error('⚠️ Error validating configuration files:', error); } }; /** * This function is called when a file is added or changed * @param filename * @returns FileCategory */ const onUpdateEvent = async (filename, callback, options = {}) => { const filePath = pathUtil.join(CMD_EXEC_PATH, filename); const potentialCategory = getFileCategory(filename); const targetPath = getTargetPath(potentialCategory, filename); let regenerateNav = false; let category = potentialCategory === 'potentialYamlOpenApiSpec' || potentialCategory === 'potentialJsonOpenApiSpec' ? 'staticFile' : potentialCategory; switch (potentialCategory) { case 'page': { let contentStr = (await readFile(filePath)).toString(); regenerateNav = await shouldRegenerateNavForPage(filename, contentStr, frontmatterHashes); const tree = await preparseMdxTree(contentStr, CMD_EXEC_PATH, filePath); const importsResponse = await findAndRemoveImports(tree); if (hasImports(importsResponse)) { contentStr = stringifyTree(await resolveAllImports({ ...importsResponse, filename })); } // set suppressErrLog true here to avoid double logging errors already logged in preparseMdxTree const { pageContent } = await createPage(filename, contentStr, CMD_EXEC_PATH, [], [], true); await fse.outputFile(targetPath, pageContent, { flag: 'w', }); break; } case 'snippet': { await fse.copy(filePath, targetPath); break; } case 'snippet-v2': { let contentStr = (await readFile(filePath)).toString(); const tree = await preparseMdxTree(contentStr, CMD_EXEC_PATH, filePath); const importsResponse = await findAndRemoveImports(tree); if (hasImports(importsResponse)) { contentStr = stringifyTree(await resolveAllImports({ ...importsResponse, filename })); } await fse.outputFile(targetPath, contentStr, { flag: 'w', }); const updatedSnippets = await generateDependentSnippets(filename, importsResponse); await generatePagesWithImports(new Set(updatedSnippets)); break; } case 'mintConfig': case 'docsConfig': { regenerateNav = true; try { const { mintConfig, openApiFiles, docsConfig } = await getDocsState(); if (mintConfig) { await MintConfigUpdater.writeConfigFile(mintConfig, CLIENT_PATH); } await DocsConfigUpdater.writeConfigFile(docsConfig, CLIENT_PATH); await updateOpenApiFiles(openApiFiles); await updateCustomLanguages(docsConfig); } catch (err) { console.error(err); } break; } case 'mintIgnore': { try { addChangeLog(_jsx(InfoLog, { message: ".mintignore has been updated. Rebuilding..." })); await fse.emptyDir(NEXT_PUBLIC_PATH); await fse.emptyDir(NEXT_PROPS_PATH); await prebuild(CMD_EXEC_PATH, options); } catch (err) { console.error(err.message); } break; } case 'potentialYamlOpenApiSpec': case 'potentialJsonOpenApiSpec': { let doc; try { const file = await fs.readFile(filePath, 'utf-8'); doc = await openApiCheck(yaml.load(file)); } catch { doc = undefined; } if (doc) { await upsertOpenApiFile({ filename: pathUtil.parse(filename).name, originalFileLocation: '/' + filename, spec: doc, }); await updateOpenApiFiles(); regenerateNav = true; category = 'openApi'; } else { const customLanguages = JSON.parse(await fs.readFile(pathUtil.join(NEXT_PROPS_PATH, 'customLanguages.json'), 'utf-8')); let updated = false; for (const [index, customLanguage] of customLanguages.entries()) { if (filePath.endsWith(customLanguage.filePath)) { updated = true; const file = await fs.readFile(filePath, 'utf-8'); customLanguages[index] = { content: file, filePath: customLanguage.filePath }; break; } } if (updated) { await fs.writeFile(pathUtil.join(NEXT_PROPS_PATH, 'customLanguages.json'), JSON.stringify(customLanguages, null, 2)); } } break; } case 'css': case 'js': case 'generatedStaticFile': case 'staticFile': { if (await isFileSizeValid(filePath, 20)) { await fse.copy(filePath, targetPath); } else { console.error(Chalk.red(`🚨 The file at ${filename} is too big. The maximum file size is 20 mb.`)); } break; } } if (regenerateNav) { // TODO: Instead of re-generating the entire nav, optimize by just updating the specific page that changed. await updateGeneratedNav(); } callback(); return category; }; export default listener;