From f17ffe32c756d1441a41b91e4777cc730dbbb53a Mon Sep 17 00:00:00 2001 From: paisley <8197966+su8su@users.noreply.github.com> Date: Mon, 2 Mar 2026 18:42:32 +0800 Subject: [PATCH] fix: patch node-domexception in bundled openclaw 3.1 to avoid ESM loader crash (#264) --- scripts/after-pack.cjs | 42 ++++++++++++++++++++++++++++++++++ scripts/bundle-openclaw.mjs | 45 ++++++++++++++++++++++++++++++++++++- 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/scripts/after-pack.cjs b/scripts/after-pack.cjs index 3244d1cec..acdbe7134 100644 --- a/scripts/after-pack.cjs +++ b/scripts/after-pack.cjs @@ -138,6 +138,44 @@ function cleanupNativePlatformPackages(nodeModulesDir, platform, arch) { return removed; } +// ── Broken module patcher ───────────────────────────────────────────────────── +// Some bundled packages have transpiled CJS that sets `module.exports = exports.default` +// without ever assigning `exports.default`, leaving module.exports === undefined. +// This causes `TypeError: Cannot convert undefined or null to object` in Node.js 22+ +// ESM interop (translators.js hasOwnProperty call). We patch these after copying. + +const MODULE_PATCHES = { + // node-domexception@1.0.0: index.js sets module.exports = undefined. + // Node.js 18+ ships DOMException as a built-in; this shim re-exports it. + 'node-domexception/index.js': [ + "'use strict';", + '// Shim: original transpiled file sets module.exports = exports.default (undefined).', + '// Node.js 18+ has DOMException as a built-in global.', + 'const dom = globalThis.DOMException ||', + ' class DOMException extends Error {', + " constructor(msg, name) { super(msg); this.name = name || 'Error'; }", + ' };', + 'module.exports = dom;', + 'module.exports.DOMException = dom;', + 'module.exports.default = dom;', + ].join('\n') + '\n', +}; + +function patchBrokenModules(nodeModulesDir) { + const { writeFileSync } = require('fs'); + let count = 0; + for (const [rel, content] of Object.entries(MODULE_PATCHES)) { + const target = join(nodeModulesDir, rel); + if (existsSync(target)) { + writeFileSync(target, content, 'utf8'); + count++; + } + } + if (count > 0) { + console.log(`[after-pack] 🩹 Patched ${count} broken module(s) in ${nodeModulesDir}`); + } +} + // ── Plugin bundler ─────────────────────────────────────────────────────────── // Bundles a single OpenClaw plugin (and its transitive deps) from node_modules // directly into the packaged resources directory. Mirrors the logic in @@ -289,6 +327,10 @@ exports.default = async function afterPack(context) { cpSync(src, dest, { recursive: true }); console.log('[after-pack] ✅ openclaw node_modules copied.'); + // Patch broken modules whose CJS transpiled output sets module.exports = undefined, + // causing TypeError in Node.js 22+ ESM interop. + patchBrokenModules(dest); + // 1.1 Bundle OpenClaw plugins directly from node_modules into packaged resources. // This is intentionally done in afterPack (not extraResources) because: // - electron-builder silently skips extraResources entries whose source diff --git a/scripts/bundle-openclaw.mjs b/scripts/bundle-openclaw.mjs index cbc913a65..be4d605ac 100644 --- a/scripts/bundle-openclaw.mjs +++ b/scripts/bundle-openclaw.mjs @@ -366,7 +366,50 @@ const sizeAfter = getDirSize(OUTPUT); echo` Removed ${cleanedCount} files/directories`; echo` Size: ${formatSize(sizeBefore)} → ${formatSize(sizeAfter)} (saved ${formatSize(sizeBefore - sizeAfter)})`; -// 7. Verify the bundle +// 7. Patch known broken packages +// +// Some packages in the ecosystem have transpiled CJS output that sets +// `module.exports = exports.default` without ever assigning `exports.default`, +// resulting in `module.exports = undefined`. This causes a TypeError in +// Node.js 22+ ESM interop when the translators try to call hasOwnProperty on +// the undefined exports object. +// +// We patch these files in-place after the copy so the bundle is safe to run. +function patchBrokenModules(nodeModulesDir) { + const patches = { + // node-domexception@1.0.0: transpiled index.js leaves module.exports = undefined. + // Node.js 18+ ships DOMException as a built-in global, so a simple shim works. + 'node-domexception/index.js': [ + `'use strict';`, + `// Shim: the original transpiled file sets module.exports = exports.default`, + `// (which is undefined), causing TypeError in Node.js 22+ ESM interop.`, + `// Node.js 18+ has DOMException as a built-in global.`, + `const dom = globalThis.DOMException ||`, + ` class DOMException extends Error {`, + ` constructor(msg, name) { super(msg); this.name = name || 'Error'; }`, + ` };`, + `module.exports = dom;`, + `module.exports.DOMException = dom;`, + `module.exports.default = dom;`, + ].join('\n'), + }; + + let count = 0; + for (const [rel, content] of Object.entries(patches)) { + const target = path.join(nodeModulesDir, rel); + if (fs.existsSync(target)) { + fs.writeFileSync(target, content + '\n', 'utf8'); + count++; + } + } + if (count > 0) { + echo` 🩹 Patched ${count} broken module(s) in node_modules`; + } +} + +patchBrokenModules(outputNodeModules); + +// 8. Verify the bundle const entryExists = fs.existsSync(path.join(OUTPUT, 'openclaw.mjs')); const distExists = fs.existsSync(path.join(OUTPUT, 'dist', 'entry.js'));