testem 3.19.1 → 3.20.1

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.
@@ -8,22 +8,21 @@ When the tab is not selected, it hides the associated log panel.
8
8
 
9
9
  */
10
10
 
11
-
12
11
  const SplitLogPanel = require('./split_log_panel');
13
12
  const View = require('./view');
14
13
  const Backbone = require('backbone');
15
14
  const pad = require('../../utils/strutils').pad;
16
15
  const Chars = require('../../utils/chars');
17
16
  const Screen = require('./screen');
18
- const notifier = require('node-notifier');
17
+ const toastNotify = require('./toast_notify');
19
18
  const constants = require('./constants');
20
19
  const TabWidth = constants.TabWidth;
21
20
  const TabStartLine = constants.TabStartLine;
22
21
  const TabHeight = constants.TabHeight;
23
22
  const TabStartCol = constants.TabStartCol;
24
- const RunnerTab = exports.RunnerTab = View.extend({
23
+ const RunnerTab = (exports.RunnerTab = View.extend({
25
24
  defaults: {
26
- allPassed: true
25
+ allPassed: true,
27
26
  },
28
27
  col: TabStartCol,
29
28
  line: TabStartLine,
@@ -46,7 +45,7 @@ const RunnerTab = exports.RunnerTab = View.extend({
46
45
  runner: runner,
47
46
  appview: appview,
48
47
  visible: visible,
49
- screen: this.get('screen')
48
+ screen: this.get('screen'),
50
49
  });
51
50
 
52
51
  this.spinnerIdx = 0;
@@ -56,7 +55,7 @@ const RunnerTab = exports.RunnerTab = View.extend({
56
55
  }
57
56
 
58
57
  this.observe(appview, {
59
- 'change:currentTab': handleCurrentTab
58
+ 'change:currentTab': handleCurrentTab,
60
59
  });
61
60
  this.observe(runner, {
62
61
  'change:name'() {
@@ -77,7 +76,7 @@ const RunnerTab = exports.RunnerTab = View.extend({
77
76
  },
78
77
  'change:allPassed'(model, value) {
79
78
  self.set('allPassed', value);
80
- }
79
+ },
81
80
  });
82
81
 
83
82
  if (results) {
@@ -91,19 +90,19 @@ const RunnerTab = exports.RunnerTab = View.extend({
91
90
  let total = results.get('total');
92
91
  let pending = results.get('pending');
93
92
 
94
- let allPassed = (passed + pending) === total;
93
+ let allPassed = passed + pending === total;
95
94
  let hasTests = total > 0;
96
95
  let failCuzNoTests = !hasTests && config.get('fail_on_zero_tests');
97
- let hasError = runner.get('messages').filter(function(m) {
98
- return m.get('type') === 'error';
99
- }).length > 0;
100
- self.set('allPassed',
101
- allPassed && !failCuzNoTests && !hasError);
96
+ let hasError =
97
+ runner.get('messages').filter(function (m) {
98
+ return m.get('type') === 'error';
99
+ }).length > 0;
100
+ self.set('allPassed', allPassed && !failCuzNoTests && !hasError);
102
101
  }
103
102
  },
104
103
  'change:all'() {
105
104
  self.renderResults();
106
- }
105
+ },
107
106
  });
108
107
  }
109
108
 
@@ -123,7 +122,7 @@ const RunnerTab = exports.RunnerTab = View.extend({
123
122
  self.renderRunnerName();
124
123
  self.renderResults();
125
124
  });
126
- }
125
+ },
127
126
  });
128
127
  this.render();
129
128
 
