All files / bin/watch watchingCliEngine.ts

98.28% Statements 57/58
50% Branches 5/10
100% Functions 12/12
98.25% Lines 56/57

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 1601x 1x 1x 1x 1x         1x           1x 1x 1x 1x       1x     1x 1x         1x     1x 3x       1x 1x 1x 1x     1x             2x     2x   2x         1x   1x         1x               1x     1x     1x   1x   1x     1x         1x       1x 1x 1x     1x 1x     1x     1x         1x 1x             1x     1x   1x     1x 1x       1x 1x 1x       1x       1x         2x 4x       2x    
import {CliEngine} from '../cliEngine';
import {DependencyTracker} from './dependencyTracker';
import { Mooltipage } from '../../lib';
import {PipelineIOImpl} from '../../lib/pipeline/standardPipeline';
import {
    TrackingCache,
    TrackingPipelineIO
} from './trackers';
import {FSWatcher} from 'chokidar';
import {
    createReadyFSWatcher,
    onAny,
    setWatched
} from './FSWatcherUtils';
 
export class WatchingCliEngine extends CliEngine {
    readonly dependencyTracker = new DependencyTracker();
    readonly stagedChanges = new Set<string>();
    private isWatching = false;
 
    async runApp(): Promise<void> {
        // Run initial compilation
        await super.runApp();
 
        // enter watch mode
        await this.enterWatchMode();
        this.cliConsole.log('Entering watch mode - modified files will be rebuilt automatically.');
    }
 
    protected createMooltipage(): Mooltipage {
        // initialize tracking data
        const currentDependencies = new Set<string>();
 
        // create shared callback to collect dependencies from all shared trackers
        const trackerCallback = function(resPath: string): void {
            currentDependencies.add(resPath);
        };
 
        // we are in watch mode, so create a tracking pipeline IO
        const inPath = this.args.inPath ?? process.cwd();
        const outPath = this.args.outPath ?? process.cwd();
        const realIO = new PipelineIOImpl(inPath, outPath);
        const trackingIO = new TrackingPipelineIO(realIO, trackerCallback);
 
        // create mooltipage instance
        const mooltipage = new Mooltipage({
            inPath: this.args.inPath,
            outPath: this.args.outPath,
            pipelineIO: trackingIO,
            formatter: this.args.formatter,
            onPageCompiled: async page => {
                // update dependencies for page
                this.dependencyTracker.setPageDependencies(page.path, currentDependencies);
 
                // reset tracker
                currentDependencies.clear();
 
                this.cliConsole.log(`Compiled [${ page.path }].`);
            }
        });
 
        // inject tracking cache
        mooltipage.pipeline.cache.fragmentCache = new TrackingCache(mooltipage.pipeline.cache.fragmentCache, trackerCallback);
        
        return mooltipage;
    }
 
    private async enterWatchMode(): Promise<void> {
        // create file watcher
        const fileWatcher = await createReadyFSWatcher({
            disableGlobbing: true,
            persistent: true,
            ignoreInitial: true,
            useFsEvents: true
        });
 
        // create timer to apply changes
        const applyChangesTimer = setTimeout(() => this.checkAndApplyStagedChanges(fileWatcher), 300);
 
        // watch files
        this.watchCurrentFiles(fileWatcher);
 
        // Define callback for FS events
        onAny(fileWatcher, [ 'change', 'unlink', 'unlinkDir' ], (_, changedFile) => {
            // Only respond to events if we are actually in watch mode
            Eif (this.isWatching) {
                // add to staged changes
                this.stagedChanges.add(changedFile);
 
                // start timer on changes
                applyChangesTimer.refresh();
            }
        });
 
        // enable
        this.isWatching = true;
    }
    
    private async checkAndApplyStagedChanges(fileWatcher: FSWatcher): Promise<void> {
        Eif (this.stagedChanges.size > 0) {
            try {
                this.isWatching = false;
 
                // get list of files to recompile and clear
                const filesToRecompile = new Set(this.stagedChanges);
                this.stagedChanges.clear();
 
                // compile changes
                await this.compileChangeSet(filesToRecompile);
 
                // reset watched files
                this.watchCurrentFiles(fileWatcher);
            } catch (e) {
                this.cliConsole.error(e);
 
            } finally {
                this.stagedChanges.clear();
                this.isWatching = true;
            }
        }
    }
 
    private async compileChangeSet(changeSet: Set<string>): Promise<void> {
        // list of pages with changes
        const changedPages = new Set<string>();
 
        // find pages impacted by each changed file
        for (const stagedChange of changeSet) {
            // convert raw path back to res path
            const stagedChangePath = this.mooltipage.pipeline.pipelineIO.getSourceResPathForAbsolutePath(stagedChange);
 
            // if this is a page, then add directly
            Eif (this.dependencyTracker.hasPage(stagedChangePath)) {
                changedPages.add(stagedChangePath);
            }
 
            // find and add all pages that depend on this
            const dependentPages = this.dependencyTracker.getDependentsForResource(stagedChangePath);
            for (const dependentPage of dependentPages) {
                changedPages.add(dependentPage);
            }
 
            // clear cache for staged changes (including resources, not just pages)
            this.mooltipage.pipeline.cache.fragmentCache.remove(stagedChangePath);
        }
 
        // compile changes
        await this.mooltipage.processPages(changedPages);
    }
 
    private watchCurrentFiles(fileWatcher: FSWatcher): void {
        // get list of absolute paths to all files
        const allFiles = Array.from(new Set<string>(Array.from(this.dependencyTracker.getAllTrackedFiles())
            .map(trackedFile => this.mooltipage.pipeline.pipelineIO.resolveSourceResource(trackedFile))
        ));
 
        // watch all files
        setWatched(fileWatcher, allFiles);
    }
}