• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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