refactor: enhance argument parsing and add skip file configuration validation#28
refactor: enhance argument parsing and add skip file configuration validation#28AlexF090 wants to merge 5 commits into
Conversation
…lidation - Updated `parseArgs` function to support `--key=value` syntax and accumulate multiple `--skip` flags. - Introduced `isSkipFileConfig` type guard to validate skip file configuration structure. - Improved error handling in `spawnJson` and `spawnText` functions to ensure graceful resolution. - Added tests for new argument parsing features and skip file configuration validation. - Updated regex pattern for `NODE_MODULES_REGEX` to better match nested dependencies.
There was a problem hiding this comment.
Pull request overview
Refactors the CLI’s argument parsing and skip-file handling to be more flexible and safer, while improving robustness of process spawning and dependency-path parsing.
Changes:
- Extend
parseArgsto support--key=valuesyntax and accumulate multiple--skipvalues. - Add
isSkipFileConfigtype guard and use it to validate.outdated-plus-skipcontent before reading fields. - Improve operational robustness (spawn error handling, abort/shutdown behavior) and expand test coverage for new parsing/regex behavior.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
src/args.ts |
Adds --key=value parsing and accumulates multiple --skip inputs; validates skip-file JSON via isSkipFileConfig. |
src/index.ts |
Improves spawn error handling, adds shutdown abort support for fetches, and hardens package.json reading. |
src/lib/utils.ts |
Introduces hasKey helper and isSkipFileConfig type guard; minor refactor in registry response guard. |
src/lib/constants.ts |
Updates NODE_MODULES_REGEX to match the last node_modules/ segment for nested dependencies. |
tests/args.test.ts |
Adds tests for --key=value syntax and multi---skip accumulation behavior. |
tests/utils.test.ts |
Adds tests for isSkipFileConfig type guard correctness. |
tests/cli.test.ts |
Adds tests for updated NODE_MODULES_REGEX, readPackageJson, and getInstalledVersions. |
- Updated `parseArgs` function to prevent duplicate `--skip` values and handle empty values for `--key=`. - Modified `toStringRecord` function to exclude arrays from object validation. - Replaced `mkdirSync` with `mkdtempSync` in tests for better temporary directory management.
…or handling in spawn functions
| obj: object, | ||
| key: K, | ||
| ): obj is Record<K, unknown> { | ||
| return key in obj; |
There was a problem hiding this comment.
hasKey uses the in operator, which also checks the prototype chain. Since this is used for validating data parsed from untrusted JSON (registry responses / skip file), it can incorrectly treat inherited properties as present. Prefer an own-property check (e.g., Object.hasOwn(data, 'packages') / Object.hasOwn(data, 'time')) to make the type guards robust against prototype pollution / unexpected prototypes.
| return key in obj; | |
| return Object.prototype.hasOwnProperty.call(obj, key); |
| const showAll = Boolean(a.get('--show-all')); | ||
| const showWanted = Boolean(a.get('--wanted')); | ||
| const quiet = Boolean(a.get('--quiet')); | ||
| const checkAll = Boolean(a.get('--check-all')); | ||
| const iso = Boolean(a.get('--iso')); |
There was a problem hiding this comment.
With the new --key=value support, boolean options can be provided as --quiet=false / --iso=false, but the current parsing (Boolean(a.get(...))) will treat any non-empty string as true. Consider explicitly parsing string values for boolean flags (e.g., accept true|false|1|0) or rejecting --flag=value for boolean-only options to avoid surprising behavior.
| /** | ||
| * Spawns a command and returns its JSON output. | ||
| * | ||
| * @param cmd - The command to execute (e.g., 'npm'). | ||
| * @param args - Array of command-line arguments. | ||
| * @returns Promise that resolves to the parsed JSON output, or an empty object if parsing fails. | ||
| * @returns Promise that resolves to the parsed JSON output, or an empty object if parsing fails. Rejects if the process fails to spawn (e.g. ENOENT). | ||
| */ | ||
| export function spawnJson(cmd: string, args: string[]): Promise<unknown> { | ||
| return new Promise((resolve) => { | ||
| return new Promise((resolve, reject) => { | ||
| const child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'ignore'] }); | ||
| let out = ''; | ||
| let settled = false; | ||
| child.stdout.on('data', (c) => { | ||
| out += String(c); | ||
| }); | ||
| child.on('error', (err) => { | ||
| if (!settled) { | ||
| settled = true; | ||
| reject(err); | ||
| } | ||
| }); |
There was a problem hiding this comment.
The PR description says spawn errors resolve with an empty object/string, but spawnJson/spawnText now reject on child.on('error') (and run() treats that as a fatal error / exit code 1). Please align either the implementation or the PR description so the documented behavior matches what the CLI will do when npm is missing (ENOENT).
| it('should throw Operation cancelled when shutdown is aborted during fetch', async () => { | ||
| mockFetch.mockImplementation( | ||
| (_url: string, opts?: { signal?: AbortSignal }) => | ||
| new Promise((_, reject) => { | ||
| opts?.signal?.addEventListener?.('abort', () => | ||
| reject(Object.assign(new Error('Aborted'), { name: 'AbortError' })), | ||
| ); | ||
| }), | ||
| ); | ||
|
|
||
| const { fetchPackageMeta } = await import('../src/index.js'); | ||
|
|
||
| const p = fetchPackageMeta('test-pkg'); | ||
| await Promise.resolve(); | ||
| globalThis.__testAbortShutdown?.(); | ||
|
|
||
| await expect(p).rejects.toThrow('Operation cancelled'); | ||
| globalThis.__testResetShutdown?.(); | ||
| }); |
There was a problem hiding this comment.
This test mutates module-level shutdown state via __testAbortShutdown() and only resets it at the end of the test body. If an assertion fails (or the test errors before reaching the reset), later tests may observe an already-aborted shutdown controller and fail/flap. Consider moving __testResetShutdown?.() into an afterEach (or a try/finally in the test) to guarantee cleanup.
Description
Improved CLI robustness and validation: spawn error handling, graceful shutdown on SIGINT/SIGTERM, safe redirect policy, robust parsing of package.json and .outdated-plus-skip, extended argument syntax (
--key=value, multiple--skip), and faster package count calculation without npm subprocess.Type of Change
Changes in Detail
What was changed?
child.on('error')handler andsettledflag so the promise does not hang whennpmis missing or spawn fails.redirect: 'error'to avoid redirect-based risks.toStringRecordhelper instead of blindly trusting the JSON structure.npm list– faster and without subprocess.--key=valuesyntax; multiple--skipvalues are accumulated instead of overwritten..outdated-plus-skipis validated with type guardisSkipFileConfig; invalid structure is ignored.hasKeyhelper andisSkipFileConfigtype guard;isValidNpmRegistryResponsewithoutascasts.NODE_MODULES_REGEXextended to.*node_modules\/(.+)$for nested node_modules paths.Why was it changed?
npmis not in PATH.redirect: 'error'is appropriate.getPackageCountvianpm listis slow and redundant when package.json is already read.--skipvalues and--key=valueimprove CLI usability.How was it implemented?
settledflag; onerrorevent, empty object or empty string is returned.run(); SIGINT/SIGTERM registered; infetchPackageMetathe signal is attached to the request AbortController and removed infinally.JSON.parse,isSkipFileConfig(parsed)is checked; configuration is used only when true.readPackageJson(process.cwd())and counts keys of dependencies and devDependencies (no duplicate filter, since packages appear in only one of the two lists).Testing
npm test)npm run lint)npm run format:check)Test Scenarios
--key=value, multiple--skip, defaults, sort/order/format/concurrency.CLI Behavior
Before
After
Breaking Changes
Migration Guide
N/A.
Checklist
Additional Information
Related Issues
Closes #
Screenshots/Videos
N/A.
Review Notes
errorandclose(settled).finallyto avoid leaks.