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