testem 3.19.1 → 3.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/config.js CHANGED
@@ -8,13 +8,11 @@ or `testem.json` config file.
8
8
 
9
9
  */
10
10
 
11
-
12
11
  const os = require('os');
13
12
  const fs = require('fs');
14
13
  const yaml = require('js-yaml');
15
14
  const log = require('./log');
16
15
  const path = require('path');
17
- const glob = require('glob').glob;
18
16
  const url = require('url');
19
17
  const querystring = require('querystring');
20
18
  const { filter, reduce } = require('./utils/promises');
@@ -29,7 +27,7 @@ const _ = require('lodash');
29
27
 
30
28
  const knownBrowsers = require('./utils/known-browsers');
31
29
  const { convertToPosix } = require('./utils/posix');
32
- const globAsync = glob;
30
+ const expandGlobPattern = require('./utils/expand_glob_pattern');
33
31
 
34
32
  class Config {
35
33
  constructor(appMode, progOptions, config) {
@@ -39,7 +37,7 @@ class Config {
39
37
  this.fileOptions = {};
40
38
  this.config = config || {};
41
39
  this.getters = {
42
- test_page: 'getTestPage'
40
+ test_page: 'getTestPage',
43
41
  };
44
42
 
45
43
  if (appMode === 'dev') {
@@ -71,12 +69,31 @@ class Config {
71
69
 
72
70
  // Try all testem.json, testem.yml and testem.js
73
71
  // testem.json gets precedence
74
- let files = ['testem.json', '.testem.json', '.testem.yml', 'testem.yml', 'testem.js', '.testem.js', 'testem.cjs', '.testem.cjs'];
75
- return filter(files.map(this.resolveConfigPath.bind(this)), fileExists).then(matched => {
72
+ let files = [
73
+ 'testem.json',
74
+ '.testem.json',
75
+ '.testem.yml',
76
+ 'testem.yml',
77
+ 'testem.js',
78
+ '.testem.js',
79
+ 'testem.cjs',
80
+ '.testem.cjs',
81
+ ];
82
+ return filter(
83
+ files.map(this.resolveConfigPath.bind(this)),
84
+ fileExists,
85
+ ).then((matched) => {
76
86
  let configFile = matched[0];
77
87
  if (matched.length > 1) {
78
- let baseNames = matched.map(fileName => path.basename(fileName));
79
- console.warn('Found ' + matched.length + ' config files (' + baseNames + '), using ' + baseNames[0]);
88
+ let baseNames = matched.map((fileName) => path.basename(fileName));
89
+ console.warn(
90
+ 'Found ' +
91
+ matched.length +
92
+ ' config files (' +
93
+ baseNames +
94
+ '), using ' +
95
+ baseNames[0],
96
+ );
80
97
  }
81
98
  if (configFile) {
82
99
  this.readConfigFile(configFile, callback);
@@ -99,7 +116,7 @@ class Config {
99
116
 
100
117
  client() {
101
118
  return {
102
- decycle_depth: this.get('client_decycle_depth')
119
+ decycle_depth: this.get('client_decycle_depth'),
103
120
  };
104
121
  }
105
122
 
@@ -122,7 +139,8 @@ class Config {
122
139
  }
123
140
 
124
141
  readConfigFile(configFile, callback) {
125
- if (!configFile) { // allow empty configFile for programmatic setups
142
+ if (!configFile) {
143
+ // allow empty configFile for programmatic setups
126
144
  if (callback) {
127
145
  callback.call(this);
128
146
  }
@@ -143,7 +161,7 @@ class Config {
143
161
  readJS(configFile, callback) {
144
162
  let exportedConfig = require(this.resolveConfigPath(configFile));
145
163
  if (typeof exportedConfig === 'function') {
146
- Promise.resolve(exportedConfig()).then(obj => {
164
+ Promise.resolve(exportedConfig()).then((obj) => {
147
165
  this.fileOptions = obj;
148
166
  if (callback) {
149
167
  callback.call(this);
@@ -158,10 +176,14 @@ class Config {
158
176
  }
159
177
 
160
178
  readYAML(configFile, callback) {
161
- return fs.promises.readFile(configFile)
162
- .then(data => {
163
- this.fileOptions = yaml.load(String(data));
164
- }, () => {})
179
+ return fs.promises
180
+ .readFile(configFile)
181
+ .then(
182
+ (data) => {
183
+ this.fileOptions = yaml.load(String(data));
184
+ },
185
+ () => {},
186
+ )
165
187
  .then(() => {
166
188
  if (callback) {
167
189
  callback.call(this);
@@ -170,11 +192,15 @@ class Config {
170
192
  }
171
193
 
172
194
  readJSON(configFile, callback) {
173
- return fs.promises.readFile(configFile)
174
- .then(data => {
175
- this.fileOptions = JSON.parse(data.toString());
176
- this.progOptions.file = configFile;
177
- }, () => {})
195
+ return fs.promises
196
+ .readFile(configFile)
197
+ .then(
198
+ (data) => {
199
+ this.fileOptions = JSON.parse(data.toString());
200
+ this.progOptions.file = configFile;
201
+ },
202
+ () => {},
203
+ )
178
204
  .then(() => {
179
205
  if (callback) {
180
206
  callback.call(this);
@@ -196,11 +222,12 @@ class Config {
196
222
 
197
223
  let urlObj = url.parse(urlString);
198
224
  let outputQueryParams = querystring.parse(urlObj.query) || {};
199
- Object.keys(queryParamsObj).forEach(param => {
225
+ Object.keys(queryParamsObj).forEach((param) => {
200
226
  outputQueryParams[param] = queryParamsObj[param];
201
227
  });
202
228
  urlObj.query = outputQueryParams;
203
- urlObj.search = querystring.stringify(outputQueryParams)
229
+ urlObj.search = querystring
230
+ .stringify(outputQueryParams)
204
231
  .replace(/=&/g, '&')
205
232
  .replace(/=$/, '');
206
233
  urlObj.path = urlObj.pathname + urlObj.search;
@@ -215,20 +242,32 @@ class Config {
215
242
  testPage = [testPage];
216
243
  }
217
244
 
218
- return testPage.map(page => this.mergeUrlAndQueryParams(page, queryParams));
245
+ return testPage.map((page) =>
246
+ this.mergeUrlAndQueryParams(page, queryParams),
247
+ );
219
248
  }
220
249
 
221
250
  getConfigProperty(key) {
222
251
  if (this.config && key in this.config) {
223
252
  return this.config[key];
224
253
  }
225
- if (key in this.progOptions && typeof this.progOptions[key] !== 'undefined') {
254
+ if (
255
+ key in this.progOptions &&
256
+ typeof this.progOptions[key] !== 'undefined'
257
+ ) {
226
258
  return this.progOptions[key];
227
259
  }
228
- if (key in this.fileOptions && typeof this.fileOptions[key] !== 'undefined') {
260
+ if (
261
+ key in this.fileOptions &&
262
+ typeof this.fileOptions[key] !== 'undefined'
263
+ ) {
229
264
  return this.fileOptions[key];
230
265
  }
231
- if (this.defaultOptions && key in this.defaultOptions && typeof this.defaultOptions[key] !== 'undefined') {
266
+ if (
267
+ this.defaultOptions &&
268
+ key in this.defaultOptions &&
269
+ typeof this.defaultOptions[key] !== 'undefined'
270
+ ) {
232
271
  return this.defaultOptions[key];
233
272
  }
234
273
  if (key in this.defaults) {
@@ -264,27 +303,35 @@ class Config {
264
303
 
265
304
  getAvailableLaunchers(cb) {
266
305
  let browsers = knownBrowsers(process.platform, this);
267
- browser_launcher.getAvailableBrowsers(this, browsers, (err, availableBrowsers) => {
268
- if (err) {
269
- return cb(err);
270
- }
306
+ browser_launcher.getAvailableBrowsers(
307
+ this,
308
+ browsers,
309
+ (err, availableBrowsers) => {
310
+ if (err) {
311
+ return cb(err);
312
+ }
271
313
 
272
- let availableLaunchers = {};
273
- availableBrowsers.forEach(browser => {
274
- let newLauncher = new LauncherFactory(browser.name, browser, this);
275
- availableLaunchers[browser.name.toLowerCase()] = newLauncher;
276
- });
314
+ let availableLaunchers = {};
315
+ availableBrowsers.forEach((browser) => {
316
+ let newLauncher = new LauncherFactory(browser.name, browser, this);
317
+ availableLaunchers[browser.name.toLowerCase()] = newLauncher;
318
+ });
277
319
 
278
- // add custom launchers
279
- let customLaunchers = this.get('launchers');
280
- if (customLaunchers) {
281
- for (let name in customLaunchers) {
282
- let newLauncher = new LauncherFactory(name, customLaunchers[name], this);
283
- availableLaunchers[name.toLowerCase()] = newLauncher;
320
+ // add custom launchers
321
+ let customLaunchers = this.get('launchers');
322
+ if (customLaunchers) {
323
+ for (let name in customLaunchers) {
324
+ let newLauncher = new LauncherFactory(
325
+ name,
326
+ customLaunchers[name],
327
+ this,
328
+ );
329
+ availableLaunchers[name.toLowerCase()] = newLauncher;
330
+ }
284
331
  }
285
- }
286
- cb(null, availableLaunchers);
287
- });
332
+ cb(null, availableLaunchers);
333
+ },
334
+ );
288
335
  }
289
336
 
290
337
  getLaunchers(cb) {
@@ -310,7 +357,7 @@ class Config {
310
357
  let skip = this.get('skip');
311
358
  if (skip) {
312
359
  skip = skip.toLowerCase().split(',');
313
- launchers = launchers.filter(name => skip.indexOf(name) === -1);
360
+ launchers = launchers.filter((name) => skip.indexOf(name) === -1);
314
361
  }
315
362
  return launchers;
316
363
  }
@@ -320,7 +367,7 @@ class Config {
320
367
  let wanted = this.getWantedLauncherNames(available);
321
368
  let err = null;
322
369
 
323
- wanted.forEach(name => {
370
+ wanted.forEach((name) => {
324
371
  let launcher = available[name.toLowerCase()];
325
372
  if (!launcher) {
326
373
  if (this.appMode === 'dev' || this.get('ignore_missing_launchers')) {
@@ -328,10 +375,12 @@ class Config {
328
375
  } else {
329
376
  err = new Error(
330
377
  'Launcher ' +
331
- name +
332
- ' not found. Not installed? Available: ' +
333
- Object.keys(available).map(elm => `"${elm}"`).join(', ') +
334
- '.'
378
+ name +
379
+ ' not found. Not installed? Available: ' +
380
+ Object.keys(available)
381
+ .map((elm) => `"${elm}"`)
382
+ .join(', ') +
383
+ '.',
335
384
  );
336
385
  }
337
386
  } else {
@@ -343,38 +392,62 @@ class Config {
343
392
 
344
393
  printLauncherInfo() {
345
394
  this.getAvailableLaunchers((err, launchers) => {
346
- let launch_in_dev = (this.get('launch_in_dev') || [])
347
- .map(s => s.toLowerCase());
395
+ let launch_in_dev = (this.get('launch_in_dev') || []).map((s) =>
396
+ s.toLowerCase(),
397
+ );
348
398
  let launch_in_ci = this.get('launch_in_ci');
349
399
  if (launch_in_ci) {
350
- launch_in_ci = launch_in_ci.map(s => s.toLowerCase());
400
+ launch_in_ci = launch_in_ci.map((s) => s.toLowerCase());
351
401
  }
352
- launchers = Object.keys(launchers).map(k => launchers[k]);
402
+ launchers = Object.keys(launchers).map((k) => launchers[k]);
353
403
 
354
- const launcherColumnWidth = launchers.reduce((acc, current) => acc > current.name.length ? acc : current.name.length, 0);
404
+ const launcherColumnWidth = launchers.reduce(
405
+ (acc, current) =>
406
+ acc > current.name.length ? acc : current.name.length,
407
+ 0,
408
+ );
355
409
  const launcherTitle = 'Launcher'.padEnd(launcherColumnWidth, ' ');
356
410
  const launcherTitleDivider = ''.padEnd(launcherColumnWidth, '-');
357
411
 
358
- console.log('Have ' + launchers.length + ' launchers available; auto-launch info displayed on the right.');
412
+ console.log(
413
+ 'Have ' +
414
+ launchers.length +
415
+ ' launchers available; auto-launch info displayed on the right.',
416
+ );
359
417
  console.log(); // newline
360
418
  console.log(`${launcherTitle} Type CI Dev`);
361
419
  console.log(`${launcherTitleDivider} ------------ -- ---`);
362
- console.log(launchers.map(launcher => {
363
- let protocol = launcher.settings.protocol;
364
- let kind = protocol === 'browser' ?
365
- 'browser' : (
366
- protocol === 'tap' ?
367
- 'process(TAP)' : 'process');
368
- let dev = launch_in_dev.indexOf(launcher.name.toLowerCase()) !== -1 ?
369
- Chars.mark :
370
- ' ';
371
- let ci = !launch_in_ci || launch_in_ci.indexOf(launcher.name.toLowerCase()) !== -1 ?
372
- Chars.mark :
373
- ' ';
374
- return (pad(launcher.name, launcherColumnWidth + 2, ' ', 1) +
375
- pad(kind, 12, ' ', 1) +
376
- ' ' + ci + ' ' + dev + ' ');
377
- }).join('\n'));
420
+ console.log(
421
+ launchers
422
+ .map((launcher) => {
423
+ let protocol = launcher.settings.protocol;
424
+ let kind =
425
+ protocol === 'browser'
426
+ ? 'browser'
427
+ : protocol === 'tap'
428
+ ? 'process(TAP)'
429
+ : 'process';
430
+ let dev =
431
+ launch_in_dev.indexOf(launcher.name.toLowerCase()) !== -1
432
+ ? Chars.mark
433
+ : ' ';
434
+ let ci =
435
+ !launch_in_ci ||
436
+ launch_in_ci.indexOf(launcher.name.toLowerCase()) !== -1
437
+ ? Chars.mark
438
+ : ' ';
439
+ return (
440
+ pad(launcher.name, launcherColumnWidth + 2, ' ', 1) +
441
+ pad(kind, 12, ' ', 1) +
442
+ ' ' +
443
+ ci +
444
+ ' ' +
445
+ dev +
446
+ ' '
447
+ );
448
+ })
449
+ .join('\n'),
450
+ );
378
451
  });
379
452
  }
380
453
 
@@ -389,7 +462,7 @@ class Config {
389
462
  // Filter glob < 6 negation patterns to still support them
390
463
  // See https://github.com/isaacs/node-glob/tree/3f883c43#comments-and-negation
391
464
  let positiveWants = [];
392
- want.forEach(patternEntry => {
465
+ want.forEach((patternEntry) => {
393
466
  let pattern = isa(patternEntry, String) ? patternEntry : patternEntry.src;
394
467
  if (pattern.indexOf('!') === 0) {
395
468
  return dontWant.push(pattern.substring(1));
@@ -398,33 +471,51 @@ class Config {
398
471
  positiveWants.push(patternEntry);
399
472
  });
400
473
 
401
- dontWant = dontWant.map(p => p ? this.resolvePath(p) : p);
402
- return reduce(positiveWants, (allThatIWant, patternEntry) => {
403
- let pattern = isa(patternEntry, String) ? patternEntry : patternEntry.src;
404
- let attrs = patternEntry.attrs || [];
405
- let patternUrl = url.parse(pattern);
406
-
407
- if (patternUrl.protocol === 'file:') {
408
- pattern = patternUrl.hostname + patternUrl.path;
409
- } else if (patternUrl.protocol) {
410
- return allThatIWant.concat({ src: pattern, attrs: attrs });
411
- }
474
+ dontWant = dontWant.map((p) => (p ? this.resolvePath(p) : p));
475
+ return reduce(
476
+ positiveWants,
477
+ (allThatIWant, patternEntry) => {
478
+ let pattern = isa(patternEntry, String)
479
+ ? patternEntry
480
+ : patternEntry.src;
481
+ let attrs = patternEntry.attrs || [];
482
+ let patternUrl = url.parse(pattern);
483
+
484
+ if (patternUrl.protocol === 'file:') {
485
+ pattern = patternUrl.hostname + patternUrl.path;
486
+ } else if (patternUrl.protocol) {
487
+ return allThatIWant.concat({ src: pattern, attrs: attrs });
488
+ }
412
489
 
413
- return globAsync(convertToPosix(this.resolvePath(pattern)), { ignore: dontWant.map(convertToPosix) })
414
- .then(files => files.sort())
415
- .then(files => allThatIWant.concat(files.map(f => {
416
- f = this.reverseResolvePath(f);
417
- return { src: f, attrs: attrs };
418
- })));
419
- }, [])
420
- .then(result => _.uniqBy(result, 'src'))
421
- .then(result => {
422
- if (callback) { callback(null, result); }
423
- return result;
424
- }, err => {
425
- if (callback) { callback(err); }
426
- throw err;
427
- });
490
+ return expandGlobPattern(
491
+ convertToPosix(this.resolvePath(pattern)),
492
+ dontWant.map(convertToPosix),
493
+ ).then((files) =>
494
+ allThatIWant.concat(
495
+ files.map((f) => {
496
+ f = this.reverseResolvePath(f);
497
+ return { src: f, attrs: attrs };
498
+ }),
499
+ ),
500
+ );
501
+ },
502
+ [],
503
+ )
504
+ .then((result) => _.uniqBy(result, 'src'))
505
+ .then(
506
+ (result) => {
507
+ if (callback) {
508
+ callback(null, result);
509
+ }
510
+ return result;
511
+ },
512
+ (err) => {
513
+ if (callback) {
514
+ callback(err);
515
+ }
516
+ throw err;
517
+ },
518
+ );
428
519
  }
429
520
 
430
521
  getSrcFiles(callback) {
@@ -440,10 +531,10 @@ class Config {
440
531
  return this.getFileSet(want, dontWant, callback);
441
532
  }
442
533
 
443
-
444
534
  getServeFiles(callback) {
445
535
  let want = this.get('serve_files') || this.get('src_files') || '*.js';
446
- let dontWant = this.get('serve_files_ignore') || this.get('src_files_ignore') || '';
536
+ let dontWant =
537
+ this.get('serve_files_ignore') || this.get('src_files_ignore') || '';
447
538
  return this.getFileSet(want, dontWant, callback);
448
539
  }
449
540
 
@@ -471,13 +562,19 @@ class Config {
471
562
  return;
472
563
  }
473
564
  if (o.options) {
474
- o.options.forEach(o => {
565
+ o.options.forEach((o) => {
475
566
  options.push(o.name());
476
567
  });
477
568
  }
478
569
  getOptions(o.parent);
479
570
  }
480
- getOptions(this.progOptions);
571
+ const progOpts = this.progOptions;
572
+ // In subcommand modes, progOptions is a plain object from optsWithGlobals() with
573
+ // a _commander reference pointing to the Command instance for option enumeration.
574
+ // In dev mode (no subcommand), progOptions is the Commander program object directly.
575
+ getOptions(
576
+ progOpts && progOpts._commander ? progOpts._commander : progOpts,
577
+ );
481
578
  return options;
482
579
  }
483
580
 
@@ -497,16 +594,16 @@ class Config {
497
594
  ret[key] = this.config[key];
498
595
  }
499
596
 
500
- const replaceSlashes = f => ({
597
+ const replaceSlashes = (f) => ({
501
598
  src: f.src.replace(/\\/g, '/'),
502
- attrs: f.attrs
599
+ attrs: f.attrs,
503
600
  });
504
601
 
505
602
  try {
506
603
  const [serveFiles, cssFiles, footerScripts] = await Promise.all([
507
604
  this.getServeFiles(),
508
605
  this.getCSSFiles(),
509
- this.getFooterScripts()
606
+ this.getFooterScripts(),
510
607
  ]);
511
608
 
512
609
  ret.serve_files = serveFiles.map(replaceSlashes);
@@ -543,10 +640,13 @@ Config.prototype.defaults = {
543
640
  client_decycle_depth: 5,
544
641
  socket_heartbeat_timeout() {
545
642
  let browserDisconnectTimeout = this.get('browser_disconnect_timeout');
546
- let defaultBrowserDisconnectTimeout = this.defaults.browser_disconnect_timeout;
643
+ let defaultBrowserDisconnectTimeout =
644
+ this.defaults.browser_disconnect_timeout;
547
645
 
548
- return (browserDisconnectTimeout !== defaultBrowserDisconnectTimeout) ? browserDisconnectTimeout : 5;
549
- }
646
+ return browserDisconnectTimeout !== defaultBrowserDisconnectTimeout
647
+ ? browserDisconnectTimeout
648
+ : 5;
649
+ },
550
650
  };
551
651
 
552
652
  module.exports = Config;
@@ -1,51 +1,125 @@
1
+ const EventEmitter = require('events').EventEmitter;
1
2
 
3
+ const Impl = require('./file_watcher_impl');
2
4
 
3
- const fireworm = require('fireworm');
4
- const EventEmitter = require('events').EventEmitter;
5
+ const kCompose = Symbol('FileWatcher.compose');
5
6
 
6
- module.exports = class FileWatcher extends EventEmitter {
7
- constructor(config) {
7
+ /**
8
+ * Coordinates file watching for Testem: it observes the current working directory (and any
9
+ * non-glob paths from config that live outside it), applies the same include / ignore rules as
10
+ * the rest of the app (`src_files`, `src_files_ignore`, `watch_files`, etc.), and reports when
11
+ * paths that matter for the test run change.
12
+ *
13
+ * Listen for {@link FileWatcher#event:fileChanged} to react to changes, and optionally for
14
+ * {@link FileWatcher#event:EMFILE} when the process hits open-file limits.
15
+ *
16
+ * ### Construction
17
+ *
18
+ * **Do not call `new FileWatcher(...)`.** The constructor is not part of the public API; it
19
+ * exists only for internal wiring and will always throw if you invoke it from outside this
20
+ * module. Obtain instances exclusively via {@link FileWatcher.create} with a config object that
21
+ * exposes `get(key)` and `isCwdMode()` (the same shape as Testem’s `Config`).
22
+ *
23
+ * ### Async vs synchronous
24
+ *
25
+ * **Return a `Promise` — use `await`, `.then()`, or `.catch()`:**
26
+ * {@link FileWatcher.create}, {@link FileWatcher#add}, {@link FileWatcher#close}.
27
+ *
28
+ * **Synchronous:** {@link FileWatcher#onFileChanged}, {@link FileWatcher#onEMFILE}, and
29
+ * inherited `EventEmitter` methods (`on`, `once`, `emit`, `removeListener`, …).
30
+ *
31
+ * @hideconstructor
32
+ * @fires FileWatcher#event:fileChanged
33
+ * @fires FileWatcher#event:EMFILE
34
+ */
35
+ class FileWatcher extends EventEmitter {
36
+ /**
37
+ * Not for public use. Application code must use {@link FileWatcher.create} instead; calling
38
+ * `new FileWatcher` directly always throws.
39
+ *
40
+ * @private
41
+ * @param {*} impl Internal implementation instance.
42
+ * @param {symbol} token Internal composition token (not exposed).
43
+ */
44
+ constructor(impl, token) {
8
45
  super();
46
+ if (token !== kCompose) {
47
+ throw new TypeError(
48
+ 'FileWatcher is not constructable; use FileWatcher.create(config)',
49
+ );
50
+ }
51
+ this._impl = impl;
52
+ impl.on('fileChanged', (filePath) => this.emit('fileChanged', filePath));
53
+ impl.on('EMFILE', () => this.emit('EMFILE'));
54
+ }
9
55
 
10
- this.fileWatcher = fireworm('./', {
11
- ignoreInitial: true,
12
- skipDirEntryPatterns: []
56
+ /**
57
+ * Create a watcher, wait until the underlying engine has finished its initial setup, and
58
+ * return a ready-to-use `FileWatcher`. This is the **only** supported way to obtain an
59
+ * instance.
60
+ *
61
+ * @param {{ get: function(string): *, isCwdMode: function(): boolean }} config Testem-style
62
+ * config (typically a `Config` instance): `get` reads keys such as `src_files`,
63
+ * `src_files_ignore`, `watch_files`, `file`, `disable_watching`; `isCwdMode` reflects cwd
64
+ * mode.
65
+ * @returns {Promise<FileWatcher>} Resolves with a connected watcher once startup is complete.
66
+ * **Await** this promise; if the engine fails during startup, the promise rejects.
67
+ */
68
+ static async create(config) {
69
+ const impl = await Impl.create(config);
70
+ return new FileWatcher(impl, kCompose);
71
+ }
72
+
73
+ /**
74
+ * Request that a **concrete** filesystem path be watched in addition to the configured
75
+ * patterns (for example, a file the dev server started serving after startup). Glob or
76
+ * minimatch strings are not accepted here — only real paths; pattern-based includes stay in
77
+ * config.
78
+ *
79
+ * @param {string} file Path relative to the current working directory or absolute, as
80
+ * accepted by the engine.
81
+ * @returns {Promise<void>} Settles when the engine has finished the add request. **Await**
82
+ * this promise (or attach `.catch()`) so validation errors, engine errors, and internal
83
+ * teardown on failure are observed.
84
+ */
85
+ add(file) {
86
+ return this._impl.add(file);
87
+ }
88
+
89
+ /**
90
+ * Tear down the watcher: close the engine, release OS watch handles, and remove listeners on
91
+ * this `FileWatcher`. Safe to call more than once; subsequent calls resolve promptly.
92
+ *
93
+ * @returns {Promise<void>} Resolves when shutdown is complete. **Await** if later code must
94
+ * run only after handles are released.
95
+ */
96
+ close() {
97
+ return Promise.resolve(this._impl.close()).then(() => {
98
+ this.removeAllListeners();
13
99
  });
14
- let onFileChanged = this.onFileChanged.bind(this);
15
- this.fileWatcher.on('change', onFileChanged);
16
- this.fileWatcher.on('add', onFileChanged);
17
- this.fileWatcher.on('remove', onFileChanged);
18
- this.fileWatcher.on('emfile', this.onEMFILE.bind(this));
19
-
20
- let watchFiles = config.get('watch_files');
21
- this.fileWatcher.clear();
22
- let confFile = config.get('file');
23
- if (confFile) {
24
- this.fileWatcher.add(confFile);
25
- }
26
- if (config.isCwdMode()) {
27
- this.fileWatcher.add('*.js');
28
- }
29
- if (watchFiles) {
30
- this.fileWatcher.add(watchFiles);
31
- }
32
- let srcFiles = config.get('src_files') || '*.js';
33
- this.fileWatcher.add(srcFiles);
34
- let ignoreFiles = config.get('src_files_ignore');
35
- if (ignoreFiles) {
36
- this.fileWatcher.ignore(ignoreFiles);
37
- }
38
100
  }
39
101
 
102
+ /**
103
+ * Manually signal that a path changed, for the same listeners as a real `fileChanged` event.
104
+ * Used in tests and narrow control flows; normal file edits are reported automatically by the
105
+ * engine.
106
+ *
107
+ * @param {string} filePath Path that should be treated as changed.
108
+ * @returns {void}
109
+ */
40
110
  onFileChanged(filePath) {
41
- this.emit('fileChanged', filePath);
111
+ return this._impl.onFileChanged(filePath);
42
112
  }
43
113
 
114
+ /**
115
+ * Manually emit the `EMFILE` condition (too many open files) for listeners that show user
116
+ * guidance. The real engine also surfaces this when appropriate.
117
+ *
118
+ * @returns {void}
119
+ */
44
120
  onEMFILE() {
45
- this.emit('EMFILE');
121
+ return this._impl.onEMFILE();
46
122
  }
123
+ }
47
124
 
48
- add(file) {
49
- this.fileWatcher.add(file);
50
- }
51
- };
125
+ module.exports = FileWatcher;