Fitbit/webpack-cluster

View on GitHub
src/CompilerAdapter.js

Summary

Maintainability
A
25 mins
Test Coverage
import {
    isBoolean,
    isError
} from 'lodash';
import PromisePool from './PromisePool';
import ForkPromise from './ForkPromise';
import CompilerResult from './CompilerResult';
import DEFAULT_OPTIONS from './CompilerAdapterOptions';
import {
    PROCESS_SIGINT
} from './Events';
import {
    findFiles,
    watchFiles
} from './FsUtil';
import {
    invalidError
} from './CompilerErrorFactory';

/**
 * @private
 * @type {WeakMap}
 */
const OPTIONS = new WeakMap();

/**
 * @private
 * @type {WeakMap}
 */
const POOL = new WeakMap();

/**
 * @private
 * @type {WeakMap}
 */
const WATCHERS = new WeakMap();

/**
 * @callback CompilerAdapterCallback
 * @returns {void}
 */
const DEFAULT_CALLBACK = () => {};

/**
 * @private
 * @type {String}
 */
const WORKER_PATH = require.resolve('./CompilerWorker');

/**
 * @class
 */
class CompilerAdapter {
    /**
     * @constructor
     * @param {CompilerAdapterOptions} [options]
     */
    constructor(options = {}) {
        OPTIONS.set(this, Object.assign({}, DEFAULT_OPTIONS, options));

        /* istanbul ignore next */
        process.on(PROCESS_SIGINT, () => this.closeAll());
    }

    /**
     * @readonly
     * @type {CompilerAdapterOptions}
     */
    get options() {
        return OPTIONS.get(this);
    }

    /**
     * @readonly
     * @private
     * @type {CompilerFailureOptions}
     */
    get failures() {
        let options = this.options.failures;

        if (isBoolean(options)) {
            options = {
                sysErrors: options,
                warnings: options,
                errors: options
            };
        }

        const { sysErrors, warnings, errors } = options;

        return {
            sysErrors,
            warnings,
            errors
        };
    }

    /**
     * @private
     * @readonly
     * @type {PromisePool}
     */
    get pool() {
        if (!POOL.has(this)) {
            POOL.set(this, new PromisePool(this.options.concurrency));
        }

        return POOL.get(this);
    }

    /**
     * @private
     * @readonly
     * @type {Set<FSWatcher>}
     */
    get watchers() {
        if (!WATCHERS.has(this)) {
            WATCHERS.set(this, new Set([]));
        }

        return WATCHERS.get(this);
    }

    /**
     * @internal
     * @returns {Promise}
     */
    closeAll() {
        return this.closePool()
            .then(() => this.closeWatchers())
            .then(() => CompilerAdapter.closeMaster());
    }

    /**
     * @private
     * @returns {Promise}
     */
    closePool() {
        return this.pool.closeAll().then(() => {
            this.pool.clear();
        });
    }

    /**
     * @private
     * @returns {Promise}
     */
    closeWatchers() {
        const promises = Array.from(this.watchers.values()).map(watcher => {
            watcher.close();

            return Promise.resolve();
        });

        return Promise.all(promises).then(() => {
            this.watchers.clear();
        });
    }

    /**
     * @private
     * @param {Object} options
     * @param {CompilerAdapterCallback} [callback]
     * @returns {Promise}
     */
    fork(options, callback) {
        return ForkPromise.fork(Object.assign({}, this.options, options), callback);
    }

    /**
     * @private
     * @param {Object} data
     * @returns {Promise<String|Error>}
     */
    done(data) {
        const result = CompilerResult.from(data),
            failures = this.failures,
            hasError = (failures.sysErrors && result.hasSysErrors) ||
                (failures.errors && result.hasErrors) ||
                (failures.warnings && result.hasWarnings);

        return Promise.resolve(hasError ? invalidError(result.filename) : result.filename);
    }

    /**
     * Builds the bundles
     * @param {String[]} patterns
     * @param {CompilerAdapterCallback} [callback]
     * @returns {Promise<String[]|Error[]>}
     */
    run(patterns, callback = DEFAULT_CALLBACK) {
        return CompilerAdapter.setupMaster().then(() => {
            return findFiles(patterns).then(files => {
                files.forEach(filename => {
                    this.pool.set(filename, () => this.fork({
                        filename,
                        watch: false
                    }, callback));
                });

                return this.pool.waitAll().then(results => {
                    return Promise.all(results.map(result => this.done(result)));
                });
            });
        }).then(results => this.closeAll().then(() => {
            const errors = results.filter(isError);

            return errors.length > 0 ? Promise.reject(errors) : Promise.resolve(results);
        }));
    }

    /**
     * Builds the bundles then starts the watcher, which rebuilds bundles whenever their source files change
     * @param {String[]} patterns
     * @param {CompilerAdapterCallback} [callback]
     * @returns {Promise}
     */
    watch(patterns, callback = DEFAULT_CALLBACK) {
        return CompilerAdapter.setupMaster().then(() => {
            return watchFiles(patterns, filename => {
                Promise.resolve().then(() => {
                    return this.pool.has(filename) ? this.pool.stop(filename) : Promise.resolve();
                }).then(() => {
                    this.pool.set(filename, () => this.fork({
                        filename,
                        watch: true
                    }, callback));

                    return this.pool.start(filename);
                });
            }).then(watchers => {
                watchers.forEach(watcher => this.watchers.add(watcher));

                return Promise.resolve([]);
            });
        });
    }

    /**
     * @private
     * @static
     * @returns {Promise}
     */
    static setupMaster() {
        return ForkPromise.setupMaster({
            exec: WORKER_PATH
        });
    }

    /**
     * @private
     * @static
     * @returns {Promise}
     */
    static closeMaster() {
        return ForkPromise.closeMaster();
    }
}

export default CompilerAdapter;