| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133 |
- // 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.`
- );
|