fix(inquirerer): defer process.stdin/stdout access in defaultCLIOptions#78
Open
pyramation wants to merge 1 commit intomainfrom
Open
fix(inquirerer): defer process.stdin/stdout access in defaultCLIOptions#78pyramation wants to merge 1 commit intomainfrom
pyramation wants to merge 1 commit intomainfrom
Conversation
Evaluating `input: process.stdin` / `output: process.stdout` as plain properties on the `defaultCLIOptions` object literal meant those streams were constructed at module-load time, not when a `CLI` was actually instantiated. `process.stdin` and `process.stdout` are lazy getters on the process object that materialise Readable/Writable streams on first access. When fd 0 is a pipe (Jest workers, spawned children, backgrounded processes), constructing stdin registers a libuv PIPEWRAP handle that the runtime must close before exit. As a result, any library that transitively required `inquirerer` (e.g. pgsql-test -> pgsql-client -> @pgpmjs/core -> genomic -> inquirerer) would open stdin as a pure import side-effect and trigger false positives under `jest --detectOpenHandles`. Convert the two fields to accessor properties so the stream references are only evaluated when someone reads them (i.e. when `new CLI(...)` actually runs). No behavioural change for direct CLI consumers; the fields still return `process.stdin` / `process.stdout` on read. Add a regression test that (1) asserts the descriptors are getters and (2) spawns a fresh Node process with piped stdin, requires the built package, and verifies no Socket/WriteStream handles remain active.
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
inquirerer/src/commander.tsdefineddefaultCLIOptionswith two eager properties:process.stdin/process.stdoutare Node getters that lazy-construct the underlying Readable / Writable on first access. When fd 0 is a pipe (Jest workers, spawned children, any backgrounded process — i.e. the common case for anything running underjest), constructing stdin registers a libuvPIPEWRAPhandle that the runtime must close before exit.Because those properties were evaluated at module-load time, any library that transitively
requiredinquirererwithout ever constructing aCLIstill opened stdin as a pure import side-effect. The chain that actually burned us:That surfaced in peptide-app-demo#40 as a
PIPEWRAPopen-handle warning fromjest --detectOpenHandleseven though no test was doing anything CLI-related.Fix
Convert
inputandoutputinto accessor properties (getters) soprocess.stdin/process.stdoutare only touched when someone actually reads them (i.e. whennew CLI(...)runs throughcommander.ts:43-46). Behaviour is unchanged for direct consumers —defaultCLIOptions.inputstill returnsprocess.stdinon read.Repro before / after
Probe:
require('inquirerer'), then insetImmediatedumpprocess._getActiveHandles()grouped by constructor name. Stdin piped (echo '' | node probe.js) so fd 0 isPIPEWRAP, matching a Jest worker.Socket(stdin)WriteStream(stdout)Regression test
Added <ref_file file="/home/ubuntu/repos/dev-utils/packages/inquirerer/tests/no-eager-stdio.test.ts" /> — three assertions:
Object.getOwnPropertyDescriptor(defaultCLIOptions, 'input'|'output')is a getter, not a data property (structural guard — cheapest).process.stdin/process.stdout(no behaviour regression).nodewith piped stdin,requires the builtdist/index.js, and asserts_getActiveHandles()contains noSocket/WriteStreamentries (end-to-end guard that survives future refactors).All 9 suites / 153 tests pass locally on
constructive-io/dev-utilsmain.Review & Testing Checklist for Human
CLIOptionsinterface shape (plainReadable/Writable) because TS treats getters as equivalent to data properties for structural typing; make sure nothing in your downstream consumers assumesObject.keys(defaultCLIOptions)enumeratesinput/outputas own-data properties. (Getters are still own and enumerable, soObject.keys/{...defaultCLIOptions}both continue to include them — but worth a glance.)nodeprocess. Timed at ~150ms locally.4.7.1) cut from this sopeptide-app-demo → pgsql-testpicks up the fix without waiting for a minor bump.Notes
defaultCLIOptions.input/.output— they still returnprocess.stdin/process.stdoutverbatim.CLIconstructor destructures these at call-time (<ref_snippet file="/home/ubuntu/repos/dev-utils/packages/inquirerer/src/commander.ts" lines="51-58" />), so the only observable difference is when stdin is opened: atnew CLI()rather than atimport 'inquirerer'.--detectOpenHandleswarning inpeptide-app-demo/packages/integration-testsdisappears with no change in that repo; the Jest scripts can keep--detectOpenHandleson as a guard against new leaks without false positives from this chain.pyramation/inquirerer(the 2.x homepage) locally — happy to open a PR there too if you want the public-mirror repo to stay in sync, but I didn't have push access so it's currently just a local branch.Link to Devin session: https://app.devin.ai/sessions/81001448b7c54b8099437f706d44a30d
Requested by: @pyramation