openclaw/scripts/patch-esm-exports.cjs
Alberto Leal 69ec65fc18 fix(plugins): add postinstall patch for ESM-only package exports
jiti (the TS/ESM loader used for plugin loading) converts imports to
CJS require() internally. Three dependencies (@buape/carbon,
osc-progress, @mariozechner/pi-coding-agent) ship export maps with
only an "import" condition and no "default" or "require" fallback,
causing ERR_PACKAGE_PATH_NOT_EXPORTED at runtime. This silently breaks
all plugin loading for any plugin importing from openclaw/plugin-sdk.

Add a postinstall script that walks node_modules and adds the missing
"default" export condition to any package whose exports have "import"
but neither "default" nor "require". The patch is idempotent, has zero
runtime cost, and becomes a no-op if upstream packages add CJS support.
2026-03-21 01:49:25 -04:00

117 lines
3.3 KiB
JavaScript

#!/usr/bin/env node
/**
* Postinstall patch: add "default" export condition to ESM-only packages.
*
* jiti (the TS/ESM loader used at runtime) converts imports to CJS require().
* Some dependencies ship export maps with only an "import" condition and no
* "default" or "require" fallback, which causes ERR_PACKAGE_PATH_NOT_EXPORTED.
* This script walks node_modules and adds the missing "default" condition so
* both ESM and CJS resolution work.
*
* Safe to run multiple times (idempotent). Never exits non-zero.
*/
"use strict";
const fs = require("node:fs");
const path = require("node:path");
const MAX_DEPTH = 8;
const SKIP_DIRS = new Set([".cache", ".store"]);
/**
* Mutate an exports object in-place, adding a "default" condition to any entry
* that has "import" but neither "default" nor "require".
*
* @param {unknown} exports - The "exports" field from package.json
* @returns {boolean} Whether any entry was modified
*/
function patchExports(exports) {
if (typeof exports !== "object" || exports === null || Array.isArray(exports)) {
return false;
}
let modified = false;
for (const key of Object.keys(exports)) {
const entry = exports[key];
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
continue;
}
if ("import" in entry && !("default" in entry) && !("require" in entry)) {
entry.default = entry.import;
modified = true;
}
}
return modified;
}
/**
* Walk a directory tree and patch every package.json whose exports need a
* "default" condition.
*
* @param {string} dir - Root directory to walk (typically node_modules)
* @returns {{ patchedCount: number, errors: Array<{ file: string, error: string }> }}
*/
function patchDir(dir) {
let patchedCount = 0;
const errors = [];
function walk(currentDir, depth) {
if (depth > MAX_DEPTH) {
return;
}
let entries;
try {
entries = fs.readdirSync(currentDir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
const name = entry.name;
if (SKIP_DIRS.has(name)) {
continue;
}
const fullPath = path.join(currentDir, name);
if (name === "package.json") {
try {
const content = fs.readFileSync(fullPath, "utf8");
const pkg = JSON.parse(content);
if (pkg.exports && patchExports(pkg.exports)) {
fs.writeFileSync(fullPath, JSON.stringify(pkg, null, 2) + "\n");
patchedCount++;
}
} catch (err) {
errors.push({ file: fullPath, error: err.message });
}
continue;
}
let isDir = entry.isDirectory();
if (!isDir && entry.isSymbolicLink()) {
try {
isDir = fs.statSync(fullPath).isDirectory();
} catch {
continue;
}
}
if (isDir) {
walk(fullPath, depth + 1);
}
}
}
walk(dir, 0);
return { patchedCount, errors };
}
if (require.main === module) {
try {
const nodeModules = path.resolve(__dirname, "..", "node_modules");
const { patchedCount } = patchDir(nodeModules);
console.log(`patch-esm-exports: patched ${patchedCount} package(s)`);
} catch (err) {
console.warn("patch-esm-exports: unexpected error —", err.message);
}
}
module.exports = { patchExports, patchDir };