// Build script for @fmode/openclaw-wechat-agent. // // We intentionally NOT use `tsc` to emit the runtime bundle. The plugin is // installed into `~/.openclaw/extensions/wechat-agent/` at the user's home, // which is NOT a descendant of the host `openclaw` package's `node_modules` // tree. Node's standard ESM resolver therefore cannot resolve bare specifiers // like `openclaw/plugin-sdk/channel-plugin-common` at runtime and channel // processes exit with `Cannot find package 'openclaw'` in a 5s restart loop. // // Mitigation: bundle every `openclaw/*` import into a single self-contained // `dist/index.js` at build time against the dev-time `openclaw` install. // The plugin tgz ships with zero runtime dependency on the host's openclaw // package tree. See README "Build & release" for details. // // Externals: only Node built-ins. Everything else (openclaw/*, our own // ./src/*, any npm deps we might add later) gets inlined. import { build } from "esbuild"; import { readFileSync, rmSync, mkdirSync, existsSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, resolve } from "node:path"; const __dirname = dirname(fileURLToPath(import.meta.url)); const projectRoot = resolve(__dirname, ".."); const pkg = JSON.parse( readFileSync(resolve(projectRoot, "package.json"), "utf8") ); const outfile = resolve(projectRoot, "dist", "index.js"); const distDir = resolve(projectRoot, "dist"); if (existsSync(distDir)) { rmSync(distDir, { recursive: true, force: true }); } mkdirSync(distDir, { recursive: true }); const result = await build({ entryPoints: [resolve(projectRoot, "index.ts")], outfile, bundle: true, format: "esm", platform: "node", // OpenClaw requires node >= 22; node20 baseline keeps us compatible with // any reasonable distro openclaw ships on. target: "node20", // Channel processes are short-lived children; inline-source-map aids post- // mortem from journalctl without requiring a separate .map file. sourcemap: "inline", legalComments: "none", // Keep names readable so `channel exited` stack traces from journalctl // still point at recognizable helpers. minify: false, keepNames: true, // Only Node built-ins stay external. Everything else — including every // `openclaw/plugin-sdk*` subpath — gets inlined. This is the whole point. // // A handful of optional/native dependencies are reachable via SDK // side-effect imports (image/canvas/audio/voice-call paths we never // trigger from wechat-agent). We mark them external so esbuild doesn't // try to parse their .node binaries or platform-specific install stubs. // At runtime these modules would fail to resolve from the customer's // ~/.openclaw/extensions/ tree, but the code paths are dead for a pure // text channel — they are only ever touched lazily by unrelated SDK // features (voice-call, speech, web-media, etc.). If a future feature // brings us into those code paths, the resulting `Cannot find package` // error will be immediate and loud rather than silent. external: [ "node:*", "@napi-rs/canvas", "@napi-rs/canvas-*", "@img/sharp-*", "sharp", "@discordjs/opus", "node-llama-cpp", "@matrix-org/matrix-sdk-crypto-nodejs", "@lancedb/lancedb", "@lancedb/lancedb-*", "silk-wasm", "sqlite-vec", "sqlite-vec-*", "playwright-core", "pdfjs-dist", "jimp", "fake-indexeddb", "@mariozechner/pi-tui", "@lydell/node-pty", ], // `.node` native bindings must not be parsed by esbuild; stub them out. // If execution ever actually hits one of these at runtime, we want a // proper ERR_MODULE_NOT_FOUND from the node loader, not a silent miss. loader: { ".node": "empty", }, // Bundle the plugin as ES module; emit .js (package.json has type:module). mainFields: ["module", "main"], conditions: ["import", "node", "default"], // Silence the "direct eval" / "unsupported" warnings from SDK internals // unless they are new; we compile against a pinned openclaw version. logLevel: "info", metafile: true, treeShaking: true, // Reminder in the banner: the file is generated and should not be hand-edited. banner: { js: `// @fmode/openclaw-wechat-agent v${pkg.version} — generated by scripts/build.mjs. Do not edit by hand.`, }, }); // Quick sanity check: the output must not contain any unbundled `openclaw/*` // import. If it does, our build config is wrong and the plugin will crash at // runtime on the customer host in the exact same way the symlink-less install // does today. const bundled = readFileSync(outfile, "utf8"); const leaks = Array.from( bundled.matchAll(/(?:from|require\()\s*["']openclaw(?:\/[^"']*)?["']/g), (m) => m[0] ); if (leaks.length > 0) { console.error( `\n\u274c build-check failed: bundle still imports openclaw/* at runtime:` ); for (const l of leaks.slice(0, 10)) console.error(` ${l}`); process.exit(1); } const inputCount = Object.keys(result.metafile.inputs).length; const outputSize = bundled.length; console.log( `\n\u2705 bundled ${inputCount} modules -> dist/index.js (${(outputSize / 1024).toFixed(1)} KiB)` ); console.log( ` zero openclaw/* runtime imports; plugin is fully self-contained.` );