Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions plugins/vite-plugin-llms-txt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* Vite plugin for generating llms.txt
*
* Generates an llms.txt file at build time that describes the Malloy models
* and schema to help LLMs understand the data explorer site.
*
* In dev mode, serves llms.txt dynamically via middleware.
*/

import type { Plugin, ResolvedConfig, ViteDevServer } from "vite";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import {
extractModelsSchema,
getDataFiles,
getNotebooks,
generateLlmsTxtContent,
} from "../src/llms-txt";

export interface LlmsTxtPluginOptions {
siteTitle?: string;
modelsDir?: string;
}

export default function llmsTxtPlugin(
options: LlmsTxtPluginOptions = {},
): Plugin {
const { siteTitle = "Malloy Data Explorer", modelsDir = "models" } = options;

let config: ResolvedConfig;

async function generateContent(): Promise<string> {
const modelsDirPath = path.join(config.root, modelsDir);

const [models, dataFiles, notebooks] = await Promise.all([
extractModelsSchema(modelsDirPath),
getDataFiles(modelsDirPath),
getNotebooks(modelsDirPath),
]);

return generateLlmsTxtContent({
siteTitle,
basePath: config.base,
models,
dataFiles,
notebooks,
});
}

return {
name: "vite-plugin-llms-txt",

configResolved(resolvedConfig) {
config = resolvedConfig;
},

// DEV MODE: Serve llms.txt dynamically
configureServer(server: ViteDevServer) {
server.middlewares.use((req, res, next) => {
if (req.url === "/llms.txt") {
void (async () => {
try {
// Regenerate on each request in dev mode for hot reloading
const content = await generateContent();
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end(content);
} catch (error) {
console.error("[llms.txt] Error generating content:", error);
res.statusCode = 500;
res.end(
`Error generating llms.txt: ${error instanceof Error ? error.message : String(error)}`,
);
}
})();
Comment on lines +59 to +74
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The void IIFE pattern void (async () => { ... })() is used to handle the async operation in the middleware. While this works, there's a subtle issue: if an error is thrown after the response headers are sent but before res.end() is called, the response might be left hanging. Consider adding error handling around the entire async block to ensure the response is always properly closed, or use a safer pattern like awaiting the promise and catching errors at the top level.

Suggested change
server.middlewares.use((req, res, next) => {
if (req.url === "/llms.txt") {
void (async () => {
try {
// Regenerate on each request in dev mode for hot reloading
const content = await generateContent();
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end(content);
} catch (error) {
console.error("[llms.txt] Error generating content:", error);
res.statusCode = 500;
res.end(
`Error generating llms.txt: ${error instanceof Error ? error.message : String(error)}`,
);
}
})();
server.middlewares.use(async (req, res, next) => {
if (req.url === "/llms.txt") {
try {
// Regenerate on each request in dev mode for hot reloading
const content = await generateContent();
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end(content);
} catch (error) {
console.error("[llms.txt] Error generating content:", error);
res.statusCode = 500;
res.end(
`Error generating llms.txt: ${error instanceof Error ? error.message : String(error)}`,
);
}

Copilot uses AI. Check for mistakes.
return;
}
next();
});
},

// BUILD MODE: Generate file after bundle
async closeBundle() {
if (process.env["VITEST"] || process.env["NODE_ENV"] === "test") {
return;
}
if (config.command !== "build") return;

try {
const content = await generateContent();

const outputPath = path.join(
config.root,
config.build.outDir,
"llms.txt",
);
await fs.writeFile(outputPath, content, "utf-8");

console.log(`[llms.txt] Generated ${outputPath}`);
} catch (error) {
console.error("[llms.txt] Error generating file:", error);
throw error;
}
},
};
}
Loading