UNPKG

fuse-box

Version:

Fuse-Box a bundler that does it right

127 lines (117 loc) 5.52 kB
import { BundleAbstraction } from "../core/BundleAbstraction"; import { each } from "realm-utils"; import { PackageAbstraction } from "../core/PackageAbstraction"; import { FileAbstraction } from "../core/FileAbstraction"; import { QuantumCore } from "./QuantumCore"; import { RequireStatement } from "../core/nodes/RequireStatement"; export class TreeShake { constructor(public core: QuantumCore) { } /** * Initiate tree shaking */ public shake(): Promise<any> { return this.eachFile(file => this.shakeExports(file)) .then(() => this.releaseReferences()) .then(() => this.removeUnusedExports()); } private releaseReferences() { return this.eachFile(file => { if (file.isNotUsedAnywhere() && this.core.opts.canBeRemovedByTreeShaking(file)) { return this.eachFile(target => target.releaseDependent(file)); } }); } /** * Remove exports if allowed and expose dead code to uglifyjs */ private removeUnusedExports() { return this.eachFile(file => { let uknownStatements = new Set<RequireStatement>() file.namedExports.forEach(fileExport => { if (!fileExport.isUsed && file.isTreeShakingAllowed() && fileExport.eligibleForTreeShaking) { const isDangerous = fileExport.name === "__esModule" || fileExport.name === "default"; if (!isDangerous) { this.core.log.echoInfo(`tree shaking: Remove ${fileExport.name} from ${file.getFuseBoxFullPath()}`) //file.localExportUsageAmount.get(fileExport.) fileExport.remove(); // check for the referenced name // for example // var bar_1 = $fsx.r(2); // exports.bar = bar_1.bar; // when removing "exports.bar" "bar_1" is left out, we need to find in require statements check if it's not used and remove it too // and of course de-refence the file as well if (fileExport.referencedVariableName) { file.requireStatements.forEach(s => { if (s.identifier === fileExport.referencedVariableName) { s.localReferences--; uknownStatements.add(s); } }); } } } }); uknownStatements.forEach(statement => { // of nobody is using this require statement if (statement.localReferences === 0) { // releasing statement file so it can be removed; let targetFile = statement.resolve(); if (targetFile) { targetFile.releaseDependent(file); } //statement.file.releaseDependent(file); statement.removeWithIdentifier(); } }); if (file.isNotUsedAnywhere() && this.core.opts.canBeRemovedByTreeShaking(file)) { this.core.log.echoInfo(`tree shaking: Mark for removal ${file.getFuseBoxFullPath()}`) file.markForRemoval(); } }); } /** * Figure out if we can actually tree shake a file * @param target */ private shakeExports(target: FileAbstraction) { return this.eachFile(file => { const dependencies = file.getDependencies(); // check if our target is referenced in that file // e.g require('./foo') - considering that foo is resolved target if (dependencies.has(target)) { const dependency = dependencies.get(target); dependency.forEach(statement => { if (statement.usedNames.size > 0) { target.shakable = true; } else { target.restrictTreeShaking(); } target.namedExports.forEach(fileExport => { // now we check if file exports matches a name const nameIsUsed = statement.usedNames.has(fileExport.name); // if an exports is used, mark // mark it if (nameIsUsed) { fileExport.isUsed = true; } else { if (target.localExportUsageAmount.get(fileExport.name) && target.localExportUsageAmount.get(fileExport.name) > 1) { fileExport.isUsed = true; } } }); }); } }); } private eachFile(fn: { (file: FileAbstraction) }) { return each(this.core.producerAbstraction.bundleAbstractions, (bundleAbstraction: BundleAbstraction​​) => { return each(bundleAbstraction.packageAbstractions, (packageAbstraction: PackageAbstraction) => { return each(packageAbstraction.fileAbstractions, (fileAbstraction: FileAbstraction) => { return fn(fileAbstraction); }); }) }); } }