82 lines
2.8 KiB
JavaScript
82 lines
2.8 KiB
JavaScript
/**
|
|
* Build script for the markdown-review MCP server.
|
|
*
|
|
* Produces two self-contained artifacts in dist/ with ZERO runtime node_modules:
|
|
* - dist/index.js Node MCP server, all deps inlined (run: node dist/index.js --stdio)
|
|
* - dist/mcp-app.html Single-file webview app (HTML + inlined JS/CSS)
|
|
*
|
|
* Build-time deps live in node_modules; runtime needs only `node`. Commit dist/
|
|
* so a fresh install runs without `npm install`.
|
|
*/
|
|
import { build, context } from "esbuild";
|
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const root = path.dirname(fileURLToPath(import.meta.url));
|
|
const dist = path.join(root, "dist");
|
|
const watch = process.argv.includes("--watch");
|
|
|
|
const serverOpts = {
|
|
entryPoints: [path.join(root, "src/main.ts")],
|
|
outfile: path.join(dist, "index.js"),
|
|
bundle: true,
|
|
platform: "node",
|
|
format: "esm",
|
|
target: "node20",
|
|
// ESM output that bundles CJS deps needs createRequire shimmed in.
|
|
banner: {
|
|
js: [
|
|
"#!/usr/bin/env node",
|
|
"import { createRequire as __cr } from 'node:module';",
|
|
"const require = __cr(import.meta.url);",
|
|
].join("\n"),
|
|
},
|
|
logLevel: "info",
|
|
};
|
|
|
|
/** Bundle the browser app to a string of JS, then inline it into the HTML shell. */
|
|
async function buildApp() {
|
|
const result = await build({
|
|
entryPoints: [path.join(root, "src/app/mcp-app.ts")],
|
|
bundle: true,
|
|
platform: "browser",
|
|
format: "iife",
|
|
target: "es2022",
|
|
minify: !watch,
|
|
sourcemap: watch ? "inline" : false,
|
|
write: false,
|
|
logLevel: "info",
|
|
});
|
|
const js = result.outputFiles[0].text;
|
|
// Escape any literal "</script>" inside the bundle, or the HTML parser closes
|
|
// the inline <script> early and the whole bundle fails to parse. (markdown-it
|
|
// and friends ship such strings.) "<\/script>" is identical JS, inert to HTML.
|
|
const safeJs = js.replace(/<\/script>/gi, "<\\/script>");
|
|
const shell = await readFile(path.join(root, "src/app/index.html"), "utf-8");
|
|
// Use a replacer FUNCTION, not a string: a string replacement interprets "$&",
|
|
// "$`", etc., and the minified bundle is full of "$" — which silently
|
|
// duplicates/mangles the shell. A function replacement is taken verbatim.
|
|
const html = shell.replace("<!--APP_BUNDLE-->", () => `<script>${safeJs}</script>`);
|
|
await writeFile(path.join(dist, "mcp-app.html"), html, "utf-8");
|
|
console.log("[build] dist/mcp-app.html");
|
|
}
|
|
|
|
async function run() {
|
|
await mkdir(dist, { recursive: true });
|
|
if (watch) {
|
|
const ctx = await context(serverOpts);
|
|
await ctx.watch();
|
|
await buildApp();
|
|
console.log("[build] watching server; rebuild app manually on change");
|
|
} else {
|
|
await build(serverOpts);
|
|
await buildApp();
|
|
console.log("[build] done");
|
|
}
|
|
}
|
|
|
|
run().catch((e) => {
|
|
console.error(e);
|
|
process.exit(1);
|
|
});
|