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