1import * as common from '../common/index.mjs'; 2import tmpdir from '../common/tmpdir.js'; 3import assert from 'node:assert'; 4import path from 'node:path'; 5import { execPath } from 'node:process'; 6import { describe, it } from 'node:test'; 7import { spawn } from 'node:child_process'; 8import { writeFileSync, readFileSync, mkdirSync } from 'node:fs'; 9import { inspect } from 'node:util'; 10import { pathToFileURL } from 'node:url'; 11import { createInterface } from 'node:readline'; 12 13if (common.isIBMi) 14 common.skip('IBMi does not support `fs.watch()`'); 15 16const supportsRecursive = common.isOSX || common.isWindows; 17 18function restart(file, content = readFileSync(file)) { 19 // To avoid flakiness, we save the file repeatedly until test is done 20 writeFileSync(file, content); 21 const timer = setInterval(() => writeFileSync(file, content), common.platformTimeout(2500)); 22 return () => clearInterval(timer); 23} 24 25let tmpFiles = 0; 26function createTmpFile(content = 'console.log("running");', ext = '.js', basename = tmpdir.path) { 27 const file = path.join(basename, `${tmpFiles++}${ext}`); 28 writeFileSync(file, content); 29 return file; 30} 31 32async function runWriteSucceed({ 33 file, watchedFile, args = [file], completed = 'Completed running', restarts = 2 34}) { 35 const child = spawn(execPath, ['--watch', '--no-warnings', ...args], { encoding: 'utf8', stdio: 'pipe' }); 36 let completes = 0; 37 let cancelRestarts = () => {}; 38 let stderr = ''; 39 const stdout = []; 40 41 child.stderr.on('data', (data) => { 42 stderr += data; 43 }); 44 45 try { 46 // Break the chunks into lines 47 for await (const data of createInterface({ input: child.stdout })) { 48 if (!data.startsWith('Waiting for graceful termination') && !data.startsWith('Gracefully restarted')) { 49 stdout.push(data); 50 } 51 if (data.startsWith(completed)) { 52 completes++; 53 if (completes === restarts) { 54 break; 55 } 56 if (completes === 1) { 57 cancelRestarts = restart(watchedFile); 58 } 59 } 60 } 61 } finally { 62 child.kill(); 63 cancelRestarts(); 64 } 65 return { stdout, stderr }; 66} 67 68async function failWriteSucceed({ file, watchedFile }) { 69 const child = spawn(execPath, ['--watch', '--no-warnings', file], { encoding: 'utf8', stdio: 'pipe' }); 70 let cancelRestarts = () => {}; 71 72 try { 73 // Break the chunks into lines 74 for await (const data of createInterface({ input: child.stdout })) { 75 if (data.startsWith('Completed running')) { 76 break; 77 } 78 if (data.startsWith('Failed running')) { 79 cancelRestarts = restart(watchedFile, 'console.log("test has ran");'); 80 } 81 } 82 } finally { 83 child.kill(); 84 cancelRestarts(); 85 } 86} 87 88tmpdir.refresh(); 89 90describe('watch mode', { concurrency: true, timeout: 60_000 }, () => { 91 it('should watch changes to a file - event loop ended', async () => { 92 const file = createTmpFile(); 93 const { stderr, stdout } = await runWriteSucceed({ file, watchedFile: file }); 94 95 assert.strictEqual(stderr, ''); 96 assert.deepStrictEqual(stdout, [ 97 'running', 98 `Completed running ${inspect(file)}`, 99 `Restarting ${inspect(file)}`, 100 'running', 101 `Completed running ${inspect(file)}`, 102 ]); 103 }); 104 105 it('should watch changes to a failing file', async () => { 106 const file = createTmpFile('throw new Error("fails");'); 107 const { stderr, stdout } = await runWriteSucceed({ file, watchedFile: file, completed: 'Failed running' }); 108 109 assert.match(stderr, /Error: fails\r?\n/); 110 assert.deepStrictEqual(stdout, [ 111 `Failed running ${inspect(file)}`, 112 `Restarting ${inspect(file)}`, 113 `Failed running ${inspect(file)}`, 114 ]); 115 }); 116 117 it('should watch changes to a file with watch-path', { 118 skip: !supportsRecursive, 119 }, async () => { 120 const dir = path.join(tmpdir.path, 'subdir1'); 121 mkdirSync(dir); 122 const file = createTmpFile(); 123 const watchedFile = createTmpFile('', '.js', dir); 124 const args = ['--watch-path', dir, file]; 125 const { stderr, stdout } = await runWriteSucceed({ file, watchedFile, args }); 126 127 assert.strictEqual(stderr, ''); 128 assert.deepStrictEqual(stdout, [ 129 'running', 130 `Completed running ${inspect(file)}`, 131 `Restarting ${inspect(file)}`, 132 'running', 133 `Completed running ${inspect(file)}`, 134 ]); 135 assert.strictEqual(stderr, ''); 136 }); 137 138 it('should watch when running an non-existing file - when specified under --watch-path', { 139 skip: !supportsRecursive 140 }, async () => { 141 const dir = path.join(tmpdir.path, 'subdir2'); 142 mkdirSync(dir); 143 const file = path.join(dir, 'non-existing.js'); 144 const watchedFile = createTmpFile('', '.js', dir); 145 const args = ['--watch-path', dir, file]; 146 const { stderr, stdout } = await runWriteSucceed({ file, watchedFile, args, completed: 'Failed running' }); 147 148 assert.match(stderr, /Error: Cannot find module/g); 149 assert.deepStrictEqual(stdout, [ 150 `Failed running ${inspect(file)}`, 151 `Restarting ${inspect(file)}`, 152 `Failed running ${inspect(file)}`, 153 ]); 154 }); 155 156 it('should watch when running an non-existing file - when specified under --watch-path with equals', { 157 skip: !supportsRecursive 158 }, async () => { 159 const dir = path.join(tmpdir.path, 'subdir3'); 160 mkdirSync(dir); 161 const file = path.join(dir, 'non-existing.js'); 162 const watchedFile = createTmpFile('', '.js', dir); 163 const args = [`--watch-path=${dir}`, file]; 164 const { stderr, stdout } = await runWriteSucceed({ file, watchedFile, args, completed: 'Failed running' }); 165 166 assert.match(stderr, /Error: Cannot find module/g); 167 assert.deepStrictEqual(stdout, [ 168 `Failed running ${inspect(file)}`, 169 `Restarting ${inspect(file)}`, 170 `Failed running ${inspect(file)}`, 171 ]); 172 }); 173 174 it('should watch changes to a file - event loop blocked', { timeout: 10_000 }, async () => { 175 const file = createTmpFile(` 176console.log("running"); 177Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0); 178console.log("don't show me");`); 179 const { stderr, stdout } = await runWriteSucceed({ file, watchedFile: file, completed: 'running' }); 180 181 assert.strictEqual(stderr, ''); 182 assert.deepStrictEqual(stdout, [ 183 'running', 184 `Restarting ${inspect(file)}`, 185 'running', 186 ]); 187 }); 188 189 it('should watch changes to dependencies - cjs', async () => { 190 const dependency = createTmpFile('module.exports = {};'); 191 const file = createTmpFile(` 192const dependency = require(${JSON.stringify(dependency)}); 193console.log(dependency); 194`); 195 const { stderr, stdout } = await runWriteSucceed({ file, watchedFile: dependency }); 196 197 assert.strictEqual(stderr, ''); 198 assert.deepStrictEqual(stdout, [ 199 '{}', 200 `Completed running ${inspect(file)}`, 201 `Restarting ${inspect(file)}`, 202 '{}', 203 `Completed running ${inspect(file)}`, 204 ]); 205 }); 206 207 it('should watch changes to dependencies - esm', async () => { 208 const dependency = createTmpFile('module.exports = {};'); 209 const file = createTmpFile(` 210import dependency from ${JSON.stringify(pathToFileURL(dependency))}; 211console.log(dependency); 212`, '.mjs'); 213 const { stderr, stdout } = await runWriteSucceed({ file, watchedFile: dependency }); 214 215 assert.strictEqual(stderr, ''); 216 assert.deepStrictEqual(stdout, [ 217 '{}', 218 `Completed running ${inspect(file)}`, 219 `Restarting ${inspect(file)}`, 220 '{}', 221 `Completed running ${inspect(file)}`, 222 ]); 223 }); 224 225 it('should restart multiple times', async () => { 226 const file = createTmpFile(); 227 const { stderr, stdout } = await runWriteSucceed({ file, watchedFile: file, restarts: 3 }); 228 229 assert.strictEqual(stderr, ''); 230 assert.deepStrictEqual(stdout, [ 231 'running', 232 `Completed running ${inspect(file)}`, 233 `Restarting ${inspect(file)}`, 234 'running', 235 `Completed running ${inspect(file)}`, 236 `Restarting ${inspect(file)}`, 237 'running', 238 `Completed running ${inspect(file)}`, 239 ]); 240 }); 241 242 it('should pass arguments to file', async () => { 243 const file = createTmpFile(` 244const { parseArgs } = require('node:util'); 245const { values } = parseArgs({ options: { random: { type: 'string' } } }); 246console.log(values.random); 247 `); 248 const random = Date.now().toString(); 249 const args = [file, '--random', random]; 250 const { stderr, stdout } = await runWriteSucceed({ file, watchedFile: file, args }); 251 252 assert.strictEqual(stderr, ''); 253 assert.deepStrictEqual(stdout, [ 254 random, 255 `Completed running ${inspect(`${file} --random ${random}`)}`, 256 `Restarting ${inspect(`${file} --random ${random}`)}`, 257 random, 258 `Completed running ${inspect(`${file} --random ${random}`)}`, 259 ]); 260 }); 261 262 it('should not load --require modules in main process', async () => { 263 const file = createTmpFile(); 264 const required = createTmpFile('setImmediate(() => process.exit(0));'); 265 const args = ['--require', required, file]; 266 const { stderr, stdout } = await runWriteSucceed({ file, watchedFile: file, args }); 267 268 assert.strictEqual(stderr, ''); 269 assert.deepStrictEqual(stdout, [ 270 'running', 271 `Completed running ${inspect(file)}`, 272 `Restarting ${inspect(file)}`, 273 'running', 274 `Completed running ${inspect(file)}`, 275 ]); 276 }); 277 278 it('should not load --import modules in main process', { 279 skip: 'enable once --import is backported', 280 }, async () => { 281 const file = createTmpFile(); 282 const imported = pathToFileURL(createTmpFile('setImmediate(() => process.exit(0));')); 283 const args = ['--import', imported, file]; 284 const { stderr, stdout } = await runWriteSucceed({ file, watchedFile: file, args }); 285 286 assert.strictEqual(stderr, ''); 287 assert.deepStrictEqual(stdout, [ 288 'running', 289 `Completed running ${inspect(file)}`, 290 `Restarting ${inspect(file)}`, 291 'running', 292 `Completed running ${inspect(file)}`, 293 ]); 294 }); 295 296 // TODO: Remove skip after https://github.com/nodejs/node/pull/45271 lands 297 it('should not watch when running an missing file', { 298 skip: !supportsRecursive 299 }, async () => { 300 const nonExistingfile = path.join(tmpdir.path, `${tmpFiles++}.js`); 301 await failWriteSucceed({ file: nonExistingfile, watchedFile: nonExistingfile }); 302 }); 303 304 it('should not watch when running an missing mjs file', { 305 skip: !supportsRecursive 306 }, async () => { 307 const nonExistingfile = path.join(tmpdir.path, `${tmpFiles++}.mjs`); 308 await failWriteSucceed({ file: nonExistingfile, watchedFile: nonExistingfile }); 309 }); 310 311 it('should watch changes to previously missing dependency', { 312 skip: !supportsRecursive 313 }, async () => { 314 const dependency = path.join(tmpdir.path, `${tmpFiles++}.js`); 315 const relativeDependencyPath = `./${path.basename(dependency)}`; 316 const dependant = createTmpFile(`console.log(require('${relativeDependencyPath}'))`); 317 318 await failWriteSucceed({ file: dependant, watchedFile: dependency }); 319 }); 320 321 it('should watch changes to previously missing ESM dependency', { 322 skip: !supportsRecursive 323 }, async () => { 324 const relativeDependencyPath = `./${tmpFiles++}.mjs`; 325 const dependency = path.join(tmpdir.path, relativeDependencyPath); 326 const dependant = createTmpFile(`import ${JSON.stringify(relativeDependencyPath)}`, '.mjs'); 327 328 await failWriteSucceed({ file: dependant, watchedFile: dependency }); 329 }); 330 331 it('should clear output between runs', async () => { 332 const file = createTmpFile(); 333 const { stderr, stdout } = await runWriteSucceed({ file, watchedFile: file }); 334 335 assert.strictEqual(stderr, ''); 336 assert.deepStrictEqual(stdout, [ 337 'running', 338 `Completed running ${inspect(file)}`, 339 `Restarting ${inspect(file)}`, 340 'running', 341 `Completed running ${inspect(file)}`, 342 ]); 343 }); 344 345 it('should preserve output when --watch-preserve-output flag is passed', async () => { 346 const file = createTmpFile(); 347 const args = ['--watch-preserve-output', file]; 348 const { stderr, stdout } = await runWriteSucceed({ file, watchedFile: file, args }); 349 350 assert.strictEqual(stderr, ''); 351 assert.deepStrictEqual(stdout, [ 352 'running', 353 `Completed running ${inspect(file)}`, 354 `Restarting ${inspect(file)}`, 355 'running', 356 `Completed running ${inspect(file)}`, 357 ]); 358 }); 359}); 360