@markdown-vue/mdv
Version:
Markdown-Vue (MDV) lets you write Vue-style components directly inside Markdown files.
258 lines (216 loc) โข 9.32 kB
text/typescript
import path from "path";
import { compileMDV, generateComponentsModule, generateGlobalComponentsModule } from "@mdv/parser";
import { MDVPluginOptions } from "@mdv/types/mdv-config";
import fs from "fs";
import chokidar from "chokidar";
export const Compiler = (options: MDVPluginOptions) => {
const extension = ".v.md";
const cacheDirName = options.cacheDir || ".mdv";
const cacheDir = path.resolve(cacheDirName);
const srcName = options.srcRoot || "src";
const srcRoot = path.resolve(srcName);
const skipCleanup = options.skipCleanup;
if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });
const mdvMeta = new Map<string, any>();
const compiledTimestamps = new Map<string, number>();
function getCachePaths(file: string) {
const vueCachePath = path
.join(cacheDir, path.relative(srcRoot, file))
.replace(/\.v\.md$/, ".vue");
return {
vue: vueCachePath,
json: vueCachePath.replace(/\.vue$/, ".mdv.json"),
shiki: vueCachePath.replace(/\.vue$/, ".shiki.js"),
};
}
function cleanupCacheFiles(file: string) {
const { vue, json, shiki } = getCachePaths(file);
[vue, json, shiki].forEach((f) => {
if (fs.existsSync(f)) {
fs.unlinkSync(f);
console.log(`--๐งน Removed cache: ${path.relative(process.cwd(), f)}`);
}
});
mdvMeta.delete(file);
compiledTimestamps.delete(file);
}
async function compileMDVFile(file: string, viteServer?: any) {
if (!fs.existsSync(file)) return;
try {
const stats = fs.statSync(file);
const lastModified = stats.mtimeMs;
if (compiledTimestamps.get(file) === lastModified) return;
console.log(`--๐จ Compiling: ${path.relative(process.cwd(), file)}`);
const { vue, json, shiki } = getCachePaths(file);
const vueDir = path.dirname(vue);
if (!fs.existsSync(vueDir)) fs.mkdirSync(vueDir, { recursive: true });
const componentsDir = path.join(cacheDir, "components");
const componentsDirRelative = path.relative(path.dirname(vue), componentsDir);
const mdContent = fs.readFileSync(file, "utf-8");
const { content, meta, shikis } = await compileMDV(
mdContent,
path.relative(cacheDir, json).replace(/\\/g, "/"),
path.relative(path.join(cacheDir, "components"), shiki).replace(/\\/g, "/"),
componentsDirRelative.toLowerCase().replace(/\\/g, "/"),
{
customComponents: {},
}
);
mdvMeta.set(file, meta);
fs.writeFileSync(vue, content, "utf-8");
fs.writeFileSync(json, JSON.stringify(meta, null, 2), "utf-8");
if (Object.keys(shikis).length > 0)
fs.writeFileSync(shiki, `export default ${JSON.stringify(shikis)}`);
compiledTimestamps.set(file, lastModified);
if (viteServer) {
const mod = viteServer.moduleGraph.getModuleById("\0mdv:" + file);
if (mod) viteServer.moduleGraph.invalidateModule(mod);
}
console.log(`--โ
Compiled: ${path.relative(process.cwd(), vue)}`);
return content;
}
catch (e) {
console.error(e);
}
}
async function compileAllMDVFiles(dir: string = srcRoot, viteServer?: any) {
// cleanup orphaned cache files
const cleanOrphans = (dir: string) => {
const files = fs.readdirSync(dir, { withFileTypes: true });
for (const f of files) {
const fullPath = path.join(dir, f.name);
if (f.isDirectory()) {
cleanOrphans(fullPath);
} else if (f.isFile() && f.name.endsWith(".vue")) {
const originalFile = path.join(
srcRoot,
path.relative(cacheDir, fullPath).replace(/\.vue$/, ".v.md"),
);
if (!fs.existsSync(originalFile)) {
cleanupCacheFiles(originalFile);
}
}
}
};
if (!skipCleanup) cleanOrphans(dir);
try {
// now compile everything
const files = fs.readdirSync(dir, { withFileTypes: true });
for (const file of files) {
const fullPath = path.join(dir, file.name);
if (file.isDirectory()) {
await compileAllMDVFiles(fullPath, viteServer);
} else if (file.isFile() && file.name.endsWith(extension)) {
await compileMDVFile(fullPath, viteServer);
}
}
} catch (e) {
console.error(e);
}
}
async function watchAll(dir: string = srcRoot, viteServer?: any) {
console.log(`๐ Watching for changes in ${dir}...`);
const watcher = chokidar.watch(dir);
watcher
.on("add", async (file) => {
if (!file.endsWith(extension)) return;
try {
await compileMDVFile(file, viteServer);
} catch (e) {
console.error(e);
}
})
.on("change", async (file) => {
if (!file.endsWith(extension)) return;
try {
await compileMDVFile(file, viteServer);
} catch (e) {
console.error(e);
}
})
.on("unlink", (file) => {
if (!file.endsWith(extension)) return;
try {
cleanupCacheFiles(file);
} catch (e) {
console.error(e);
}
console.log(`--๐๏ธ MDV removed: ${file}`);
});
}
/**
* Scan a directory for .v.md files, generate GlobalComponents declaration, write to .mdv folder
*/
function writeGlobalComponentsDTS(dir: string = srcRoot) {
console.log(`--๐ช Generating global components typings...`);
const mdvFiles = getMdvFiles(dir);
// Get TS module string from parser
const content = generateGlobalComponentsModule(
mdvFiles.map((f) => `./${path.relative(srcRoot, f).replace(/\\/g, "/")}`),
);
// Write to .d.ts
fs.mkdirSync(cacheDir, { recursive: true });
const dtsPath = path.join(cacheDir, "mdv-global-components.d.ts");
fs.writeFileSync(dtsPath, content, "utf-8");
}
/**
* Scan a directory for .vue files, generate Components declaration, write to .mdv folder
*
* @param dir
*/
function writeComponentsDTS(dir: string = srcRoot) {
console.log(`--๐ช Generating components typings...`);
const vueFiles = getMdvFiles();
// Get TS module string from parser
const content = generateComponentsModule(
vueFiles.map(f => `${path.relative(dir, f).replace(/\.v\.md$/, ".vue").replace(/\\/g, "/")}`)
);
// Write to .d.ts
fs.mkdirSync(cacheDir, { recursive: true });
const dtsPath = path.join(cacheDir, "mdv-components.d.ts");
fs.writeFileSync(dtsPath, content, "utf-8");
}
/**
* Copy components directory to .mdv folder
*/
function copyComponentsDir(dir: string = srcRoot) {
console.log(`--๐ช Copying MDV components directory...`);
// copy components directory to cache dir
const componentsDir = path.join(dir, "components");
if (fs.existsSync(componentsDir)) {
const componentsCacheDir = path.join(cacheDir, "components");
if (!fs.existsSync(componentsCacheDir)) fs.mkdirSync(componentsCacheDir, { recursive: true });
fs.rmSync(componentsCacheDir, { recursive: true });
fs.cpSync(componentsDir, componentsCacheDir, { recursive: true });
}
}
function getMdvFiles(dir: string = srcRoot, ext: string = ".v.md") {
const mdvFiles: string[] = [];
function scanDir(d: string) {
const entries = fs.readdirSync(d, { withFileTypes: true });
for (const e of entries) {
const fullPath = path.join(d, e.name);
if (e.isDirectory()) scanDir(fullPath);
else if (e.isFile() && e.name.endsWith(ext))
mdvFiles.push(fullPath);
}
}
scanDir(dir);
return mdvFiles;
}
return {
compileMDVFile,
compileAllMDVFiles,
watchAll,
writeComponentsDTS,
writeGlobalComponentsDTS,
copyComponentsDir,
getMdvFiles,
extension,
cacheDir,
srcRoot,
srcName,
mdvMeta,
compiledTimestamps,
};
};