11"use strict" ;
22
3- const { clearTimeout, setTimeout } = global ;
4-
53const debug = require ( "debug" ) ( "mocha:cli:watch" ) ;
64const path = require ( "node:path" ) ;
75const chokidar = require ( "chokidar" ) ;
@@ -10,7 +8,6 @@ const isPathInside = require("is-path-inside");
108const { minimatch } = require ( "minimatch" ) ;
119const Context = require ( "../context.mjs" ) . Context ;
1210const collectFiles = require ( "./collect-files" ) ;
13- const runHelpers = require ( "./run-helpers" ) ;
1411const { logSymbols } = require ( "../utils" ) ;
1512
1613/**
@@ -57,7 +54,7 @@ exports.watchParallelRun = (
5754 return createWatcher ( mocha , {
5855 watchFiles,
5956 watchIgnore,
60- async beforeRun ( { mocha } ) {
57+ beforeRun ( { mocha } ) {
6158 // I don't know why we're cloning the root suite.
6259 const rootSuite = mocha . suite . clone ( ) ;
6360
@@ -69,12 +66,9 @@ exports.watchParallelRun = (
6966 // test depends on this module.
7067 const Mocha = require ( "../mocha" ) ;
7168
72- // reload rootHooks and other plugins, in case those files have changed
73- const plugins = await runHelpers . handleRequires ( mocha . options . require ) ;
74-
7569 // ... and now that we've gotten a new module, we need to use it again due
7670 // to `mocha.ui()` call
77- const newMocha = new Mocha ( Object . assign ( { } , mocha . options , plugins ) ) ;
71+ const newMocha = new Mocha ( mocha . options ) ;
7872 // don't know why this is needed
7973 newMocha . suite = rootSuite ;
8074 // nor this
@@ -120,7 +114,9 @@ exports.watchRun = (mocha, { watchFiles, watchIgnore }, fileCollectParams) => {
120114 return createWatcher ( mocha , {
121115 watchFiles,
122116 watchIgnore,
123- async beforeRun ( { mocha } ) {
117+ beforeRun ( { mocha } ) {
118+ mocha . unloadFiles ( ) ;
119+
124120 // I don't know why we're cloning the root suite.
125121 const rootSuite = mocha . suite . clone ( ) ;
126122
@@ -132,17 +128,9 @@ exports.watchRun = (mocha, { watchFiles, watchIgnore }, fileCollectParams) => {
132128 // test depends on this module.
133129 const Mocha = require ( "../mocha" ) ;
134130
135- // reload rootHooks and other plugins, in case those files have changed
136- const plugins = await runHelpers . handleRequires ( mocha . options . require , {
137- // disable requireOrImport, since if the module gets loaded via ESM
138- // the cache won't get busted between runs.
139- // watch serial mode doesn't currently support ESM anyway.
140- forceRequire : true ,
141- } ) ;
142-
143131 // ... and now that we've gotten a new module, we need to use it again due
144132 // to `mocha.ui()` call
145- const newMocha = new Mocha ( { ... mocha . options , ... plugins } ) ;
133+ const newMocha = new Mocha ( mocha . options ) ;
146134 // don't know why this is needed
147135 newMocha . suite = rootSuite ;
148136 // nor this
@@ -397,19 +385,6 @@ function createPathMatcher(allowed, ignored, basePath) {
397385 return matcher ;
398386}
399387
400- const TEST_MODE = Boolean ( process . env . __MOCHA_WATCH_MOCK_CHOKIDAR ) ;
401-
402- // grab Date.now before anything like sinon.createFakeTimers can monkeypatch it
403- // eslint-disable-next-line no-restricted-globals
404- const dateNow = Date . now ;
405- if ( TEST_MODE ) debug ( "in test mode" ) ;
406-
407- const sendMessage = ( msg ) => {
408- if ( ! TEST_MODE ) return ;
409- process . send ( msg ) ;
410- debug ( "sent message" , msg ) ;
411- } ;
412-
413388/**
414389 * Bootstraps a Chokidar watcher. Handles keyboard input & signals
415390 * @param {Mocha } mocha - Mocha instance
@@ -454,70 +429,29 @@ const createWatcher = (
454429 const matcher = createPathMatcher ( allowed , ignored , basePath ) ;
455430
456431 // Chokidar has to watch the directory paths in case new files are created
457- let watcher ;
458- /* istanbul ignore next */
459- if ( TEST_MODE ) {
460- const EventEmitter = require ( "node:events" ) ;
461- watcher = new EventEmitter ( ) ;
462- process . on ( "message" , ( msg ) => {
463- debug ( "got message" , msg ) ;
464- if ( msg . watcher ) watcher . emit ( ...msg . watcher ) ;
465- } ) ;
466- } else {
467- watcher = chokidar . watch ( Array . from ( allowed . dir . paths ) , {
468- ignoreInitial : true ,
469- ignored : matcher . ignore ,
470- alwaysStat : true ,
471- } ) ;
472- }
473-
474- // if files change during global setup, we need to blast the cache before the first
475- // run in case any tests depend on changed modules that were loaded by global setup
476- let blastCacheBeforeNextRun = false ;
477- const rerunner = createRerunner ( mocha , watcher , matcher , {
478- beforeRun : async ( opts ) => {
479- if ( blastCacheBeforeNextRun ) {
480- blastCacheBeforeNextRun = false ;
481- blastCache ( matcher ) ;
482- }
483- return await beforeRun ( opts ) ;
484- } ,
432+ const watcher = chokidar . watch ( Array . from ( allowed . dir . paths ) , {
433+ ignoreInitial : true ,
434+ ignored : matcher . ignore ,
485435 } ) ;
486436
487- const startTime = dateNow ( ) ;
488- debug ( "start time: %d" , startTime ) ;
437+ const rerunner = createRerunner ( mocha , watcher , {
438+ beforeRun,
439+ } ) ;
489440
490- watcher . on ( "all" , function handleEvent ( ...args ) {
491- const [ event , filePath , stat ] = args ;
492- /* istanbul ignore next */
493- if ( TEST_MODE ) sendMessage ( { received : [ "all" , ...args ] } ) ;
494- if ( exiting ) return ;
495- if ( ! matcher . allow ( filePath ) ) {
496- debug ( "event doesn't match filter: %s, %s" , event , filePath ) ;
497- return ;
498- }
499- if (
500- event === "unlink" ||
501- // birthtime can be > mtime on some filesystems; also check ctime just in case
502- ( stat && Math . max ( stat . mtime , stat . ctime , stat . birthtime ) > startTime )
503- ) {
504- // we don't want to accidentally trigger a run before globalFixtureContext
505- // has been created. If it hasn't then it's okay to do nothing here because
506- // the code that creates globalFixtureContext will run the tests afterward
507- if ( globalFixtureContext ) rerunner . scheduleRun ( ) ;
508- // if files change during global setup, we need to blast the cache before the first
509- // run in case any tests depend on changed modules that were loaded by global setup
510- else blastCacheBeforeNextRun = true ;
441+ watcher . on ( "ready" , async ( ) => {
442+ debug ( "watcher ready" ) ;
443+ if ( ! globalFixtureContext ) {
444+ debug ( "triggering global setup" ) ;
445+ globalFixtureContext = await mocha . runGlobalSetup ( ) ;
511446 }
447+ rerunner . run ( ) ;
512448 } ) ;
513449
514- /* istanbul ignore next */
515- if ( TEST_MODE ) sendMessage ( { listening : true , startTime } ) ;
516-
517- debug ( "triggering global setup" ) ;
518- mocha . runGlobalSetup ( ) . then ( ( context ) => {
519- globalFixtureContext = context ;
520- return rerunner . run ( ) ;
450+ watcher . on ( "all" , ( _event , filePath ) => {
451+ // only allow file paths that match the allowed patterns
452+ if ( matcher . allow ( filePath ) ) {
453+ rerunner . scheduleRun ( ) ;
454+ }
521455 } ) ;
522456
523457 hideCursor ( ) ;
@@ -575,71 +509,51 @@ const createWatcher = (
575509 *
576510 * @param {Mocha } mocha - Mocha instance
577511 * @param {FSWatcher } watcher - Chokidar `FSWatcher` instance
578- * @param {PathMatcher } matcher - `PathMatcher` instance
579- * @param {Object } opts - Options!
580- * @param {BeforeWatchRun } opts.beforeRun - Function to call before `mocha.run()`. Must be provided.
512+ * @param {Object } [opts] - Options!
513+ * @param {BeforeWatchRun } [opts.beforeRun] - Function to call before `mocha.run()`
581514 * @returns {Rerunner }
582515 * @ignore
583516 * @private
584517 */
585- const createRerunner = ( mocha , watcher , matcher , { beforeRun } ) => {
586- // Set to `Runner.abort ` when mocha is running. Set to another abort function
587- // during `beforeRun()`. Set to `null` otherwise .
588- let abortRun = null ;
518+ const createRerunner = ( mocha , watcher , { beforeRun } = { } ) => {
519+ // Set to a `Runner` when mocha is running. Set to `null` when mocha is not
520+ // running .
521+ let runner = null ;
589522
590523 // true if a file has changed during a test run
591524 let rerunScheduled = false ;
592525
593- const run = async ( ) => {
526+ const run = ( ) => {
594527 try {
595- if ( TEST_MODE ) sendMessage ( { run : true } ) ;
596- let aborted = false ;
597- abortRun = ( ) => ( aborted = true ) ;
598- mocha = ( await beforeRun ( { mocha, watcher } ) ) || mocha ;
599- if ( aborted ) {
600- abortRun = null ;
601- blastCache ( matcher ) ;
602- rerun ( ) ;
603- }
604- const runner = mocha . run ( ( ) => {
528+ mocha = beforeRun ? beforeRun ( { mocha, watcher } ) || mocha : mocha ;
529+ runner = mocha . run ( ( ) => {
605530 debug ( "finished watch run" ) ;
606- if ( TEST_MODE ) sendMessage ( { runFinished : true } ) ;
607- abortRun = null ;
608- blastCache ( matcher ) ;
531+ runner = null ;
532+ blastCache ( watcher ) ;
609533 if ( rerunScheduled ) {
610534 rerun ( ) ;
611535 } else {
612536 console . error ( `${ logSymbols . info } [mocha] waiting for changes...` ) ;
613537 }
614538 } ) ;
615- abortRun = ( ) => runner . abort ( ) ;
616539 } catch ( err ) {
617- abortRun = null ;
618540 console . error ( err . stack ) ;
619541 }
620542 } ;
621543
622544 const scheduleRun = ( ) => {
623- if ( TEST_MODE ) sendMessage ( { scheduleRun : true } ) ;
624545 if ( rerunScheduled ) {
625546 return ;
626547 }
627548
628549 rerunScheduled = true ;
629- if ( abortRun ) {
630- abortRun ( ) ;
550+ if ( runner ) {
551+ runner . abort ( ) ;
631552 } else {
632- debounceRerun ( ) ;
553+ rerun ( ) ;
633554 }
634555 } ;
635556
636- let rerunTimeout ;
637-
638- const debounceRerun = ( ) => {
639- clearTimeout ( rerunTimeout ) ;
640- rerunTimeout = setTimeout ( rerun , 10 ) ;
641- } ;
642-
643557 const rerun = ( ) => {
644558 rerunScheduled = false ;
645559 eraseLine ( ) ;
@@ -652,6 +566,25 @@ const createRerunner = (mocha, watcher, matcher, { beforeRun }) => {
652566 } ;
653567} ;
654568
569+ /**
570+ * Return the list of absolute paths watched by a Chokidar watcher.
571+ *
572+ * @param watcher - Instance of a Chokidar watcher
573+ * @return {string[] } - List of absolute paths
574+ * @ignore
575+ * @private
576+ */
577+ const getWatchedFiles = ( watcher ) => {
578+ const watchedDirs = watcher . getWatched ( ) ;
579+ return Object . keys ( watchedDirs ) . reduce (
580+ ( acc , dir ) => [
581+ ...acc ,
582+ ...watchedDirs [ dir ] . map ( ( file ) => path . join ( dir , file ) ) ,
583+ ] ,
584+ [ ] ,
585+ ) ;
586+ } ;
587+
655588/**
656589 * Hide the cursor.
657590 * @ignore
@@ -680,17 +613,14 @@ const eraseLine = () => {
680613
681614/**
682615 * Blast all of the watched files out of `require.cache`
683- * @param {PathMatcher } matcher - only delete keys allowed by this matcher
616+ * @param {FSWatcher } watcher - Chokidar FSWatcher
684617 * @ignore
685618 * @private
686619 */
687- const blastCache = ( matcher ) => {
688- let deletedCount = 0 ;
689- for ( const key in require . cache ) {
690- if ( matcher . allow ( key ) ) {
691- deletedCount ++ ;
692- delete require . cache [ key ] ;
693- }
694- }
695- debug ( "deleted %d file(s) from the require cache" , deletedCount ) ;
620+ const blastCache = ( watcher ) => {
621+ const files = getWatchedFiles ( watcher ) ;
622+ files . forEach ( ( file ) => {
623+ delete require . cache [ file ] ;
624+ } ) ;
625+ debug ( "deleted %d file(s) from the require cache" , files . length ) ;
696626} ;
0 commit comments