@mintlify/previewing
Version:
Preview Mintlify docs locally
285 lines (284 loc) • 11.7 kB
JavaScript
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;