vitepress-plugin-llms
Version:
๐ A VitePress plugin for generating LLM-friendly documentation
25 lines (16 loc) โข 8.43 kB
JavaScript
// Built with bunup
import q from"node:fs/promises";import A from"node:path";import C from"gray-matter";import{minimatch as Q1}from"minimatch";import _ from"picocolors";import{remark as X1}from"remark";import Y1 from"remark-frontmatter";import{remove as Z1}from"unist-util-remove";var P="vitepress-plugin-llms";import{millify as p}from"millify";import{approximateTokenSize as o}from"tokenx";var z=`# {title}
{description}
{details}
## Table of Contents
{toc}`;import j1 from"node:fs/promises";import y1 from"node:path";import k from"gray-matter";import I from"node:path";import B from"node:path";import l from"byte-size";import a from"markdown-title";var x=(y)=>({dir:B.dirname(y),file:B.basename(y)}),F=(y)=>{let{dir:j,file:J}=x(y);return B.join(j,B.basename(J,B.extname(J)))},U=(y)=>{let{dir:j,file:J}=x(y);return B.posix.join(j,B.basename(J,B.extname(J)))};function u(y){let j=y.data?.title||y.data?.titleTemplate,J;if(!j)J=a(y.content);return j||J}var i=(y)=>new RegExp(`(\\n\\s*\\n)?\\{${y}\\}`,"gi");function t(y,j,J,$){return y.replace(i(j),(w,Q)=>{let Z=J?.length?J:$?.length?$:"";return Z?`${Q?`
`:""}${Z}`:""})}var O=(y,j)=>{return Object.entries(j).reduce((J,[$,w])=>t(J,$,w),y)},v=(y,j,J,$)=>O("{domain}/{path}{extension}",{domain:j||"",path:y,extension:$?"":J});function E(y,j){let{domain:J,filePath:$,linksExtension:w,cleanUrls:Q}=j,Z={};if(Z.url=v(U($),J,w??".md",Q),y.data?.description?.length)Z.description=y.data?.description;return Z}var M=(y)=>l(new Blob([y]).size).toString();var S=(y,j,J,$,w=!1)=>{let Q=y.file.data.description;return`- [${y.title}](${v(U(J),j,$??".md",w)})${Q?`: ${Q.trim()}`:""}
`};async function h(y){return Promise.all(y.map(async(j)=>{let J=[];if(j.link)J.push(j.link);if(j.items&&Array.isArray(j.items)){let $=await h(j.items);J.push(...$)}return J})).then((j)=>j.flat())}function T(y){let j=U(y);if(I.basename(j)==="index")return I.dirname(j);return j}function g(y,j){let J=T(y),$=T(j);return J===$||J===`${$}.md`}async function c(y,j,J,$,w,Q,Z=3){let K="";if(y.text)K+=`${"#".repeat(Z)} ${y.text}
`;if(y.items&&Array.isArray(y.items)){let[W,G]=await Promise.all([Promise.all(y.items.filter((X)=>typeof X.link==="string").map(async(X)=>{let H=T(X.link),Y=j.find((N)=>{let D=`/${U(I.relative(J,N.path))}`;return g(D,H)});if(Y){let N=I.relative(J,Y.path);return S(Y,$,N,w,Q)}return null})).then((X)=>X.filter((H)=>H!==null)),Promise.all(y.items.filter((X)=>Array.isArray(X.items)&&X.items.length>0).map((X)=>c(X,j,J,$,w,Q,Z+1)))]);if(W.length>0)K+=W.join("");if(W.length>0&&G.length>0)K+=`
`;if(G.length>0)K+=G.join(`
`)}return K}function e(y){if(Array.isArray(y))return y;if(typeof y==="object")return Object.values(y).flat();return[]}async function d(y,j){let{srcDir:J,domain:$,sidebarConfig:w,linksExtension:Q,cleanUrls:Z}=j,K="";if(w){let W=e(w);if(W.length>0){let G=await Promise.all(W.map((Y)=>c(Y,y,J,$,Q,Z)));K+=`${G.join(`
`)}
`;let X=await h(W),H=y.filter((Y)=>{let N=`/${U(I.relative(J,Y.path))}`;return!X.some((D)=>g(N,D))});if(H.length>0){K+=`### Other
`;let Y=[];await Promise.all(H.map(async(N)=>{let D=I.relative(J,N.path);Y.push(S(N,$,D,Q,Z))})),K+=Y.join("")}return K}}if(y.length>0){let W=await Promise.all(y.map(async(G)=>{let X=I.relative(J,G.path);return S(G,$,X,Q,Z)}));K+=W.join("")}return K}async function m(y,j){let{indexMd:J,srcDir:$,LLMsTxtTemplate:w=z,templateVariables:Q={},vitepressConfig:Z,domain:K,sidebar:W,linksExtension:G,cleanUrls:X}=j;k.clearCache();let H=await j1.readFile(J,"utf-8"),Y=k(H);if(Q.title??=Y.data?.hero?.name||Y.data?.title||Z?.title||Z?.titleTemplate||u(Y)||"LLMs Documentation",Q.description??=Y.data?.hero?.text||Z?.description||Y?.data?.description||Y.data?.titleTemplate,Q.description)Q.description=`> ${Q.description}`;return Q.details??=Y.data?.hero?.tagline||Y.data?.tagline||!Q.description&&"This file contains links to all documentation sections.",Q.toc??=await d(y,{srcDir:$,domain:K,sidebarConfig:W||Z?.themeConfig?.sidebar,linksExtension:G,cleanUrls:X}),O(w,Q)}async function f(y,j){let{srcDir:J,domain:$,linksExtension:w,cleanUrls:Q}=j;return(await Promise.all(y.map(async(K)=>{let W=y1.relative(J,K.path),G=await E(K.file,{domain:$,filePath:W,linksExtension:w,cleanUrls:Q});return k.stringify(K.file.content,G)}))).join(`
---
`)}import V from"picocolors";var L=V.blue("llmstxt")+V.dim(" ยป "),J1={info:(y)=>console.log(`${L} ${y}`),success:(y)=>console.log(`${L}${V.green("โ")} ${y}`),warn:(y)=>console.warn(`${L}${V.yellow("โ ")} ${V.yellow(y)}`),error:(y)=>console.error(`${L}${V.red("โ")} ${V.red(y)}`)},R=J1;var r=P;function $1(y={}){let j={generateLLMsTxt:!0,generateLLMsFullTxt:!0,generateLLMFriendlyDocsForEachPage:!0,ignoreFiles:[],workDir:void 0,stripHTML:!0,...y},J,$=new Set,w=!1;return{name:r,configResolved(Q){if(J=Q,j.workDir)j.workDir=A.resolve(J.vitepress.srcDir,j.workDir);else j.workDir=J.vitepress.srcDir;w=!!Q.build?.ssr,R.info(`${_.bold(r)} initialized ${w?_.dim("(SSR build)"):_.dim("(client build)")} with workDir: ${_.cyan(j.workDir)}`)},async configureServer(Q){R.info("Dev server configured for serving plain text docs for LLMs"),Q.middlewares.use(async(Z,K,W)=>{if(Z.url?.endsWith(".md")||Z.url?.endsWith(".txt"))try{let G=A.resolve(J.vitepress?.outDir??"dist",`${F(Z.url)}.md`),X=await q.readFile(G,"utf-8");K.setHeader("Content-Type","text/plain; charset=utf-8"),K.end(X);return}catch(G){R.warn(`Failed to return ${_.cyan(Z.url)}: File not found`),W()}W()})},buildStart(){$.clear(),R.info("Build started, file collection cleared")},async transform(Q,Z){if(!Z.endsWith(".md"))return null;if(!Z.startsWith(j.workDir))return null;if(j.ignoreFiles?.length){if((await Promise.all(j.ignoreFiles.map(async(W)=>{if(typeof W==="string")return Q1(A.relative(j.workDir,Z),W);return!1}))).some((W)=>W===!0))return null}return $.add(Z),null},async generateBundle(){if(w){R.info("Skipping LLMs docs generation in SSR build");return}let Q=J.vitepress?.outDir??"dist";try{await q.access(Q)}catch{R.info(`Creating output directory: ${_.cyan(Q)}`),await q.mkdir(Q,{recursive:!0})}let Z=Array.from($),K=Z.length;if(K===0){R.warn(`No markdown files found to process. Check your \`${_.bold("workDir")}\` and \`${_.bold("ignoreFiles")}\` settings.`);return}R.info(`Processing ${_.bold(K.toString())} markdown files from ${_.cyan(j.workDir)}`);let W=await Promise.all(Z.map(async(X)=>{let H=await q.readFile(X,"utf-8"),Y;if(j.stripHTML){let n=await X1().use(Y1).use(()=>{return(b)=>{return Z1(b,{type:"html"}),b}}).process(H);Y=C(String(n))}else Y=C(H);let N=u(Y)?.trim()||"Untitled";return{path:A.basename(X)==="index.md"&&A.dirname(X)!==j.workDir?`${A.dirname(X)}.md`:X,title:N,file:Y}}));W.sort((X,H)=>X.title.localeCompare(H.title));let G=[];if(j.generateLLMsTxt){let X=A.resolve(Q,"llms.txt"),H={title:j.title,description:j.description,details:j.details,toc:j.toc,...j.customTemplateVariables};G.push((async()=>{R.info(`Generating ${_.cyan("llms.txt")}...`);let Y=await m(W,{indexMd:A.resolve(j.workDir,"index.md"),srcDir:j.workDir,LLMsTxtTemplate:j.customLLMsTxtTemplate||z,templateVariables:H,vitepressConfig:J?.vitepress?.userConfig,domain:j.domain,sidebar:j.sidebar,linksExtension:!j.generateLLMFriendlyDocsForEachPage?".html":void 0,cleanUrls:J.cleanUrls});await q.writeFile(X,Y,"utf-8"),R.success(O("Generated {file} (~{tokens} tokens, {size}) with {fileCount} documentation links",{file:_.cyan("llms.txt"),tokens:_.bold(p(o(Y))),size:_.bold(M(Y)),fileCount:_.bold(K.toString())}))})())}if(j.generateLLMsFullTxt){let X=A.resolve(Q,"llms-full.txt");G.push((async()=>{R.info(`Generating full documentation bundle (${_.cyan("llms-full.txt")})...`);let H=await f(W,{srcDir:j.workDir,domain:j.domain,linksExtension:!j.generateLLMFriendlyDocsForEachPage?".html":void 0,cleanUrls:J.cleanUrls});await q.writeFile(X,H,"utf-8"),R.success(O("Generated {file} (~{tokens} tokens, {size}) with {fileCount} markdown files",{file:_.cyan("llms-full.txt"),tokens:_.bold(p(o(H))),size:_.bold(M(H)),fileCount:_.bold(K.toString())}))})())}if(j.generateLLMFriendlyDocsForEachPage)G.push(...W.map(async(X)=>{let H=A.relative(j.workDir,X.path);try{let Y=X.file,N=A.resolve(Q,H);await q.mkdir(A.dirname(N),{recursive:!0}),await q.writeFile(N,C.stringify(Y.content,await E(Y,{domain:j.domain,filePath:H,linksExtension:".md",cleanUrls:J.cleanUrls}))),R.success(`Processed ${_.cyan(H)}`)}catch(Y){R.error(`Failed to process ${_.cyan(H)}: ${Y.message}`)}}));if(G.length)await Promise.all(G)}}}export{$1 as default};