@@ -132,7 +131,10 @@ const RunnerTab = exports.RunnerTab = View.extend({
132
131
 
133
132
  updateSplitPanelVisibility() {
134
133
  let appview = this.get('appview');
135
- this.splitPanel.set('visible', this.get('selected') && !appview.isPopupVisible());
134
+ this.splitPanel.set(
135
+ 'visible',
136
+ this.get('selected') && !appview.isPopupVisible(),
137
+ );
136
138
  },
137
139
 
138
140
  color() {
@@ -147,7 +149,7 @@ const RunnerTab = exports.RunnerTab = View.extend({
147
149
  let passed = results.get('passed');
148
150
  pending = results.get('pending');
149
151
  let total = results.get('total');
150
- equal = (passed + pending) === total;
152
+ equal = passed + pending === total;
151
153
  hasTests = total > 0;
152
154
  }
153
155
  let failCuzNoTests = !hasTests && config.get('fail_on_zero_tests');
@@ -199,8 +201,7 @@ const RunnerTab = exports.RunnerTab = View.extend({
199
201
  let runnerName = runner.get('name');
200
202
 
201
203
  // write line 1
202
- screen
203
- .foreground(this.color());
204
+ screen.foreground(this.color());
204
205
 
205
206
  if (this.get('selected')) {
206
207
  screen.display('bright');
@@ -233,11 +234,12 @@ const RunnerTab = exports.RunnerTab = View.extend({
233
234
  let passed = results.get('passed');
234
235
  let pending = results.get('pending');
235
236
  resultsDisplay = passed + '/' + total;
236
- equal = (passed + pending) === total;
237
+ equal = passed + pending === total;
237
238
  }
238
239
 
239
240
  if (results && results.get('all')) {
240
- resultsDisplay += ' ' + ((this.get('allPassed') && equal) ? Chars.success : Chars.fail);
241
+ resultsDisplay +=
242
+ ' ' + (this.get('allPassed') && equal ? Chars.success : Chars.fail);
241
243
  } else if (!results && runner.get('allPassed') !== undefined) {
242
244
  resultsDisplay = runner.get('allPassed') ? Chars.success : Chars.fail;
243
245
  } else {
@@ -250,8 +252,7 @@ const RunnerTab = exports.RunnerTab = View.extend({
250
252
  resultsDisplay = pad(resultsDisplay, width - 4, ' ', 2);
251
253
 
252
254
  // write line 1
253
- screen
254
- .foreground(this.color());
255
+ screen.foreground(this.color());
255
256
 
256
257
  if (this.get('selected')) {
257
258
  screen.display('bright');
@@ -267,12 +268,13 @@ const RunnerTab = exports.RunnerTab = View.extend({
267
268
  let runner = this.get('runner');
268
269
  let results = runner.get('results');
269
270
  let name = runner.get('name');
270
- let resultsDisplay = results ?
271
- (results.get('passed') + '/' + results.get('total')) : 'finished';
271
+ let resultsDisplay = results
272
+ ? results.get('passed') + '/' + results.get('total')
273
+ : 'finished';
272
274
 
273
- notifier.notify({
274
- title: 'Test\'em',
275
- message: name + ' : ' + resultsDisplay
275
+ toastNotify.notify({
276
+ title: "Test'em",
277
+ message: name + ' : ' + resultsDisplay,
276
278
  });
277
279
  },
278
280
 
@@ -329,9 +331,11 @@ const RunnerTab = exports.RunnerTab = View.extend({
329
331
  let firstCol = index === 0;
330
332
  screen.position(col, line);
331
333
 
332
- screen.write((firstCol ? Chars.horizontal : Chars.topLeft) +
333
- new Array(width - 1).join(Chars.horizontal) +
334
- Chars.topRight);
334
+ screen.write(
335
+ (firstCol ? Chars.horizontal : Chars.topLeft) +
336
+ new Array(width - 1).join(Chars.horizontal) +
337
+ Chars.topRight,
338
+ );
335
339
  for (let i = 1; i < height - 1; i++) {
336
340
  if (!firstCol) {
337
341
  screen.position(col, line + i);
@@ -341,8 +345,10 @@ const RunnerTab = exports.RunnerTab = View.extend({
341
345
  screen.write(Chars.vertical);
342
346
  }
343
347
 
344
- let bottomLine = (firstCol ? ' ' : Chars.bottomRight) +
345
- new Array(width - 1).join(' ') + Chars.bottomLeft;
348
+ let bottomLine =
349
+ (firstCol ? ' ' : Chars.bottomRight) +
350
+ new Array(width - 1).join(' ') +
351
+ Chars.bottomLeft;
346
352
  screen.position(col, line + height - 1);
347
353
  screen.write(bottomLine);
348
354
  },
@@ -351,8 +357,8 @@ const RunnerTab = exports.RunnerTab = View.extend({
351
357
  this.stopSpinner();
352
358
  this.splitPanel.destroy();
353
359
  View.prototype.destroy.call(this);
354
- }
355
- });
360
+ },
361
+ }));
356
362
 
357
363
  // View container for all the tabs. It'll handle clean up of removed tabs and draw
358
364
  // the edge for where there are no tabs.
@@ -366,12 +372,12 @@ exports.RunnerTabs = Backbone.Collection.extend({
366
372
  let currentTab = self.appview.get('currentTab');
367
373
  if (currentTab >= self.length) {
368
374
  currentTab--;
369
- self.appview.set('currentTab', currentTab, {silent: true});
375
+ self.appview.set('currentTab', currentTab, { silent: true });
370
376
  }
371
377
  self.forEach((runner, idx) => {
372
378
  runner.set({
373
379
  index: idx,
374
- selected: idx === currentTab
380
+ selected: idx === currentTab,
375
381
  });
376
382
  });
377
383
  self.eraseLast();
@@ -380,9 +386,12 @@ exports.RunnerTabs = Backbone.Collection.extend({
380
386
  self.blankOutBackground();
381
387
  }
382
388
  });
383
- this.appview.on('change:isPopupVisible change:lines change:cols', function() {
384
- self.reRenderAll();
385
- });
389
+ this.appview.on(
390
+ 'change:isPopupVisible change:lines change:cols',
391
+ function () {
392
+ self.reRenderAll();
393
+ },
394
+ );
386
395
  },
387
396
 
388
397
  reRenderAll() {
@@ -397,9 +406,7 @@ exports.RunnerTabs = Backbone.Collection.extend({
397
406
  let screen = this.screen;
398
407
  let cols = this.appview.get('cols');
399
408
  for (let i = 0; i < TabHeight; i++) {
400
- screen
401
- .position(0, TabStartLine + i)
402
- .write(pad('', cols, ' ', 1));
409
+ screen.position(0, TabStartLine + i).write(pad('', cols, ' ', 1));
403
410
  }
404
411
  },
405
412
 
@@ -439,9 +446,7 @@ exports.RunnerTabs = Backbone.Collection.extend({
439
446
  let col = TabStartCol + index * width;
440
447
 
441
448
  for (let i = 0; i < height - 1; i++) {
442
- screen
443
- .position(col, line + i)
444
- .write(new Array(width + 1).join(' '));
449
+ screen.position(col, line + i).write(new Array(width + 1).join(' '));
445
450
  }
446
451
 
447
452
  let bottomLine = new Array(width + 1).join(Chars.horizontal);
@@ -452,5 +457,5 @@ exports.RunnerTabs = Backbone.Collection.extend({
452
457
  isPopupVisible() {
453
458
  let appview = this.appview;
454
459
  return appview && appview.isPopupVisible();
455
- }
460
+ },
456
461
  });
@@ -205,7 +205,6 @@ module.exports = View.extend({
205
205
  var runner = this.get('runner');
206
206
  var results = runner.get('results');
207
207
  var topLevelError = results ? results.get('topLevelError') : null;
208
- var tests = null;
209
208
  var out = '';
210
209
  var pendingOut = '';
211
210
 
@@ -213,6 +212,7 @@ module.exports = View.extend({
213
212
  out += 'Top Level:\n' + indent(topLevelError) + '\n\n';
214
213
  }
215
214
 
215
+ var tests;
216
216
  if (results && (tests = results.get('tests')) && !topLevelError) {
217
217
  var total = results.get('total');
218
218
  var pending = results.get('pending');
@@ -0,0 +1,5 @@
1
+ const notifier = require('toasted-notifier');
2
+
3
+ exports.notify = function notify(opts) {
4
+ return notifier.notify(opts);
5
+ };
@@ -40,7 +40,9 @@ module.exports = class BrowserTestRunner {
40
40
  } else {
41
41
  this.launcher.start().then(browserProcess => {
42
42
  this.process = browserProcess;
43
- this.process.on('processExit', this.onProcessExit.bind(this));
43
+ if (!this.launcher.settings.ignoreProcessExit) {
44
+ this.process.on('processExit', this.onProcessExit.bind(this));
45
+ }
44
46
  this.process.on('processError', this.onProcessError.bind(this));
45
47
  this.setupStartTimer();
46
48
  }).catch(err => {
@@ -55,7 +55,6 @@ module.exports = class HookRunner {
55
55
  } else if (exe) {
56
56
  args = this.varsub(args || []);
57
57
  hookProcessPromise = this.processCtl.spawn(exe, args, options);
58
- command = exe + ' ' + args.join(' ');
59
58
  } else {
60
59
  return Promise.reject(new Error('No command or exe/args specified for hook ' + hook));
61
60
  }
@@ -103,8 +103,10 @@ class Server extends EventEmitter {
103
103
  })
104
104
  );
105
105
 
106
+ // In Express 5 (path-to-regexp v8), wildcard params like /*file are captured
107
+ // as an array of path segments, so we rejoin them with '/' to reconstruct the path.
106
108
  var serveStaticFile = (req, res) => {
107
- this.serveStaticFile(req.params[0], req, res);
109
+ this.serveStaticFile('/' + req.params.file.join('/'), req, res);
108
110
  };
109
111
 
110
112
  if (this.config.get('key') || this.config.get('pfx')) {
@@ -142,16 +144,31 @@ class Server extends EventEmitter {
142
144
  res.redirect(`/${String(Math.floor(Math.random() * 10000))}`);
143
145
  });
144
146
 
145
- app.get(/\/(-?[0-9]+)$/, (req, res) => {
146
- this.serveHomePage(req, res);
147
- });
148
-
149
147
  app.get('/testem.js', (req, res) => {
150
148
  this.serveTestemClientJs(req, res);
151
149
  });
152
150
 
153
- app.all(/^\/(?:-?[0-9]+)(\/.+)$/, serveStaticFile);
154
- app.all(/^(.+)$/, serveStaticFile);
151
+ // Express 5 dropped regex-in-param syntax (e.g. /:id(-?\d+)), so we match
152
+ // any single-segment :id and validate numerically in the handler.
153
+ app.get('/:id', (req, res, next) => {
154
+ if (/^-?\d+$/.test(req.params.id)) {
155
+ this.serveHomePage(req, res);
156
+ } else {
157
+ next();
158
+ }
159
+ });
160
+
161
+ // Serve static files prefixed with a numeric browser id (e.g. /1234/web/test.js).
162
+ // The numeric guard prevents /:id from greedily swallowing non-numeric paths.
163
+ app.all('/:id/*file', (req, res, next) => {
164
+ if (/^-?\d+$/.test(req.params.id)) {
165
+ serveStaticFile(req, res);
166
+ } else {
167
+ next();
168
+ }
169
+ });
170
+ // Catch-all: serve any other path as a static file.
171
+ app.all('/*file', serveStaticFile);
155
172
 
156
173
  app.use((err, req, res, next) => {
157
174
  if (err) {
@@ -229,7 +246,13 @@ class Server extends EventEmitter {
229
246
  });
230
247
  return;
231
248
  }
232
- app.all(`${url}*`, (req, res, next) => {
249
+ // Express 5 no longer accepts bare wildcard segments (e.g. /api*). We
250
+ // replace each * in the user-supplied proxy URL with a named param, then
251
+ // append an optional catch-all for trailing path segments.
252
+ let wIdx = 0;
253
+ const proxyPath =
254
+ url.replace(/\*/g, () => `:_proxyW${wIdx++}`).replace(/\/$/, '') + '{/*_proxyRest}';
255
+ app.all(proxyPath, (req, res, next) => {
233
256
  var opts = proxies[url];
234
257
  if (this.shouldProxy(req, opts)) {
235
258
  if (opts.host) {
@@ -376,6 +399,12 @@ class Server extends EventEmitter {
376
399
  this.emit('file-requested', filePath);
377
400
  });
378
401
  } else {
402
+ // `dotfiles: 'allow'` restores the Express 4 (send@0.19) behavior of
403
+ // serving paths that contain intermediate dot-prefixed segments — common
404
+ // in pnpm projects, whose entire dependency tree lives under
405
+ // `node_modules/.pnpm/...`. send@1.x defaults to `'ignore'` and 404s any
406
+ // such path.
407
+ var sendOptions = { dotfiles: 'allow' };
379
408
  fs.promises.stat(filePath)
380
409
  .then(stat => {
381
410
  this.emit('file-requested', filePath);
@@ -385,11 +414,11 @@ class Server extends EventEmitter {
385
414
  res.render(dirListingPage, { files: files });
386
415
  });
387
416
  }
388
- res.sendFile(filePath);
417
+ res.sendFile(filePath, sendOptions);
389
418
  })
390
419
  .catch(() => {
391
420
  this.emit('file-requested', filePath);
392
- res.sendFile(filePath);
421
+ res.sendFile(filePath, sendOptions);
393
422
  });
394
423
  }
395
424
  }
@@ -3,40 +3,92 @@
3
3
  const yaml = require('js-yaml');
4
4
  const extend = Object.assign;
5
5
  const EventEmitter = require('events').EventEmitter;
6
- const TapParser = require('tap-parser');
6
+ const { Parser: TapParser } = require('tap-parser');
7
7
  const log = require('./log');
8
8
 
9
9
  module.exports = class TapConsumer extends EventEmitter {
10
10
  constructor() {
11
11
  super();
12
12
 
13
+ this.namePrefix = '';
14
+ this.prefixStack = [];
15
+ this.childParsers = [];
16
+
13
17
  this.stream = new TapParser();
14
- this.stream.on('assert', this.onTapAssert.bind(this));
15
- this.stream.on('extra', this.onTapExtra.bind(this));
18
+ this.attachParser(this.stream, true);
19
+ }
16
20
 
17
- this.stream.on('complete', this.onTapEnd.bind(this));
18
- this.stream.on('bailout', this.onTapError.bind(this));
21
+ attachParser(parser, isRoot) {
22
+ parser.on('assert', this.onTapAssert.bind(this));
23
+ parser.on('extra', this.onTapExtra.bind(this));
24
+ parser.on('child', this.onChildParser.bind(this));
25
+ parser.on('complete', this.onParserComplete.bind(this, parser));
26
+ if (isRoot) {
27
+ parser.on('bailout', this.onTapError.bind(this));
28
+ }
19
29
  }
20
30
 
21
- onTapAssert(data) {
22
- log.info(data);
23
- if (data.skip) {
31
+ onChildParser(childParser) {
32
+ this.prefixStack.push(this.namePrefix);
33
+ const parentPrefix = this.namePrefix;
34
+ const segment = (childParser.name && childParser.name.trim()) || '';
35
+ if (parentPrefix && segment) {
36
+ this.namePrefix = `${parentPrefix} > ${segment}`;
37
+ } else if (segment) {
38
+ this.namePrefix = segment;
39
+ } else if (parentPrefix) {
40
+ this.namePrefix = parentPrefix;
41
+ } else {
42
+ this.namePrefix = '';
43
+ }
44
+
45
+ this.childParsers.push(childParser);
46
+ this.attachParser(childParser, false);
47
+ }
48
+
49
+ onParserComplete(parser) {
50
+ if (parser === this.stream) {
51
+ this.onTapEnd();
24
52
  return;
25
53
  }
54
+ this.namePrefix = this.prefixStack.pop();
55
+ const idx = this.childParsers.indexOf(parser);
56
+ if (idx !== -1) {
57
+ this.childParsers.splice(idx, 1);
58
+ }
59
+ }
60
+
61
+ onTapAssert(data) {
62
+ log.verbose(JSON.stringify(data, null, 2));
26
63
 
27
64
  if (data.id === undefined) {
28
65
  return;
29
66
  }
30
67
 
68
+ const baseName = data.name ? data.name.trim() : '';
69
+ const qualifiedName = this.namePrefix
70
+ ? `${this.namePrefix} > ${baseName}`
71
+ : baseName;
72
+
31
73
  let test = {
32
74
  passed: 0,
33
75
  failed: 0,
34
76
  total: 1,
35
77
  id: data.id,
36
- name: data.name ? data.name.trim() : '',
78
+ name: qualifiedName,
37
79
  items: []
38
80
  };
39
81
 
82
+ if (data.skip) {
83
+ test.skipped = true;
84
+ this.emit('test-result', test);
85
+ return;
86
+ }
87
+
88
+ if (data.todo) {
89
+ test.todo = true;
90
+ }
91
+
40
92
  if (!data.ok) {
41
93
  let stack;
42
94
  if (data.diag) {
@@ -71,6 +123,16 @@ module.exports = class TapConsumer extends EventEmitter {
71
123
  }
72
124
  }
73
125
 
126
+ detachAllListeners() {
127
+ this.stream.removeAllListeners();
128
+ for (const p of this.childParsers) {
129
+ p.removeAllListeners();
130
+ }
131
+ this.childParsers = [];
132
+ this.prefixStack = [];
133
+ this.namePrefix = '';
134
+ }
135
+
74
136
  onTapError(reason) {
75
137
  let test = {
76
138
  failed: 1,
@@ -81,13 +143,13 @@ module.exports = class TapConsumer extends EventEmitter {
81
143
  }
82
144
  };
83
145
 
84
- this.stream.removeAllListeners();
146
+ this.detachAllListeners();
85
147
  this.emit('test-result', test);
86
148
  this.emit('all-test-results');
87
149
  }
88
150
 
89
151
  onTapEnd() {
90
- this.stream.removeAllListeners();
152
+ this.detachAllListeners();
91
153
  this.emit('all-test-results');
92
154
  }
93
155
  };
@@ -2,7 +2,7 @@
2
2
  * Shared esbuild config for bundling Node.js-dependent test code to run in a
3
3
  * browser via testem.
4
4
  *
5
- * esbuild's `@esbuild-plugins/node-modules-polyfill` stores the `process`
5
+ * `esbuild-plugins-node-modules-polyfill` stores the `process`
6
6
  * polyfill as a scoped variable inside the IIFE it emits, but CJS module
7
7
  * factories (wrapped in `__commonJS`) reference `process`, `__dirname`, and
8
8
  * `setImmediate` as free globals — causing ReferenceErrors at runtime.
@@ -1,5 +1,5 @@
1
1
  // esbuild inject shim: exposes Buffer as a global for browser bundles.
2
- // The @esbuild-plugins/node-modules-polyfill plugin resolves `require('buffer')`
2
+ // The esbuild-plugins-node-modules-polyfill plugin resolves `require('buffer')`
3
3
  // to a browser polyfill, but does not assign it to globalThis automatically.
4
4
  import { Buffer } from 'buffer';
5
5
  if (typeof globalThis !== 'undefined') { globalThis.Buffer = Buffer; }
@@ -0,0 +1,19 @@
1
+ const { glob } = require('glob');
2
+
3
+ /**
4
+ * Expand one resolved pattern to matching file paths using the project glob backend.
5
+ *
6
+ * Callers pass POSIX-style paths (see `convertToPosix`); this keeps options aligned
7
+ * with `glob` today and allows swapping the implementation later without touching Config.
8
+ *
9
+ * @param {string} resolvedPatternPosix - Single pattern (already resolved to absolute or cwd-relative form, forward slashes).
10
+ * @param {string[]} [ignorePatternsPosix] - Ignore globs in POSIX form; empty entries are dropped.
11
+ * @returns {Promise<string[]>} Matching paths, sorted lexicographically (same ordering as before extraction).
12
+ */
13
+ async function expandGlobPattern(resolvedPatternPosix, ignorePatternsPosix) {
14
+ const ignore = (ignorePatternsPosix || []).filter(Boolean);
15
+ const files = await glob(resolvedPatternPosix, { ignore });
16
+ return files.slice().sort();
17
+ }
18
+
19
+ module.exports = expandGlobPattern;
@@ -0,0 +1,119 @@
1
+ const url = require('url');
2
+
3
+ const expandGlobPattern = require('./expand_glob_pattern');
4
+ const { convertToPosix } = require('./posix');
5
+ const { pathMatchesAny } = require('./path_pattern_match');
6
+
7
+ /**
8
+ * Normalize config `src_files`-style values to a list of glob strings.
9
+ * @param {string|string[]|Array<{src: string}>|undefined|null} value
10
+ * @returns {string[]}
11
+ */
12
+ function flattenPatternList(value) {
13
+ if (value === undefined || value === null) {
14
+ return [];
15
+ }
16
+ if (Array.isArray(value)) {
17
+ return value.map(function(entry) {
18
+ if (typeof entry === 'string') {
19
+ return entry;
20
+ }
21
+ if (entry && typeof entry.src === 'string') {
22
+ return entry.src;
23
+ }
24
+ return String(entry);
25
+ });
26
+ }
27
+ return [String(value)];
28
+ }
29
+
30
+ /**
31
+ * Build the include / ignore pattern lists used by `FileWatcher` with the watch engine
32
+ * (policy filtering; the engine watches cwd and applies these patterns). Single place for glob +
33
+ * ignore semantics.
34
+ *
35
+ * @param {{ get: function(string): *, isCwdMode: function(): boolean }} config
36
+ * @returns {{ includePatterns: string[], ignorePatterns: string[] }}
37
+ */
38
+ function buildWatchGlobPolicy(config) {
39
+ const includePatterns = [];
40
+
41
+ const confFile = config.get('file');
42
+ if (confFile) {
43
+ includePatterns.push(String(confFile));
44
+ }
45
+ if (config.isCwdMode()) {
46
+ includePatterns.push('*.js');
47
+ }
48
+ const watchFiles = config.get('watch_files');
49
+ if (watchFiles) {
50
+ includePatterns.push(...flattenPatternList(watchFiles));
51
+ }
52
+ const srcFiles = config.get('src_files');
53
+ const srcList = srcFiles !== undefined && srcFiles !== null ? srcFiles : '*.js';
54
+ includePatterns.push(...flattenPatternList(srcList));
55
+
56
+ const ignoreRaw = config.get('src_files_ignore');
57
+ const ignorePatterns = ignoreRaw ? flattenPatternList(ignoreRaw) : [];
58
+
59
+ return { includePatterns, ignorePatterns };
60
+ }
61
+
62
+ /**
63
+ * Whether a path would be treated as a watched file (matches an include pattern and
64
+ * no ignore pattern). Paths are normalized with {@link convertToPosix} for matching
65
+ * on Windows vs POSIX.
66
+ *
67
+ * @param {string} filePath - Absolute or relative path as emitted by the filesystem / watcher.
68
+ * @param {{ includePatterns: string[], ignorePatterns: string[] }} policy
69
+ * @param {object} [matchOptions] - forwarded to minimatch (via path_pattern_match).
70
+ * @returns {boolean}
71
+ */
72
+ function pathMatchesWatchTarget(filePath, policy, matchOptions) {
73
+ const posix = convertToPosix(filePath);
74
+ if (pathMatchesAny(posix, policy.ignorePatterns, matchOptions)) {
75
+ return false;
76
+ }
77
+ return pathMatchesAny(posix, policy.includePatterns, matchOptions);
78
+ }
79
+
80
+ /**
81
+ * Expand all on-disk files matching the watch policy (same idea as `getFileSet` for
82
+ * `src_files`, but driven by the watch policy). Skips `http:` / `https:` URLs; keeps
83
+ * them as literal entries. Uses {@link expandGlobPattern} with `config.resolvePath`.
84
+ *
85
+ * @param {{ get: function(string): *, cwd: function(): string, resolvePath: function(string): string }} config
86
+ * @returns {Promise<string[]>} Sorted unique absolute paths (and any literal URLs).
87
+ */
88
+ async function expandWatchableFilePaths(config) {
89
+ const policy = buildWatchGlobPolicy(config);
90
+ const ignorePosix = policy.ignorePatterns.map(function(p) {
91
+ return convertToPosix(config.resolvePath(p));
92
+ });
93
+ const out = [];
94
+
95
+ for (let i = 0; i < policy.includePatterns.length; i++) {
96
+ const pattern = policy.includePatterns[i];
97
+ const parsed = url.parse(pattern);
98
+ if (parsed.protocol && parsed.protocol !== 'file:' && parsed.protocol !== '') {
99
+ out.push(pattern);
100
+ continue;
101
+ }
102
+ let fsPattern = pattern;
103
+ if (parsed.protocol === 'file:') {
104
+ fsPattern = parsed.hostname + parsed.path;
105
+ }
106
+ const resolved = convertToPosix(config.resolvePath(fsPattern));
107
+ const files = await expandGlobPattern(resolved, ignorePosix);
108
+ out.push(...files);
109
+ }
110
+
111
+ return [...new Set(out)].sort();
112
+ }
113
+
114
+ module.exports = {
115
+ buildWatchGlobPolicy,
116
+ flattenPatternList,
117
+ pathMatchesWatchTarget,
118
+ expandWatchableFilePaths,
119
+ };