Skip to content

Commit 596a4ca

Browse files
committed
feat(packages/run): add elideNodeDebuggerStringsFromStderr option
1 parent b1b642d commit 596a4ca

File tree

4 files changed

+110
-0
lines changed

4 files changed

+110
-0
lines changed

packages/run/package.json

+5
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,15 @@
6767
"test:packages:all": "symbiote test --env NODE_NO_WARNINGS=1 --tests all-local --scope unlimited --coverage"
6868
},
6969
"dependencies": {
70+
"core-js": "^3.40.0",
7071
"execa": "^9.5.2",
7172
"rejoinder": "^1.2.4",
73+
"strip-final-newline": "^4.0.0",
7274
"type-fest": "^4.34.1"
7375
},
76+
"devDependencies": {
77+
"@-xun/fs": "^1.0.0"
78+
},
7479
"engines": {
7580
"node": "^18.20.0 || ^20.18.0 || ^22.12.0 || >=23.3.0"
7681
},

packages/run/src/index.ts

+39
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ import type {
1313
let runCounter = 0;
1414
const debug = createDebugLogger({ namespace: globalDebuggerNamespace });
1515

16+
const nodejsDebugStringRegExps = [
17+
/^Debugger attached\.(\n|$)/gm,
18+
/^Debugger listening on .+(\n|$)/gm,
19+
/^For help, see: https:\/\/nodejs\.org\/en\/docs\/inspector(\n|$)/gm,
20+
/^Waiting for the debugger to disconnect\.\.\.(\n|$)/gm
21+
];
22+
1623
export type { Subprocess } from 'execa' with { 'resolution-mode': 'import' };
1724
export type * from 'universe+run:types.ts';
1825

@@ -27,6 +34,9 @@ export type * from 'universe+run:types.ts';
2734
*
2835
* 2. Coerces output to a string. Set `coerceOutputToString: false` (or `lines:
2936
* true`) to override this.
37+
*
38+
* 3. Elides Node.js debugger strings. Set `elideNodeDebuggerStringsFromStderr:
39+
* false` to override this.
3040
*/
3141
export async function run(
3242
file: string,
@@ -48,6 +58,7 @@ export async function run<OptionsType extends RunOptions = DefaultRunOptions>(
4858
const {
4959
useIntermediate,
5060
coerceOutputToString = true,
61+
elideNodeDebuggerStringsFromStderr = true,
5162
...execaOptions
5263
} = options || ({} as RunOptions);
5364

@@ -57,7 +68,9 @@ export async function run<OptionsType extends RunOptions = DefaultRunOptions>(
5768

5869
const shouldCoerceOutputToString = coerceOutputToString && !execaOptions.lines;
5970
runDebug('output coercion: %O', shouldCoerceOutputToString);
71+
runDebug('debug elision: %O', elideNodeDebuggerStringsFromStderr);
6072

73+
const stripFinalNewline = (await import('strip-final-newline')).default;
6174
const intermediateResult = (await import('execa')).execa(file, args, execaOptions);
6275

6376
await useIntermediate?.(intermediateResult);
@@ -70,6 +83,25 @@ export async function run<OptionsType extends RunOptions = DefaultRunOptions>(
7083
finalResult.stderr = finalResult.stderr?.toString() ?? '';
7184
}
7285

86+
if (elideNodeDebuggerStringsFromStderr) {
87+
/* istanbul ignore else */
88+
if (typeof finalResult.stderr === 'string') {
89+
let { stderr } = finalResult;
90+
91+
nodejsDebugStringRegExps.forEach((regExp) => {
92+
stderr = stderr.replaceAll(regExp, '');
93+
});
94+
95+
finalResult.stderr =
96+
execaOptions.stripFinalNewline === false ? stderr : stripFinalNewline(stderr);
97+
} else if (Array.isArray(finalResult.stderr)) {
98+
finalResult.stderr = finalResult.stderr.filter((filterTarget_) => {
99+
const filterTarget = String(filterTarget_);
100+
return nodejsDebugStringRegExps.every((regExp) => !filterTarget.match(regExp));
101+
});
102+
}
103+
}
104+
73105
runDebug('execution result: %O', finalResult);
74106
return finalResult;
75107
}
@@ -94,6 +126,7 @@ export async function runWithInheritedIo(
94126
return run(file, args, {
95127
...options,
96128
coerceOutputToString: false,
129+
elideNodeDebuggerStringsFromStderr: false,
97130
stdio: 'inherit'
98131
}) as Promise<Omit<RunReturnType, 'all' | 'stdout' | 'stderr' | 'stdio'>>;
99132
}
@@ -108,6 +141,9 @@ export async function runWithInheritedIo(
108141
*
109142
* 2. Coerces output to a string. Set `coerceOutputToString: false` (or `lines:
110143
* true`) to override this.
144+
*
145+
* 3. Elides Node.js debugger strings. Set `elideNodeDebuggerStringsFromStderr:
146+
* false` to override this.
111147
*/
112148
export async function runNoRejectOnBadExit(
113149
file: string,
@@ -142,6 +178,9 @@ export async function runNoRejectOnBadExit<
142178
*
143179
* 2. Coerces output to a string. Set `coerceOutputToString: false` (or
144180
* `lines: true`) to override this.
181+
*
182+
* 3. Elides Node.js debugger strings. Set `elideNodeDebuggerStringsFromStderr:
183+
* false` to override this.
145184
*/
146185
export function runnerFactory<FactoryOptionsType extends RunOptions = DefaultRunOptions>(
147186
file: string,

packages/run/src/types.ts

+11
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ export type RunOptions = ExecaOptions & {
2424
* output will be coerced to an array of strings regardless of this option.
2525
*/
2626
coerceOutputToString?: boolean;
27+
/**
28+
* When attaching a debugger to a Node.js process, such as when using the
29+
* `--inspect*` flags, Node will add it's own "debugger attached" lines to
30+
* stderr. Set `elideNodeDebuggerStringsFromStderr` to `true` to remove them
31+
* automatically, or `false` to do nothing.
32+
*
33+
* @default true
34+
* @see https://github.com/nodejs/node/issues/34799
35+
*/
36+
elideNodeDebuggerStringsFromStderr?: boolean;
2737
};
2838

2939
/**
@@ -32,6 +42,7 @@ export type RunOptions = ExecaOptions & {
3242
export type DefaultRunOptions = RunOptions & {
3343
lines: false;
3444
coerceOutputToString: true;
45+
elideNodeDebuggerStringsFromStderr: true;
3546
all: false;
3647
};
3748

packages/run/test/unit.test.ts

+55
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,61 @@ describe('::run', () => {
6969
expect(result.all).toStrictEqual(testOutput.split('\n'));
7070
}
7171
});
72+
73+
it('elides Node.js inspector debug strings from stderr by default', async () => {
74+
expect.hasAssertions();
75+
76+
const testScriptString = /* js */ `
77+
console.error('Waiting for the debugger to disconnect...');
78+
console.error('Debugger listening on ws://127.0.0.1:9229/4dad5ead-6482-44b3-9c2c-8e880788863f');
79+
console.error('For help, see: https://nodejs.org/en/docs/inspector');
80+
console.error('Debugger attached.');
81+
console.error('test');
82+
console.error('Debugger attached.');
83+
console.error('Debugger attached.');
84+
console.error('Waiting for the debugger to disconnect...');
85+
console.error('Waiting for the debugger to disconnect...');
86+
`;
87+
88+
{
89+
const result = await run('node', ['-e', testScriptString]);
90+
91+
expect(result.stdout).toBeEmpty();
92+
expect(result.stderr).toBe('test');
93+
expect(result.exitCode).toBe(0);
94+
}
95+
96+
{
97+
const result = await run('node', ['-e', testScriptString], {
98+
lines: true
99+
});
100+
101+
expect(result.stdout).toBeEmpty();
102+
expect(result.stderr).toStrictEqual(['test']);
103+
expect(result.exitCode).toBe(0);
104+
}
105+
106+
{
107+
const result = await run('node', ['-e', testScriptString], {
108+
elideNodeDebuggerStringsFromStderr: false
109+
});
110+
111+
expect(result.stdout).toBeEmpty();
112+
expect(result.stderr).not.toBe('test');
113+
expect(result.exitCode).toBe(0);
114+
}
115+
116+
{
117+
const result = await run('node', ['-e', testScriptString], {
118+
lines: true,
119+
elideNodeDebuggerStringsFromStderr: false
120+
});
121+
122+
expect(result.stdout).toBeEmpty();
123+
expect(result.stderr).not.toStrictEqual(['test']);
124+
expect(result.exitCode).toBe(0);
125+
}
126+
});
72127
});
73128

74129
describe('::runWithInheritedIo', () => {

0 commit comments

Comments
 (0)