|
|
@ -17,17 +17,17 @@ import { defaultLLMsTxtTemplate } from './constants'
|
|
|
|
import { generateLLMsFullTxt, generateLLMsTxt } from './helpers/index'
|
|
|
|
import { generateLLMsFullTxt, generateLLMsTxt } from './helpers/index'
|
|
|
|
import log from './helpers/logger'
|
|
|
|
import log from './helpers/logger'
|
|
|
|
import {
|
|
|
|
import {
|
|
|
|
expandTemplate,
|
|
|
|
expandTemplate,
|
|
|
|
extractTitle,
|
|
|
|
extractTitle,
|
|
|
|
generateMetadata,
|
|
|
|
generateMetadata,
|
|
|
|
getHumanReadableSizeOf,
|
|
|
|
getHumanReadableSizeOf,
|
|
|
|
stripExt,
|
|
|
|
stripExt
|
|
|
|
} from './helpers/utils'
|
|
|
|
} from './helpers/utils'
|
|
|
|
import type {
|
|
|
|
import type {
|
|
|
|
CustomTemplateVariables,
|
|
|
|
CustomTemplateVariables,
|
|
|
|
LlmstxtSettings,
|
|
|
|
LlmstxtSettings,
|
|
|
|
PreparedFile,
|
|
|
|
PreparedFile,
|
|
|
|
VitePressConfig,
|
|
|
|
VitePressConfig
|
|
|
|
} from './types'
|
|
|
|
} from './types'
|
|
|
|
|
|
|
|
|
|
|
|
const PLUGIN_NAME = 'llmstxt'
|
|
|
|
const PLUGIN_NAME = 'llmstxt'
|
|
|
@ -42,324 +42,324 @@ const PLUGIN_NAME = 'llmstxt'
|
|
|
|
* @see https://llmstxt.org/
|
|
|
|
* @see https://llmstxt.org/
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
export default function llmstxt(userSettings: LlmstxtSettings = {}): Plugin {
|
|
|
|
export default function llmstxt(userSettings: LlmstxtSettings = {}): Plugin {
|
|
|
|
// Create a settings object with defaults explicitly merged
|
|
|
|
// Create a settings object with defaults explicitly merged
|
|
|
|
const settings: Omit<LlmstxtSettings, 'workDir'> & { workDir: string } = {
|
|
|
|
const settings: Omit<LlmstxtSettings, 'workDir'> & { workDir: string } = {
|
|
|
|
generateLLMsTxt: true,
|
|
|
|
generateLLMsTxt: true,
|
|
|
|
generateLLMsFullTxt: true,
|
|
|
|
generateLLMsFullTxt: true,
|
|
|
|
generateLLMFriendlyDocsForEachPage: true,
|
|
|
|
generateLLMFriendlyDocsForEachPage: true,
|
|
|
|
ignoreFiles: [],
|
|
|
|
ignoreFiles: [],
|
|
|
|
workDir: undefined as unknown as string,
|
|
|
|
workDir: undefined as unknown as string,
|
|
|
|
stripHTML: true,
|
|
|
|
stripHTML: true,
|
|
|
|
...userSettings,
|
|
|
|
...userSettings
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Store the resolved Vite config
|
|
|
|
// Store the resolved Vite config
|
|
|
|
let config: VitePressConfig
|
|
|
|
let config: VitePressConfig
|
|
|
|
|
|
|
|
|
|
|
|
// Set to store all markdown file paths
|
|
|
|
// Set to store all markdown file paths
|
|
|
|
const mdFiles: Set<string> = new Set()
|
|
|
|
const mdFiles: Set<string> = new Set()
|
|
|
|
|
|
|
|
|
|
|
|
// Flag to identify which build we're in
|
|
|
|
// Flag to identify which build we're in
|
|
|
|
let isSsrBuild = false
|
|
|
|
let isSsrBuild = false
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
name: PLUGIN_NAME,
|
|
|
|
name: PLUGIN_NAME,
|
|
|
|
|
|
|
|
|
|
|
|
/** Resolves the Vite configuration and sets up the working directory. */
|
|
|
|
/** Resolves the Vite configuration and sets up the working directory. */
|
|
|
|
configResolved(resolvedConfig) {
|
|
|
|
configResolved(resolvedConfig) {
|
|
|
|
config = resolvedConfig as VitePressConfig
|
|
|
|
config = resolvedConfig as VitePressConfig
|
|
|
|
if (settings.workDir) {
|
|
|
|
if (settings.workDir) {
|
|
|
|
settings.workDir = path.resolve(
|
|
|
|
settings.workDir = path.resolve(
|
|
|
|
config.vitepress.srcDir,
|
|
|
|
config.vitepress.srcDir,
|
|
|
|
settings.workDir as string,
|
|
|
|
settings.workDir as string
|
|
|
|
)
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
settings.workDir = config.vitepress.srcDir
|
|
|
|
settings.workDir = config.vitepress.srcDir
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Detect if this is the SSR build
|
|
|
|
// Detect if this is the SSR build
|
|
|
|
isSsrBuild = !!resolvedConfig.build?.ssr
|
|
|
|
isSsrBuild = !!resolvedConfig.build?.ssr
|
|
|
|
log.info(
|
|
|
|
log.info(
|
|
|
|
`${pc.bold(PLUGIN_NAME)} initialized ${isSsrBuild ? pc.dim('(SSR build)') : pc.dim('(client build)')} with workDir: ${pc.cyan(settings.workDir as string)}`,
|
|
|
|
`${pc.bold(PLUGIN_NAME)} initialized ${isSsrBuild ? pc.dim('(SSR build)') : pc.dim('(client build)')} with workDir: ${pc.cyan(settings.workDir as string)}`
|
|
|
|
)
|
|
|
|
)
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/** Configures the development server to handle `llms.txt` and markdown files for LLMs. */
|
|
|
|
/** Configures the development server to handle `llms.txt` and markdown files for LLMs. */
|
|
|
|
async configureServer(server: ViteDevServer) {
|
|
|
|
async configureServer(server: ViteDevServer) {
|
|
|
|
log.info('Dev server configured for serving plain text docs for LLMs')
|
|
|
|
log.info('Dev server configured for serving plain text docs for LLMs')
|
|
|
|
server.middlewares.use(async (req, res, next) => {
|
|
|
|
server.middlewares.use(async (req, res, next) => {
|
|
|
|
if (req.url?.endsWith('.md') || req.url?.endsWith('.txt')) {
|
|
|
|
if (req.url?.endsWith('.md') || req.url?.endsWith('.txt')) {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
// Try to read and serve the markdown file
|
|
|
|
// Try to read and serve the markdown file
|
|
|
|
const filePath = path.resolve(
|
|
|
|
const filePath = path.resolve(
|
|
|
|
config.vitepress?.outDir ?? 'dist',
|
|
|
|
config.vitepress?.outDir ?? 'dist',
|
|
|
|
`${stripExt(req.url)}.md`,
|
|
|
|
`${stripExt(req.url)}.md`
|
|
|
|
)
|
|
|
|
)
|
|
|
|
const content = await fs.readFile(filePath, 'utf-8')
|
|
|
|
const content = await fs.readFile(filePath, 'utf-8')
|
|
|
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8')
|
|
|
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8')
|
|
|
|
res.end(content)
|
|
|
|
res.end(content)
|
|
|
|
return
|
|
|
|
return
|
|
|
|
} catch (e) {
|
|
|
|
} catch (e) {
|
|
|
|
// If file doesn't exist or can't be read, continue to next middleware
|
|
|
|
// If file doesn't exist or can't be read, continue to next middleware
|
|
|
|
log.warn(`Failed to return ${pc.cyan(req.url)}: File not found`)
|
|
|
|
log.warn(`Failed to return ${pc.cyan(req.url)}: File not found`)
|
|
|
|
next()
|
|
|
|
next()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Pass to next middleware if not handled
|
|
|
|
// Pass to next middleware if not handled
|
|
|
|
next()
|
|
|
|
next()
|
|
|
|
})
|
|
|
|
})
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Resets the collection of markdown files when the build starts.
|
|
|
|
* Resets the collection of markdown files when the build starts.
|
|
|
|
* This ensures we don't include stale data from previous builds.
|
|
|
|
* This ensures we don't include stale data from previous builds.
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
buildStart() {
|
|
|
|
buildStart() {
|
|
|
|
mdFiles.clear()
|
|
|
|
mdFiles.clear()
|
|
|
|
log.info('Build started, file collection cleared')
|
|
|
|
log.info('Build started, file collection cleared')
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Processes each file that Vite transforms and collects markdown files.
|
|
|
|
* Processes each file that Vite transforms and collects markdown files.
|
|
|
|
*
|
|
|
|
*
|
|
|
|
* @param _ - The file content (not used).
|
|
|
|
* @param _ - The file content (not used).
|
|
|
|
* @param id - The file identifier (path).
|
|
|
|
* @param id - The file identifier (path).
|
|
|
|
* @returns null if the file is processed, otherwise returns the original content.
|
|
|
|
* @returns null if the file is processed, otherwise returns the original content.
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
async transform(_, id: string) {
|
|
|
|
async transform(_, id: string) {
|
|
|
|
if (!id.endsWith('.md')) {
|
|
|
|
if (!id.endsWith('.md')) {
|
|
|
|
return null
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Skip files outside workDir if it's configured
|
|
|
|
// Skip files outside workDir if it's configured
|
|
|
|
if (!id.startsWith(settings.workDir as string)) {
|
|
|
|
if (!id.startsWith(settings.workDir as string)) {
|
|
|
|
return null
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (settings.ignoreFiles?.length) {
|
|
|
|
if (settings.ignoreFiles?.length) {
|
|
|
|
const shouldIgnore = await Promise.all(
|
|
|
|
const shouldIgnore = await Promise.all(
|
|
|
|
settings.ignoreFiles.map(async (pattern) => {
|
|
|
|
settings.ignoreFiles.map(async (pattern) => {
|
|
|
|
if (typeof pattern === 'string') {
|
|
|
|
if (typeof pattern === 'string') {
|
|
|
|
return await Promise.resolve(
|
|
|
|
return await Promise.resolve(
|
|
|
|
minimatch(
|
|
|
|
minimatch(
|
|
|
|
path.relative(settings.workDir as string, id),
|
|
|
|
path.relative(settings.workDir as string, id),
|
|
|
|
pattern,
|
|
|
|
pattern
|
|
|
|
),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
return false
|
|
|
|
}),
|
|
|
|
})
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if (shouldIgnore.some((result) => result === true)) {
|
|
|
|
if (shouldIgnore.some((result) => result === true)) {
|
|
|
|
return null
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add markdown file path to our collection
|
|
|
|
// Add markdown file path to our collection
|
|
|
|
mdFiles.add(id)
|
|
|
|
mdFiles.add(id)
|
|
|
|
// Return null to avoid modifying the file
|
|
|
|
// Return null to avoid modifying the file
|
|
|
|
return null
|
|
|
|
return null
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Runs only in the client build (not SSR) after completion.
|
|
|
|
* Runs only in the client build (not SSR) after completion.
|
|
|
|
* This ensures the processing happens exactly once.
|
|
|
|
* This ensures the processing happens exactly once.
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
async generateBundle() {
|
|
|
|
async generateBundle() {
|
|
|
|
// Skip processing during SSR build
|
|
|
|
// Skip processing during SSR build
|
|
|
|
if (isSsrBuild) {
|
|
|
|
if (isSsrBuild) {
|
|
|
|
log.info('Skipping LLMs docs generation in SSR build')
|
|
|
|
log.info('Skipping LLMs docs generation in SSR build')
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const outDir = config.vitepress?.outDir ?? 'dist'
|
|
|
|
const outDir = config.vitepress?.outDir ?? 'dist'
|
|
|
|
|
|
|
|
|
|
|
|
// Create output directory if it doesn't exist
|
|
|
|
// Create output directory if it doesn't exist
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
await fs.access(outDir)
|
|
|
|
await fs.access(outDir)
|
|
|
|
} catch {
|
|
|
|
} catch {
|
|
|
|
log.info(`Creating output directory: ${pc.cyan(outDir)}`)
|
|
|
|
log.info(`Creating output directory: ${pc.cyan(outDir)}`)
|
|
|
|
await fs.mkdir(outDir, { recursive: true })
|
|
|
|
await fs.mkdir(outDir, { recursive: true })
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const mdFilesList = Array.from(mdFiles)
|
|
|
|
const mdFilesList = Array.from(mdFiles)
|
|
|
|
const fileCount = mdFilesList.length
|
|
|
|
const fileCount = mdFilesList.length
|
|
|
|
|
|
|
|
|
|
|
|
// Skip if no files found
|
|
|
|
// Skip if no files found
|
|
|
|
if (fileCount === 0) {
|
|
|
|
if (fileCount === 0) {
|
|
|
|
log.warn(
|
|
|
|
log.warn(
|
|
|
|
`No markdown files found to process. Check your \`${pc.bold('workDir')}\` and \`${pc.bold('ignoreFiles')}\` settings.`,
|
|
|
|
`No markdown files found to process. Check your \`${pc.bold('workDir')}\` and \`${pc.bold('ignoreFiles')}\` settings.`
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
log.info(
|
|
|
|
log.info(
|
|
|
|
`Processing ${pc.bold(fileCount.toString())} markdown files from ${pc.cyan(settings.workDir)}`,
|
|
|
|
`Processing ${pc.bold(fileCount.toString())} markdown files from ${pc.cyan(settings.workDir)}`
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const preparedFiles: PreparedFile[] = await Promise.all(
|
|
|
|
const preparedFiles: PreparedFile[] = await Promise.all(
|
|
|
|
mdFilesList.map(async (file) => {
|
|
|
|
mdFilesList.map(async (file) => {
|
|
|
|
const content = await fs.readFile(file, 'utf-8')
|
|
|
|
const content = await fs.readFile(file, 'utf-8')
|
|
|
|
|
|
|
|
|
|
|
|
let mdFile: matter.GrayMatterFile<Input>
|
|
|
|
let mdFile: matter.GrayMatterFile<Input>
|
|
|
|
|
|
|
|
|
|
|
|
if (settings.stripHTML) {
|
|
|
|
if (settings.stripHTML) {
|
|
|
|
const cleanedMarkdown = await remark()
|
|
|
|
const cleanedMarkdown = await remark()
|
|
|
|
.use(remarkFrontmatter)
|
|
|
|
.use(remarkFrontmatter)
|
|
|
|
.use(() => {
|
|
|
|
.use(() => {
|
|
|
|
// Strip HTML tags
|
|
|
|
// Strip HTML tags
|
|
|
|
return (tree) => {
|
|
|
|
return (tree) => {
|
|
|
|
remove(tree, { type: 'html' })
|
|
|
|
remove(tree, { type: 'html' })
|
|
|
|
return tree
|
|
|
|
return tree
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
.process(content)
|
|
|
|
.process(content)
|
|
|
|
|
|
|
|
|
|
|
|
mdFile = matter(String(cleanedMarkdown))
|
|
|
|
mdFile = matter(String(cleanedMarkdown))
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
mdFile = matter(content)
|
|
|
|
mdFile = matter(content)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Extract title from frontmatter or use the first heading
|
|
|
|
// Extract title from frontmatter or use the first heading
|
|
|
|
const title = extractTitle(mdFile)?.trim() || 'Untitled'
|
|
|
|
const title = extractTitle(mdFile)?.trim() || 'Untitled'
|
|
|
|
|
|
|
|
|
|
|
|
const filePath =
|
|
|
|
const filePath =
|
|
|
|
path.basename(file) === 'index.md' &&
|
|
|
|
path.basename(file) === 'index.md' &&
|
|
|
|
path.dirname(file) !== settings.workDir
|
|
|
|
path.dirname(file) !== settings.workDir
|
|
|
|
? `${path.dirname(file)}.md`
|
|
|
|
? `${path.dirname(file)}.md`
|
|
|
|
: file
|
|
|
|
: file
|
|
|
|
|
|
|
|
|
|
|
|
return { path: filePath, title, file: mdFile }
|
|
|
|
return { path: filePath, title, file: mdFile }
|
|
|
|
}),
|
|
|
|
})
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if (settings.generateLLMFriendlyDocsForEachPage) {
|
|
|
|
if (settings.generateLLMFriendlyDocsForEachPage) {
|
|
|
|
await Promise.all(
|
|
|
|
await Promise.all(
|
|
|
|
preparedFiles.map(async (file) => {
|
|
|
|
preparedFiles.map(async (file) => {
|
|
|
|
const relativePath = path.relative(settings.workDir, file.path)
|
|
|
|
const relativePath = path.relative(settings.workDir, file.path)
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const mdFile = file.file
|
|
|
|
const mdFile = file.file
|
|
|
|
const targetPath = path.resolve(outDir, relativePath)
|
|
|
|
const targetPath = path.resolve(outDir, relativePath)
|
|
|
|
|
|
|
|
|
|
|
|
// Ensure target directory exists (async version)
|
|
|
|
// Ensure target directory exists (async version)
|
|
|
|
await fs.mkdir(path.dirname(targetPath), {
|
|
|
|
await fs.mkdir(path.dirname(targetPath), {
|
|
|
|
recursive: true,
|
|
|
|
recursive: true
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// Copy file to output directory (async version)
|
|
|
|
// Copy file to output directory (async version)
|
|
|
|
await fs.writeFile(
|
|
|
|
await fs.writeFile(
|
|
|
|
targetPath,
|
|
|
|
targetPath,
|
|
|
|
matter.stringify(
|
|
|
|
matter.stringify(
|
|
|
|
mdFile.content,
|
|
|
|
mdFile.content,
|
|
|
|
generateMetadata(mdFile, {
|
|
|
|
generateMetadata(mdFile, {
|
|
|
|
domain: settings.domain,
|
|
|
|
domain: settings.domain,
|
|
|
|
filePath: relativePath,
|
|
|
|
filePath: relativePath
|
|
|
|
}),
|
|
|
|
})
|
|
|
|
),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
log.success(`Processed ${pc.cyan(relativePath)}`)
|
|
|
|
log.success(`Processed ${pc.cyan(relativePath)}`)
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error) {
|
|
|
|
log.error(
|
|
|
|
log.error(
|
|
|
|
// @ts-ignore
|
|
|
|
// @ts-ignore
|
|
|
|
`Failed to process ${pc.cyan(relativePath)}: ${error.message}`,
|
|
|
|
`Failed to process ${pc.cyan(relativePath)}: ${error.message}`
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}),
|
|
|
|
})
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Sort files by title for better organization
|
|
|
|
// Sort files by title for better organization
|
|
|
|
preparedFiles.sort((a, b) => a.title.localeCompare(b.title))
|
|
|
|
preparedFiles.sort((a, b) => a.title.localeCompare(b.title))
|
|
|
|
|
|
|
|
|
|
|
|
const tasks: Promise<void>[] = []
|
|
|
|
const tasks: Promise<void>[] = []
|
|
|
|
|
|
|
|
|
|
|
|
// Generate llms.txt - table of contents with links
|
|
|
|
// Generate llms.txt - table of contents with links
|
|
|
|
if (settings.generateLLMsTxt) {
|
|
|
|
if (settings.generateLLMsTxt) {
|
|
|
|
const llmsTxtPath = path.resolve(outDir, 'llms.txt')
|
|
|
|
const llmsTxtPath = path.resolve(outDir, 'llms.txt')
|
|
|
|
const templateVariables: CustomTemplateVariables = {
|
|
|
|
const templateVariables: CustomTemplateVariables = {
|
|
|
|
title: settings.title,
|
|
|
|
title: settings.title,
|
|
|
|
description: settings.description,
|
|
|
|
description: settings.description,
|
|
|
|
details: settings.details,
|
|
|
|
details: settings.details,
|
|
|
|
toc: settings.toc,
|
|
|
|
toc: settings.toc,
|
|
|
|
...settings.customTemplateVariables,
|
|
|
|
...settings.customTemplateVariables
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tasks.push(
|
|
|
|
tasks.push(
|
|
|
|
(async () => {
|
|
|
|
(async () => {
|
|
|
|
log.info(`Generating ${pc.cyan('llms.txt')}...`)
|
|
|
|
log.info(`Generating ${pc.cyan('llms.txt')}...`)
|
|
|
|
|
|
|
|
|
|
|
|
const llmsTxt = await generateLLMsTxt(preparedFiles, {
|
|
|
|
const llmsTxt = await generateLLMsTxt(preparedFiles, {
|
|
|
|
indexMd: path.resolve(settings.workDir as string, 'index.md'),
|
|
|
|
indexMd: path.resolve(settings.workDir as string, 'index.md'),
|
|
|
|
srcDir: settings.workDir as string,
|
|
|
|
srcDir: settings.workDir as string,
|
|
|
|
LLMsTxtTemplate:
|
|
|
|
LLMsTxtTemplate:
|
|
|
|
settings.customLLMsTxtTemplate || defaultLLMsTxtTemplate,
|
|
|
|
settings.customLLMsTxtTemplate || defaultLLMsTxtTemplate,
|
|
|
|
templateVariables,
|
|
|
|
templateVariables,
|
|
|
|
vitepressConfig: config?.vitepress?.userConfig,
|
|
|
|
vitepressConfig: config?.vitepress?.userConfig,
|
|
|
|
domain: settings.domain,
|
|
|
|
domain: settings.domain,
|
|
|
|
sidebar: settings.sidebar,
|
|
|
|
sidebar: settings.sidebar,
|
|
|
|
linksExtension: !settings.generateLLMFriendlyDocsForEachPage
|
|
|
|
linksExtension: !settings.generateLLMFriendlyDocsForEachPage
|
|
|
|
? '.html'
|
|
|
|
? '.html'
|
|
|
|
: undefined,
|
|
|
|
: undefined,
|
|
|
|
cleanUrls: config.cleanUrls,
|
|
|
|
cleanUrls: config.cleanUrls
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
await fs.writeFile(llmsTxtPath, llmsTxt, 'utf-8')
|
|
|
|
await fs.writeFile(llmsTxtPath, llmsTxt, 'utf-8')
|
|
|
|
|
|
|
|
|
|
|
|
log.success(
|
|
|
|
log.success(
|
|
|
|
expandTemplate(
|
|
|
|
expandTemplate(
|
|
|
|
'Generated {file} (~{tokens} tokens, {size}) with {fileCount} documentation links',
|
|
|
|
'Generated {file} (~{tokens} tokens, {size}) with {fileCount} documentation links',
|
|
|
|
{
|
|
|
|
{
|
|
|
|
file: pc.cyan('llms.txt'),
|
|
|
|
file: pc.cyan('llms.txt'),
|
|
|
|
tokens: pc.bold(millify(approximateTokenSize(llmsTxt))),
|
|
|
|
tokens: pc.bold(millify(approximateTokenSize(llmsTxt))),
|
|
|
|
size: pc.bold(getHumanReadableSizeOf(llmsTxt)),
|
|
|
|
size: pc.bold(getHumanReadableSizeOf(llmsTxt)),
|
|
|
|
fileCount: pc.bold(fileCount.toString()),
|
|
|
|
fileCount: pc.bold(fileCount.toString())
|
|
|
|
},
|
|
|
|
}
|
|
|
|
),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
})(),
|
|
|
|
})()
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Generate llms-full.txt - all content in one file
|
|
|
|
// Generate llms-full.txt - all content in one file
|
|
|
|
if (settings.generateLLMsFullTxt) {
|
|
|
|
if (settings.generateLLMsFullTxt) {
|
|
|
|
const llmsFullTxtPath = path.resolve(outDir, 'llms-full.txt')
|
|
|
|
const llmsFullTxtPath = path.resolve(outDir, 'llms-full.txt')
|
|
|
|
|
|
|
|
|
|
|
|
tasks.push(
|
|
|
|
tasks.push(
|
|
|
|
(async () => {
|
|
|
|
(async () => {
|
|
|
|
log.info(
|
|
|
|
log.info(
|
|
|
|
`Generating full documentation bundle (${pc.cyan('llms-full.txt')})...`,
|
|
|
|
`Generating full documentation bundle (${pc.cyan('llms-full.txt')})...`
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const llmsFullTxt = generateLLMsFullTxt(preparedFiles, {
|
|
|
|
const llmsFullTxt = generateLLMsFullTxt(preparedFiles, {
|
|
|
|
srcDir: settings.workDir as string,
|
|
|
|
srcDir: settings.workDir as string,
|
|
|
|
domain: settings.domain,
|
|
|
|
domain: settings.domain,
|
|
|
|
linksExtension: !settings.generateLLMFriendlyDocsForEachPage
|
|
|
|
linksExtension: !settings.generateLLMFriendlyDocsForEachPage
|
|
|
|
? '.html'
|
|
|
|
? '.html'
|
|
|
|
: undefined,
|
|
|
|
: undefined,
|
|
|
|
cleanUrls: config.cleanUrls,
|
|
|
|
cleanUrls: config.cleanUrls
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// Write content to llms-full.txt
|
|
|
|
// Write content to llms-full.txt
|
|
|
|
await fs.writeFile(llmsFullTxtPath, llmsFullTxt, 'utf-8')
|
|
|
|
await fs.writeFile(llmsFullTxtPath, llmsFullTxt, 'utf-8')
|
|
|
|
log.success(
|
|
|
|
log.success(
|
|
|
|
expandTemplate(
|
|
|
|
expandTemplate(
|
|
|
|
'Generated {file} (~{tokens} tokens, {size}) with {fileCount} markdown files',
|
|
|
|
'Generated {file} (~{tokens} tokens, {size}) with {fileCount} markdown files',
|
|
|
|
{
|
|
|
|
{
|
|
|
|
file: pc.cyan('llms-full.txt'),
|
|
|
|
file: pc.cyan('llms-full.txt'),
|
|
|
|
tokens: pc.bold(millify(approximateTokenSize(llmsFullTxt))),
|
|
|
|
tokens: pc.bold(millify(approximateTokenSize(llmsFullTxt))),
|
|
|
|
size: pc.bold(getHumanReadableSizeOf(llmsFullTxt)),
|
|
|
|
size: pc.bold(getHumanReadableSizeOf(llmsFullTxt)),
|
|
|
|
fileCount: pc.bold(fileCount.toString()),
|
|
|
|
fileCount: pc.bold(fileCount.toString())
|
|
|
|
},
|
|
|
|
}
|
|
|
|
),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
})(),
|
|
|
|
})()
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (tasks.length) {
|
|
|
|
if (tasks.length) {
|
|
|
|
await Promise.all(tasks)
|
|
|
|
await Promise.all(tasks)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|