#16019: fix(plugins): add postinstall patch for ESM-only package exports
scripts
size: L
Cluster:
Plugin Fixes and Enhancements
## Summary
Fixes plugin loading failures caused by ESM-only dependencies that lack CJS-compatible export conditions.
- **Root cause**: jiti (the TS/ESM loader used for plugins) converts `import` to CJS `require()` internally. Three dependencies (`@buape/carbon`, `osc-progress`, `@mariozechner/pi-coding-agent`) ship export maps with only an `"import"` condition — 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`.
- **Fix**: A postinstall script (`scripts/patch-esm-exports.cjs`) 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.
- **Scope**: Affects all deployment modes (npm, Docker, source) — not just custom Docker builds. Most users never notice because plugin failures are non-fatal and the web channel (compiled into the main bundle) works regardless.
### Related issues
- #12854 (same jiti ESM/CJS boundary class, reverse direction)
- #7312 (ESM/CJS barriers blocking OTel instrumentation)
- #15686 (plugins fail to load, gateway continues)
- #7668 (request for e2e tests to catch module errors)
## Test plan
- [x] **22 unit tests** (`src/scripts/patch-esm-exports.test.ts`) — `patchExports` logic, `patchDir` directory walking, edge cases (malformed JSON, string shorthand, idempotency, depth limits, skip dirs), affected package simulations
- [x] **10 e2e tests** (`src/scripts/patch-esm-exports.e2e.test.ts`):
- Reproduces `ERR_PACKAGE_PATH_NOT_EXPORTED` with ESM-only fixtures (proves bug exists)
- Confirms `patchDir` resolves the failure (proves fix works)
- Validates all 3 affected packages resolve via CJS `require.resolve` in real `node_modules`
- Verifies real jiti can resolve `@buape/carbon` through patched exports
- [x] `pnpm check` clean (format + typecheck + lint)
<!-- greptile_comment -->
<h2>Greptile Overview</h2>
<h3>Greptile Summary</h3>
Adds a postinstall script (`scripts/patch-esm-exports.cjs`) that patches ESM-only dependencies (`@buape/carbon`, `osc-progress`, `@mariozechner/pi-coding-agent`) by adding a `"default"` export condition to their `package.json` exports maps. This resolves `ERR_PACKAGE_PATH_NOT_EXPORTED` errors when jiti (the runtime TS/ESM loader) converts `import` to CJS `require()` internally.
- The patch script is idempotent, never exits non-zero, and becomes a no-op once upstream packages add CJS support
- Includes 22 unit tests and 10 e2e tests that reproduce the actual error and verify the fix
- The `patchExports` function handles one-level-deep condition maps (subpath → condition → value), which is sufficient for all three affected packages
- E2e tests verify real `require.resolve` and jiti resolution after patching
<h3>Confidence Score: 5/5</h3>
- This PR is safe to merge — it fixes a real runtime error with an idempotent, zero-runtime-cost postinstall patch backed by thorough tests.
- The patch script is well-structured with proper error handling, never exits non-zero, and is idempotent. The fix is minimal and targeted — it only adds a missing "default" condition to packages that have "import" but no CJS fallback. 32 tests (22 unit + 10 e2e) comprehensively verify both the patching logic and the real-world resolution of affected packages. The code handles edge cases like malformed JSON, symlinks, depth limits, and skipped directories. No functional issues were found.
- No files require special attention.
<sub>Last reviewed commit: f6e58e3</sub>
<!-- greptile_other_comments_section -->
<sub>(2/5) Greptile learns from your feedback when you react with thumbs up/down!</sub>
<!-- /greptile_comment -->
Most Similar PRs
#13109: fix(plugins): inject globalThis.require for CJS interop in jiti-loa...
by mcaxtr · 2026-02-10
81.1%
#13176: fix: resolve llm-task module import for global installs
by striking · 2026-02-10
75.3%
#21039: Fix npm-spec plugin installs when npm pack output is empty
by graysurf · 2026-02-19
75.1%
#20055: fix(plugins): strip workspace:* dev-deps before npm install
by openperf · 2026-02-18
74.7%
#20402: Pr/load openclaw plugins async
by ramarnat · 2026-02-18
74.6%
#20424: Fix plugin extension path traversal in discovery/install
by markmusson · 2026-02-18
73.9%
#14112: test(security): harden plugin install against script execution
by davidahmann · 2026-02-11
73.8%
#18998: fix(llm-task): fall back to dist/ path for runEmbeddedPiAgent import
by Phineas1500 · 2026-02-17
73.7%
#2556: fix(plugin-install): handle existing plugins and filter workspace deps
by longmaba · 2026-01-27
73.4%
#14292: fix(scripts): add js extension
by hannahhoward · 2026-02-11
73.4%