import { defineConfig, Plugin, PluginOption } from "vite";
import react from "@vitejs/plugin-react";
import { viteSingleFile } from "vite-plugin-singlefile";
import { nodePolyfills } from "vite-plugin-node-polyfills";
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync } from "fs";
import { join, resolve } from "path";
const componentsDir = resolve(__dirname, "src/ui/components");
// Use node_modules/.cache for generated HTML entries - these are build artifacts, not source files
const entriesDir = resolve(__dirname, "node_modules/.cache/mongodb-mcp-server/ui-entries");
const templatePath = resolve(__dirname, "src/ui/build/template.html");
const mountPath = resolve(__dirname, "src/ui/build/mount.tsx");
const generatedDir = resolve(__dirname, "src/ui/lib");
const uiDistPath = resolve(__dirname, "dist/ui");
// Converts PascalCase to kebab-case: "ListDatabases" -> "list-databases"
function toKebabCase(pascalCase: string): string {
return pascalCase
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
.replace(/([A-Z])([A-Z][a-z])/g, "$1-$2")
.toLowerCase();
}
// Discovers component directories and builds tool name mappings
function discoverComponents(): { components: string[]; toolToComponentMap: Record<string, string> } {
const components: string[] = [];
const toolToComponentMap: Record<string, string> = {};
for (const entry of readdirSync(componentsDir)) {
const entryPath = join(componentsDir, entry);
const indexPath = join(entryPath, "index.ts");
if (statSync(entryPath).isDirectory() && existsSync(indexPath)) {
components.push(entry);
toolToComponentMap[toKebabCase(entry)] = entry;
}
}
return { components, toolToComponentMap };
}
const { components, toolToComponentMap } = discoverComponents();
/**
* Vite plugin that generates HTML entry files for each discovered component
* based on the template.html file.
*/
function generateHtmlEntries(): Plugin {
return {
name: "generate-html-entries",
buildStart() {
const template = readFileSync(templatePath, "utf-8");
if (!existsSync(entriesDir)) {
mkdirSync(entriesDir, { recursive: true });
}
for (const componentName of components) {
const html = template
.replace("{{COMPONENT_NAME}}", componentName)
.replace("{{TITLE}}", componentName.replace(/([A-Z])/g, " $1").trim()) // "ListDatabases" -> "List Databases"
.replace("{{MOUNT_PATH}}", mountPath);
const outputPath = join(entriesDir, `${componentName}.html`);
writeFileSync(outputPath, html);
console.log(`[generate-html-entries] Generated ${componentName}.html`);
}
},
};
}
/**
* Vite plugin that generates per-tool UI modules after the build completes.
*/
function generateUIModule(): Plugin {
return {
name: "generate-ui-module",
closeBundle() {
if (!existsSync(uiDistPath)) {
console.warn("[generate-ui-module] dist/ui not found, skipping module generation");
return;
}
const toolsDir = join(generatedDir, "tools");
if (!existsSync(generatedDir)) {
mkdirSync(generatedDir, { recursive: true });
}
if (!existsSync(toolsDir)) {
mkdirSync(toolsDir, { recursive: true });
}
const generatedTools: string[] = [];
for (const [toolName, componentName] of Object.entries(toolToComponentMap)) {
const htmlFile = join(uiDistPath, `${componentName}.html`);
if (!existsSync(htmlFile)) {
console.warn(
`[generate-ui-module] HTML file not found for component "${componentName}" (tool: "${toolName}")`
);
continue;
}
const html = readFileSync(htmlFile, "utf-8");
const toolModuleContent = `/**
* AUTO-GENERATED FILE - DO NOT EDIT MANUALLY
* Generated by: vite build --config vite.ui.config.ts
* Tool: ${toolName}
* Component: ${componentName}
*/
export const ${componentName}Html = ${JSON.stringify(html)};
`;
writeFileSync(join(toolsDir, `${toolName}.ts`), toolModuleContent);
generatedTools.push(toolName);
}
console.log(
`[generate-ui-module] Generated ${generatedTools.length} lazy UI module(s): ${generatedTools.join(", ")}`
);
},
};
}
export default defineConfig({
root: entriesDir,
plugins: [
generateHtmlEntries(),
nodePolyfills({
include: ["buffer", "stream"],
globals: {
Buffer: true,
},
}) as unknown as PluginOption,
react(),
viteSingleFile({
removeViteModuleLoader: true,
}),
generateUIModule(),
],
build: {
outDir: resolve(__dirname, "dist/ui"),
emptyOutDir: true,
rollupOptions: {
input: Object.fromEntries(components.map((name) => [name, resolve(entriesDir, `${name}.html`)])),
output: {
inlineDynamicImports: false,
},
},
assetsInlineLimit: 100000000,
sourcemap: false,
minify: "esbuild",
},
resolve: {
alias: {
"@ui": resolve(__dirname, "src/ui"),
},
},
});