feat(js): add a CommonJS entry point for the synchronous API - #193
feat(js): add a CommonJS entry point for the synchronous API#193konard wants to merge 2 commits into
Conversation
Adding .gitkeep for PR creation (default mode). This file will be removed when the task is complete. Issue: #189
Issue #189: the package exported only ./src/$.mjs, so a CommonJS host could reach the library only through `await import('command-stream')`. That is asynchronous by definition, which made the synchronous ProcessRunner.sync() API unusable at a synchronous launch-time probe boundary. On runtimes without require(esm) the require() call failed with ERR_REQUIRE_ESM; on newer Node.js it resolved to a module namespace object that is not callable as `$`. src/$.cjs is now published under the "require" export condition. It loads the same ESM module graph via require(esm), so require() and import() share one instance (no dual-package hazard) and exports the `$` tagged template with every named export attached to it. Runtimes without require(esm) support get an actionable error instead of ERR_REQUIRE_ESM. - src/$.cjs: CommonJS entry point - package.json: main/module/exports conditions for require and import - tests/commonjs-entry.test.mjs: Bun suite (shape, sync(), shared instance) - tests/node-commonjs-entry.mjs: node --test suite for the CI Node matrix - tests/commonjs-sandbox.mjs: shared bare-specifier sandbox helper - experiments/repro-189-commonjs-require.mjs: issue reproduction harness - examples/commonjs-launch-probe.cjs: synchronous launch-time probe example - eslint.config.js: lint .cjs files as CommonJS scripts - js.yml: exercise the CommonJS entry on Node.js 20, 22 and 24 - README.md: document both module formats
Working session summaryDone. PR #193 is pushed, updated and marked ready for review: #193 Root cause
Fix
Evidence
Verification
One CI note worth flagging: the "JS/Rust source parity" job failed because Also included: This summary was automatically extracted from the AI working session output. |
🤖 Solution Draft LogThis log file contains the complete execution trace of the AI solution draft process. 💰 Cost: $7.682410📊 Context and tokens usage:Claude Opus 5: (2 sub-sessions)
Total: (2.4K new + 151.6K cache writes + 9.0M cache reads) input tokens, 65.2K output tokens, $7.682410 cost 🤖 Models used:
📎 Log file uploaded as Gist (3317KB)Now working session is ended, feel free to review and add any feedback on the solution draft. |
Summary
command-stream@0.18.0published a single ESM specifier, so a CommonJS host could not use the synchronous API:await import('command-stream')works but is asynchronous by definition, which makes it unusable at a synchronous launch-time probe boundary — exactly whereProcessRunner.sync()is most useful.This PR publishes
src/$.cjsunder therequireexport condition. It loads the existing ESM module graph throughrequire(esm)and re-exports the callable$with every named export attached to it.Fixes #189
Root cause
js/package.jsondeclared only the ESM entry:There is no
requirecondition, so Node resolvesrequire('command-stream')straight to the.mjsfile. On runtimes withoutrequire(esm)that throwsERR_REQUIRE_ESM; on Node.js >= 20.19 / >= 22.12 it resolves, but to a module namespace object, which is not callable —loaded`echo hi`fails withTypeError: loaded is not a function.Approach
src/$.cjsis a thin wrapper rather than a bundled CJS build. That keeps a single module instance, sorequire()andimport()share the samevirtualCommandsregistry, shell settings and cleanup state — no dual-package hazard, and no build step:Runtimes without
require(esm)support now get an actionable message instead ofERR_REQUIRE_ESM:Reproduction
js/experiments/repro-189-commonjs-require.mjsinstalls the package into a throwaway sandbox and reports what a CommonJS host observes:$ node js/experiments/repro-189-commonjs-require.mjsBefore (Node.js v20.20.2, package.json without the
requirecondition):After:
Usage
js/examples/commonjs-launch-probe.cjsdemonstrates the issue's real-world use case (synchronous launch-time availability probes) and runs under bothnodeandbun.Tests
js/tests/commonjs-entry.test.mjs— 9 tests: manifest wiring, in-processrequire()shape (callable$,default,__esModule, named exports), a real.sync()call through the CJS entry,require()/import()returning the identicalProcessRunner/shellobjects, and that the ESM$is left unpolluted. Three of them spawn a realnodeprocess resolving the bare specifiercommand-streamfrom a sandbox.js/tests/node-commonjs-entry.mjs—node --testsuite so the CI Node.js matrix (20, 22, 24) exercisesrequire('command-stream')end to end; skips itself on runtimes withoutrequire(esm).js/tests/commonjs-sandbox.mjs— shared sandbox helper used by both suites and the experiment..github/workflows/js.yml— the Node.js compatibility job now loadssrc/$.cjswithrequire()and runs the newnode --testsuite.Full suite: 804 pass, 5 skip, 0 fail across 57 files.
eslint .,prettier --check .andjscpd .are clean.CI on this branch is green: the Node.js matrix reports "CommonJS entry loads successfully in Node.js 20 / 22 / 24" and the new
node --testsuite passes on all three.Changes
js/src/$.cjsjs/package.jsonmain/module/exportsconditions forrequireandimportjs/README.mdjs/eslint.config.js**/*.cjsas CommonJS scriptsjs/.changeset/commonjs-entry-point.mdminorreleaseNo existing behaviour changes: the ESM entry point, its exports and every existing test are untouched.
Language parity
Labelled
parity-exempt: this is a JavaScript module-resolution change (package.jsonexport conditions and a CommonJS entry point). CommonJS has no counterpart in the Rust crate, so there is no equivalent change to make underrust/src/**. No runtime behaviour of the library itself changed.