UNPKG

@fesjs/fes-design

Version:
242 lines (208 loc) 7.4 kB
import path from 'node:path'; import fse from 'fs-extra'; import { getHighlighter } from 'shiki'; import cheapWatch from 'cheap-watch'; import { getProjectRootDir } from './utils.js'; import { SCRIPT_TEMPLATE, DEMO_ENTRY_FILE } from './constants.js'; const rootDir = getProjectRootDir(); const CODE_PATH = path.join( rootDir, './docs/.vitepress/theme/components/demoCode.json', ); const componentDocSrc = path.join(rootDir, './docs/.vitepress/components'); function getDemoCode() { if (fse.existsSync(CODE_PATH)) { return JSON.parse(fse.readFileSync(CODE_PATH, 'utf-8')); } return { app: DEMO_ENTRY_FILE, }; } const code = getDemoCode(); function genOutputPath(name) { return path.join(rootDir, `./docs/zh/components/${name}.md`); } function handleCompDoc(compCode, compName, demoName) { const codeName = `${compName}.${demoName}`; const codeSrc = encodeURIComponent(code[`${codeName}`]); const codeFormat = encodeURIComponent(code[`${codeName}-code`]); return compCode.replace( /<template>([\s\S]*)<\/template>/, (match, p1) => `<template><ComponentDoc codeName="${codeName}" codeSrc="${codeSrc}" codeFormat="${codeFormat}"><ClientOnly>${p1}</ClientOnly></ComponentDoc></template>`, ); } const htmlEscapes = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;', }; function escapeHtml(html) { return html.replace(/[&<>"']/g, (chr) => htmlEscapes[chr]); } let highlighter; const codeTheme = 'material-theme-palenight'; const highlight = async (code, lang = 'vue') => { if (!lang || lang === 'text') { return `<pre v-pre><code>${escapeHtml(code)}</code></pre>`; } if (!highlighter) { highlighter = await getHighlighter({ langs: ['vue'], themes: [codeTheme], }); } return highlighter .codeToHtml(code, { lang, theme: codeTheme }) .replace(/^<pre.*?>/, '<pre v-pre>'); }; async function genComponentExample(dir, name) { const output = genOutputPath(name); const indexPath = path.join(dir, 'index.md'); if (!fse.existsSync(indexPath)) return; let fileContent = fse.readFileSync(indexPath, 'utf-8'); const demos = fse.readdirSync(dir); const demoMDStrs = []; const scriptCode = { imports: [], components: [], }; const tempCode = {}; for (const filename of demos) { const fullPath = path.join(dir, filename); if ( fse.statSync(fullPath).isFile() && path.extname(fullPath) === '.vue' ) { const demoContent = []; const demoName = path.basename(fullPath, '.vue'); const compName = demoName.replace(/^\S/, (s) => s.toUpperCase()); const tempCompPath = path.join( dir, `../../.temp/components/${name}/${demoName}.vue`, ); scriptCode.imports.push( `import ${compName} from '../../.vitepress/.temp/components/${name}/${demoName}.vue'`, ); fse.outputFileSync( tempCompPath, handleCompDoc( fse.readFileSync(fullPath, 'utf-8'), name, demoName, ), ); scriptCode.components.push(compName); // 防止文档样式覆盖组件样式,详见:https://vitepress.dev/guide/markdown#raw demoContent.push(`::: raw\n<${compName} />\n:::`); const rawCode = fse.readFileSync(fullPath, 'utf-8'); tempCode[`${name}.${demoName}`] = rawCode; tempCode[`${name}.${demoName}-code`] = await highlight(rawCode); const dashMatchRegExp = new RegExp(`--${demoName}`, 'ig'); const colonMatchRegExp = new RegExp( `:::demo[\\s]*${demoName}\.vue[\\s]*:::`, 'g', ); if ( dashMatchRegExp.test(fileContent) || colonMatchRegExp.test(fileContent) ) { fileContent = fileContent .replace(dashMatchRegExp, demoContent.join('\n\n\n')) .replace(colonMatchRegExp, demoContent.join('\n\n\n')); } else { demoMDStrs.push(...demoContent); } } } const scriptStr = SCRIPT_TEMPLATE.replace( 'IMPORT_EXPRESSION', scriptCode.imports.join('\n'), ); demoMDStrs.push(scriptStr); const dashCodeMatchRegExp = new RegExp(`--CODE`); const colonCodeMatchRegExp = new RegExp(`:::code[\\s\\S]*:::`); if ( !( dashCodeMatchRegExp.test(fileContent) || colonCodeMatchRegExp.test(fileContent) ) ) { const appendContent = '\n\n:::code:::\n\n'; fileContent = fileContent + appendContent; } fse.outputFileSync( output, fileContent .replace(dashCodeMatchRegExp, demoMDStrs.join('\n\n')) .replace(colonCodeMatchRegExp, demoMDStrs.join('\n\n')), ); if (Object.keys(tempCode).length) { fse.outputFileSync( CODE_PATH, JSON.stringify(Object.assign(code, tempCode), null, 2), ); } } async function genComponents(src) { const components = fse.readdirSync(src); for (const name of components) { await genComponentExample(path.join(src, name), name); } } export async function watch(src) { const watcher = new cheapWatch({ dir: src, debounce: 50, }); await watcher.init(); const handleGen = (file) => { // 只监听目录变更 if (file.stats.isDirectory()) { const pathSeps = file.path.split(path.sep); const pkgName = pathSeps[0]; genComponentExample(path.join(src, pkgName), pkgName); } }; const handleDelete = (file) => { const pathSeps = file.path.split(path.sep); // 删除组件文档 if (pathSeps.length === 1) { const name = pathSeps[0]; let hasDeleteCode = false; Object.keys(code).forEach((key) => { if (key.startsWith(name)) { hasDeleteCode = true; delete code[key]; } }); if (hasDeleteCode) { fse.writeFileSync(CODE_PATH, JSON.stringify(code, null, 2)); } const outputPath = genOutputPath(name); if (fse.existsSync(outputPath)) { fse.unlinkSync(outputPath); } } else if (file.stats.isFile() && path.extname(file.path) === '.vue') { const pkgName = pathSeps[0]; // 删除组件属性 const codekey = `${pkgName}.${path.basename(file.path, '.vue')}`; if (code[codekey]) { delete code[codekey]; fse.writeFileSync(CODE_PATH, JSON.stringify(code, null, 2)); } genComponentExample(path.join(src, pkgName), pkgName); } }; watcher.on('+', handleGen); watcher.on('-', handleDelete); } export const genComponentDoc = async () => { await genComponents(componentDocSrc); }; export const genComponentDocWatch = async () => { await watch(componentDocSrc); }; genComponentDoc();