UNPKG

@goto-bus-stop/common-shake

Version:
213 lines (176 loc) 5.99 kB
'use strict'; const debug = require('debug')('common-shake:module'); function Module(resource) { this.resource = resource; this.sideEffects = true; this.bailouts = false; this.issuers = new Set(); this.uses = new Map(); this.declarations = []; this.requireNodes = new Map(); this.dependentUses = []; this.pendingUses = []; this.computing = false; this.forced = false; } module.exports = Module; // Public API Module.prototype.addRequire = function addRequire(name, node) { if (!this.requireNodes.has(name)) { this.requireNodes.set(name, []); } this.requireNodes.get(name).push(node); }; Module.prototype.forceExport = function forceExport() { this.forced = true; }; Module.prototype.isUsed = function isUsed(name, dependentStack) { this.compute(); if (this.bailouts || this.forced) return true; // Detect loops if (this.computing) { const pending = this.pendingUses.some(use => use.property === name); if (pending) return true; const dependent = this.dependentUses.find(use => use.property === name); // Make sure we don't hang on recursive dependencies, `exports.a` // calling `exports.b` and vice versa. if (dependent && (dependentStack === undefined || dependentStack.indexOf(dependent.property) === -1)) { if (dependentStack === undefined) { dependentStack = [dependent.dependency]; } else { dependentStack.push(dependent.dependency); } const result = dependent.from.isUsed(dependent.dependency, dependentStack); dependentStack.pop(); return result; } } return this.uses.has(name); }; Module.prototype.getInfo = function getInfo() { this.compute(); return { removeImport: !this.sideEffects && this.uses.size === 0, bailouts: this.bailouts, declarations: this.declarations.map(decl => decl.name), uses: Array.from(this.uses.keys()) }; }; Module.prototype.getDeclarations = function getDeclarations() { return this.declarations.slice(); }; // Private API Module.prototype.bailout = function bailout(reason, loc, source, level) { const bail = { reason, loc, source: source || null, level: level || 'warning' }; if (this.bailouts) this.bailouts.push(bail); else this.bailouts = [ bail ]; this.sealed = false; }; Module.prototype.use = function use(property, from, recursive, dependency) { if (dependency !== undefined) { debug('dependent use this=%j property=%j from=%j dependency=%j', this.resource, property, from.resource, dependency); this.dependentUses.push({ property, from, dependency }); return; } if (recursive !== false) { debug('pending use this=%j property=%j from=%j recursive=%j', this.resource, property, from.resource, recursive); this.pendingUses.push({ property, from, recursive }); return; } debug('use this=%j property=%j from=%j recursive=%j', this.resource, property, from.resource, recursive); if (this.uses.has(property)) this.uses.get(property).add(from); else this.uses.set(property, new Set([ from ])); }; Module.prototype.seal = function seal() { this.sealed = true; }; Module.prototype.declare = function declare(property) { this.declarations.push(property); return !this.sealed; }; Module.prototype.multiDeclare = function multiDeclare(declarations) { const success = this.declarations.length === 0 && !this.sealed; this.seal(); for (let i = 0; i < declarations.length; i++) this.declarations.push(declarations[i]); return success; }; Module.prototype.mergeFrom = function mergeFrom(unresolved) { if (unresolved.bailouts) { unresolved.bailouts.forEach((b) => { this.bailout(b.reason, b.loc, b.source, b.level); }); } unresolved.uses.forEach((from, property) => { from.forEach(resource => this.use(property, resource, false)); }); unresolved.declarations.forEach(declaration => this.declare(declaration)); this.pendingUses = this.pendingUses.concat(unresolved.pendingUses); this.dependentUses = this.dependentUses.concat(unresolved.dependentUses); unresolved.clear(); }; Module.prototype.addIssuer = function addIssuer(issuer) { this.issuers.add(issuer); }; Module.prototype.clear = function clear() { this.uses = null; this.declarations = null; this.pendingUses = null; this.dependentUses = null; }; Module.prototype.compute = function compute() { // Already computed or cleared if (this.pendingUses === null) return; if (this.computing) return; this.computing = true; debug('compute this=%j pending=%d dependent=%d', this.resource, this.pendingUses.length, this.dependentUses.length); // Do several passes until it will stabilize // TODO(indutny): what is complexity of this? Exponential? let before; do { before = this.pendingUses.length; // NOTE: it is important to overwrite this, since recursive lookups will // get to it. this.pendingUses = this.pendingUses.filter((use) => { return use.from.isUsed(use.recursive); }); debug('compute pass this=%j before=%d after=%d', this.resource, before, this.pendingUses.length); } while (this.pendingUses.length !== before); do { before = this.dependentUses.length; this.dependentUses = this.dependentUses.filter((use) => { return use.from.isUsed(use.dependency); }); debug('compute pass (dependent) this=%j before=%d after=%d', this.resource, before, this.dependentUses.length); } while (this.dependentUses.length !== before); this.pendingUses.forEach(use => this.use(use.property, use.from, false)); this.dependentUses.forEach(use => { debug('dependent mark used from=%j property=%j', use.from.resource, use.property); this.use(use.property, use.from, false); }); this.pendingUses = null; this.dependentUses = null; this.computing = false; };