Skip to content

Commit ce4cc26

Browse files
committed
docs: document CJS/ESM interop issue and solution in AGENTS.md
All four import strategies fail in OpenCode's Bun runtime for CJS packages that use Object.defineProperty getters. The only reliable fix is to bundle CJS dependencies with tsup by moving them to devDependencies. Signed-off-by: xuezhaojun <xuezhaokeepgoing@gmail.com>
1 parent 3cc34ea commit ce4cc26

1 file changed

Lines changed: 69 additions & 0 deletions

File tree

AGENTS.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,74 @@ OIDC Trusted Publishing requires the package to already exist on npm:
303303

304304
All plugins are published under the `@kubeopencode` npm scope (e.g. `@kubeopencode/opencode-slack-plugin`). The `package.json` `name` field must use this scoped format.
305305

306+
## CJS/ESM Interop (Critical)
307+
308+
OpenCode plugins run inside **Bun's JavaScript runtime**, not Node.js. Bun's CJS/ESM interop differs from Node.js in ways that cause silent failures for CJS packages that use `Object.defineProperty` (getter/setter) for their exports.
309+
310+
### The Problem
311+
312+
Packages like `@slack/web-api` and `@slack/socket-mode` are **CJS modules** that export classes via `Object.defineProperty`:
313+
314+
```js
315+
// @slack/web-api/dist/index.js
316+
Object.defineProperty(exports, "WebClient", { enumerable: true, get: function() { return ... } });
317+
```
318+
319+
In **Node.js**, `import { WebClient } from "@slack/web-api"` works because Node creates a live binding to the getter. But **Bun** (which OpenCode uses internally to load plugins via `await import()`) creates a namespace object that **drops getter-defined properties**. This means:
320+
321+
| Import style | Node.js | Bun (OpenCode runtime) |
322+
|---|---|---|
323+
| `import { WebClient }` ||`undefined` |
324+
| `import * as M; M.WebClient` ||`undefined` |
325+
| `const { WebClient } = await import(...)` ||`undefined` |
326+
| `createRequire()(...).WebClient` ||`require() async module unsupported` |
327+
328+
All approaches fail. The error manifests as:
329+
330+
```
331+
undefined is not a constructor (evaluating 'new WebClient(botToken)')
332+
```
333+
334+
### The Solution: Bundle CJS Dependencies
335+
336+
**Always bundle CJS dependencies into the plugin output.** This eliminates runtime CJS/ESM interop entirely — tsup's bundler resolves all exports at build time using `__commonJS` wrappers that correctly handle `Object.defineProperty`.
337+
338+
**Configuration:**
339+
340+
1. Move CJS dependencies from `dependencies` to `devDependencies` in `package.json`:
341+
342+
```json
343+
{
344+
"dependencies": {},
345+
"devDependencies": {
346+
"@slack/web-api": "^7.13.0",
347+
"@slack/socket-mode": "^2.0.5"
348+
}
349+
}
350+
```
351+
352+
2. tsup automatically bundles `devDependencies` — no extra config needed. Dependencies listed in `dependencies` are treated as external (left as `import` statements in the output).
353+
354+
3. Use standard named imports in source:
355+
356+
```ts
357+
import { WebClient } from "@slack/web-api"
358+
import { SocketModeClient } from "@slack/socket-mode"
359+
```
360+
361+
**Trade-off:** The bundle grows from ~17KB to ~830KB when including `@slack/web-api`. This is acceptable for a plugin that runs once per Agent pod.
362+
363+
### Detection Checklist
364+
365+
If a plugin dependency uses CJS (`"type"` is not `"module"` or absent in `package.json`, `main` points to a `.js` file without ESM exports), and uses `Object.defineProperty` for exports, it **must** be bundled. Check with:
366+
367+
```bash
368+
# Does the package use Object.defineProperty for exports?
369+
grep "Object.defineProperty(exports" node_modules/<pkg>/dist/index.js
370+
```
371+
372+
If you see `Object.defineProperty(exports, "ClassName"`, the package has this issue and **must** be bundled.
373+
306374
## Style Guide
307375

308376
- TypeScript, ESM (`"type": "module"`)
@@ -311,3 +379,4 @@ All plugins are published under the `@kubeopencode` npm scope (e.g. `@kubeopenco
311379
- Avoid `try/catch` when possible; use `.catch(() => {})` for best-effort operations
312380
- Keep each plugin in a single file unless complexity demands splitting
313381
- English comments only
382+
- **CJS dependencies must be bundled** (see CJS/ESM Interop section above)

0 commit comments

Comments
 (0)