build.mjs 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. // Build script for @fmode/openclaw-wechat-agent.
  2. //
  3. // We intentionally NOT use `tsc` to emit the runtime bundle. The plugin is
  4. // installed into `~/.openclaw/extensions/wechat-agent/` at the user's home,
  5. // which is NOT a descendant of the host `openclaw` package's `node_modules`
  6. // tree. Node's standard ESM resolver therefore cannot resolve bare specifiers
  7. // like `openclaw/plugin-sdk/channel-plugin-common` at runtime and channel
  8. // processes exit with `Cannot find package 'openclaw'` in a 5s restart loop.
  9. //
  10. // Mitigation: bundle every `openclaw/*` import into a single self-contained
  11. // `dist/index.js` at build time against the dev-time `openclaw` install.
  12. // The plugin tgz ships with zero runtime dependency on the host's openclaw
  13. // package tree. See README "Build & release" for details.
  14. //
  15. // Externals: only Node built-ins. Everything else (openclaw/*, our own
  16. // ./src/*, any npm deps we might add later) gets inlined.
  17. import { build } from "esbuild";
  18. import { readFileSync, rmSync, mkdirSync, existsSync } from "node:fs";
  19. import { fileURLToPath } from "node:url";
  20. import { dirname, resolve } from "node:path";
  21. const __dirname = dirname(fileURLToPath(import.meta.url));
  22. const projectRoot = resolve(__dirname, "..");
  23. const pkg = JSON.parse(
  24. readFileSync(resolve(projectRoot, "package.json"), "utf8")
  25. );
  26. const outfile = resolve(projectRoot, "dist", "index.js");
  27. const distDir = resolve(projectRoot, "dist");
  28. if (existsSync(distDir)) {
  29. rmSync(distDir, { recursive: true, force: true });
  30. }
  31. mkdirSync(distDir, { recursive: true });
  32. const result = await build({
  33. entryPoints: [resolve(projectRoot, "index.ts")],
  34. outfile,
  35. bundle: true,
  36. format: "esm",
  37. platform: "node",
  38. // OpenClaw requires node >= 22; node20 baseline keeps us compatible with
  39. // any reasonable distro openclaw ships on.
  40. target: "node20",
  41. // Channel processes are short-lived children; inline-source-map aids post-
  42. // mortem from journalctl without requiring a separate .map file.
  43. sourcemap: "inline",
  44. legalComments: "none",
  45. // Keep names readable so `channel exited` stack traces from journalctl
  46. // still point at recognizable helpers.
  47. minify: false,
  48. keepNames: true,
  49. // Only Node built-ins stay external. Everything else — including every
  50. // `openclaw/plugin-sdk*` subpath — gets inlined. This is the whole point.
  51. //
  52. // A handful of optional/native dependencies are reachable via SDK
  53. // side-effect imports (image/canvas/audio/voice-call paths we never
  54. // trigger from wechat-agent). We mark them external so esbuild doesn't
  55. // try to parse their .node binaries or platform-specific install stubs.
  56. // At runtime these modules would fail to resolve from the customer's
  57. // ~/.openclaw/extensions/ tree, but the code paths are dead for a pure
  58. // text channel — they are only ever touched lazily by unrelated SDK
  59. // features (voice-call, speech, web-media, etc.). If a future feature
  60. // brings us into those code paths, the resulting `Cannot find package`
  61. // error will be immediate and loud rather than silent.
  62. external: [
  63. "node:*",
  64. "@napi-rs/canvas",
  65. "@napi-rs/canvas-*",
  66. "@img/sharp-*",
  67. "sharp",
  68. "@discordjs/opus",
  69. "node-llama-cpp",
  70. "@matrix-org/matrix-sdk-crypto-nodejs",
  71. "@lancedb/lancedb",
  72. "@lancedb/lancedb-*",
  73. "silk-wasm",
  74. "sqlite-vec",
  75. "sqlite-vec-*",
  76. "playwright-core",
  77. "pdfjs-dist",
  78. "jimp",
  79. "fake-indexeddb",
  80. "@mariozechner/pi-tui",
  81. "@lydell/node-pty",
  82. ],
  83. // `.node` native bindings must not be parsed by esbuild; stub them out.
  84. // If execution ever actually hits one of these at runtime, we want a
  85. // proper ERR_MODULE_NOT_FOUND from the node loader, not a silent miss.
  86. loader: {
  87. ".node": "empty",
  88. },
  89. // Bundle the plugin as ES module; emit .js (package.json has type:module).
  90. mainFields: ["module", "main"],
  91. conditions: ["import", "node", "default"],
  92. // Silence the "direct eval" / "unsupported" warnings from SDK internals
  93. // unless they are new; we compile against a pinned openclaw version.
  94. logLevel: "info",
  95. metafile: true,
  96. treeShaking: true,
  97. // Reminder in the banner: the file is generated and should not be hand-edited.
  98. banner: {
  99. js: `// @fmode/openclaw-wechat-agent v${pkg.version} — generated by scripts/build.mjs. Do not edit by hand.`,
  100. },
  101. });
  102. // Quick sanity check: the output must not contain any unbundled `openclaw/*`
  103. // import. If it does, our build config is wrong and the plugin will crash at
  104. // runtime on the customer host in the exact same way the symlink-less install
  105. // does today.
  106. const bundled = readFileSync(outfile, "utf8");
  107. const leaks = Array.from(
  108. bundled.matchAll(/(?:from|require\()\s*["']openclaw(?:\/[^"']*)?["']/g),
  109. (m) => m[0]
  110. );
  111. if (leaks.length > 0) {
  112. console.error(
  113. `\n\u274c build-check failed: bundle still imports openclaw/* at runtime:`
  114. );
  115. for (const l of leaks.slice(0, 10)) console.error(` ${l}`);
  116. process.exit(1);
  117. }
  118. const inputCount = Object.keys(result.metafile.inputs).length;
  119. const outputSize = bundled.length;
  120. console.log(
  121. `\n\u2705 bundled ${inputCount} modules -> dist/index.js (${(outputSize / 1024).toFixed(1)} KiB)`
  122. );
  123. console.log(
  124. ` zero openclaw/* runtime imports; plugin is fully self-contained.`
  125. );