@@ -5,7 +5,7 @@ import path from 'node:path';
55import { execPath } from 'node:process' ;
66import { describe , it } from 'node:test' ;
77import { spawn } from 'node:child_process' ;
8- import { writeFileSync , mkdirSync } from 'node:fs' ;
8+ import { writeFileSync , mkdirSync , chmodSync } from 'node:fs' ;
99import { inspect } from 'node:util' ;
1010import { createInterface } from 'node:readline' ;
1111
@@ -48,6 +48,44 @@ async function runNode({
4848 return { stdout, stderr, pid : child . pid } ;
4949}
5050
51+ async function runExecutable ( {
52+ file,
53+ expectedCompletionLog = 'Completed running' ,
54+ options = { } ,
55+ timeout = common . platformTimeout ( 10_000 ) ,
56+ } ) {
57+ const child = spawn ( file , [ ] , { encoding : 'utf8' , stdio : 'pipe' , ...options } ) ;
58+ let stderr = '' ;
59+ const stdout = [ ] ;
60+ let timedOut = true ;
61+
62+ child . stderr . on ( 'data' , ( data ) => {
63+ stderr += data ;
64+ } ) ;
65+
66+ const timer = setTimeout ( ( ) => {
67+ child . kill ( ) ;
68+ } , timeout ) ;
69+
70+ try {
71+ for await ( const data of createInterface ( { input : child . stdout } ) ) {
72+ if ( ! data . startsWith ( 'Waiting for graceful termination' ) &&
73+ ! data . startsWith ( 'Gracefully restarted' ) ) {
74+ stdout . push ( data ) ;
75+ }
76+ if ( data . startsWith ( expectedCompletionLog ) ) {
77+ timedOut = false ;
78+ break ;
79+ }
80+ }
81+ } finally {
82+ clearTimeout ( timer ) ;
83+ child . kill ( ) ;
84+ }
85+
86+ return { stdout, stderr, timedOut } ;
87+ }
88+
5189tmpdir . refresh ( ) ;
5290
5391describe ( 'watch mode - watch flags' , { concurrency : ! process . env . TEST_PARALLEL , timeout : 60_000 } , ( ) => {
@@ -94,4 +132,38 @@ describe('watch mode - watch flags', { concurrency: !process.env.TEST_PARALLEL,
94132 `Completed running ${ inspect ( file ) } . Waiting for file changes before restarting...` ,
95133 ] ) ;
96134 } ) ;
135+
136+ it ( 'should not recursively re-enter watch mode for shebang scripts when NODE_OPTIONS=--watch' ,
137+ { skip : common . isWindows || process . config . variables . node_without_node_options } ,
138+ async ( ) => {
139+ const projectDir = tmpdir . resolve ( 'project-watch-node-options-shebang' ) ;
140+ mkdirSync ( projectDir ) ;
141+
142+ const file = createTmpFile (
143+ '#!/usr/bin/env node\nconsole.log("shebang run");\n' ,
144+ '.js' ,
145+ projectDir ,
146+ ) ;
147+ chmodSync ( file , 0o755 ) ;
148+
149+ const { stdout, stderr, timedOut } = await runExecutable ( {
150+ file,
151+ options : {
152+ cwd : projectDir ,
153+ env : {
154+ ...process . env ,
155+ NODE_OPTIONS : '--watch' ,
156+ // Ensure shebang resolves this test binary, not system node.
157+ PATH : `${ path . dirname ( execPath ) } ${ path . delimiter } ${ process . env . PATH ?? '' } ` ,
158+ } ,
159+ } ,
160+ } ) ;
161+
162+ assert . strictEqual ( timedOut , false ) ;
163+ assert . strictEqual ( stderr , '' ) ;
164+ assert . deepStrictEqual ( stdout , [
165+ 'shebang run' ,
166+ `Completed running ${ inspect ( file ) } . Waiting for file changes before restarting...` ,
167+ ] ) ;
168+ } ) ;
97169} ) ;
0 commit comments