UNPKG

fuse-box

Version:

Fuse-Box a bundler that does it right

482 lines (418 loc) 17.4 kB
import { acornParse } from "../../analysis/FileAnalysis"; import { PackageAbstraction } from "./PackageAbstraction"; import { ASTTraverse } from "../../ASTTraverse"; import { RequireStatement } from "./nodes/RequireStatement"; import * as escodegen from "escodegen"; import * as path from "path"; import { ensureFuseBoxPath, transpileToEs5 } from "../../Utils"; import { matchesAssignmentExpression, matchesLiteralStringExpression, matchesSingleFunction, matchesDoubleMemberExpression, matcheObjectDefineProperty, matchesEcmaScript6, matchesTypeOf, matchRequireIdentifier, trackRequireMember, matchNamedExport, isExportMisused, matchesNodeEnv, matchesExportReference, matchesIfStatementProcessEnv, compareStatement, matchesIfStatementFuseBoxIsEnvironment, isExportComputed } from "./AstUtils"; import { ExportsInterop } from "./nodes/ExportsInterop"; import { UseStrict } from "./nodes/UseStrict"; import { TypeOfExportsKeyword } from "./nodes/TypeOfExportsKeyword"; import { TypeOfModuleKeyword } from "./nodes/TypeOfModuleKeyword"; import { TypeOfWindowKeyword } from "./nodes/TypeOfWindowKeyword"; import { NamedExport } from "./nodes/NamedExport"; import { GenericAst } from "./nodes/GenericAst"; import { QuantumItem } from "../plugin/QuantumSplit"; import { QuantumCore } from "../plugin/QuantumCore"; import { ReplaceableBlock } from "./nodes/ReplaceableBlock"; const globalNames = new Set<string>(["__filename", "__dirname", "exports", "module"]); export class FileAbstraction { private id: string; private fileMapRequested = false; private treeShakingRestricted = false; public dependents = new Set<FileAbstraction>(); private dependencies = new Map<FileAbstraction, Set<RequireStatement​​>>(); public ast: any; public fuseBoxDir; public isEcmaScript6 = false; public shakable = false; public globalsName: string; public amountOfReferences = 0; public canBeRemoved = false; public quantumItem: QuantumItem; public namedRequireStatements = new Map<string, RequireStatement​​>(); /** FILE CONTENTS */ public requireStatements = new Set<RequireStatement​​>(); public dynamicImportStatements = new Set<RequireStatement​​>(); public fuseboxIsEnvConditions = new Set<ReplaceableBlock>(); public exportsInterop = new Set<ExportsInterop>(); public useStrict = new Set<UseStrict>(); public typeofExportsKeywords = new Set<TypeOfExportsKeyword>(); public typeofModulesKeywords = new Set<TypeOfModuleKeyword>(); public typeofWindowKeywords = new Set<TypeOfWindowKeyword>(); public typeofGlobalKeywords = new Set<GenericAst>(); public typeofDefineKeywords = new Set<GenericAst>(); public typeofRequireKeywords = new Set<GenericAst>(); public namedExports = new Map<string, NamedExport>(); public processNodeEnv = new Set<ReplaceableBlock>(); public core: QuantumCore; public isEntryPoint = false; public wrapperArguments: string[]; public localExportUsageAmount = new Map<string, number>(); private globalVariables = new Set<string>(); constructor(public fuseBoxPath: string, public packageAbstraction: PackageAbstraction) { this.fuseBoxDir = ensureFuseBoxPath(path.dirname(fuseBoxPath)); this.setID(fuseBoxPath); packageAbstraction.registerFileAbstraction(this); this.core = this.packageAbstraction.bundleAbstraction.producerAbstraction.quantumCore; // removing process polyfill if not required // techincally this is not necessary when tree shaking is enable. Because of: // StatementModification.ts lines: // if (resolvedFile.isProcessPolyfill() && !core.opts.shouldBundleProcessPolyfill()) { // return statement.removeWithIdentifier(); // } // Which doesn't add the references if (this.core && !this.core.opts.shouldBundleProcessPolyfill() && this.isProcessPolyfill()) { this.markForRemoval(); } } public isProcessPolyfill() { return this.getFuseBoxFullPath() === "process/index.js"; } public registerHoistedIdentifiers(identifier: string, statement: RequireStatement, resolvedFile: FileAbstraction) { const bundle = this.packageAbstraction.bundleAbstraction; bundle.registerHoistedIdentifiers(identifier, statement, resolvedFile); } public getFuseBoxFullPath() { return `${this.packageAbstraction.name}/${this.fuseBoxPath}`; } public isNotUsedAnywhere() { return this.getID().toString() !== "0" && this.dependents.size === 0 && !this.quantumItem && !this.isEntryPoint; } public releaseDependent(file: FileAbstraction) { this.dependents.delete(file); } public markForRemoval() { this.canBeRemoved = true; } /** * Initiates an abstraction from string */ public loadString(contents: string) { this.ast = acornParse​​(contents); this.analyse(); } public setID(id: any) { this.id = id; } public referenceQuantumSplit(item: QuantumItem) { item.addFile(this); this.quantumItem = item; } public getSplitReference(): QuantumItem { return this.quantumItem; } public getID() { return this.id; } public addFileMap() { this.fileMapRequested = true; } public isTreeShakingAllowed() { return this.treeShakingRestricted === false && this.shakable; } public restrictTreeShaking() { this.treeShakingRestricted = true; } public addDependency(file: FileAbstraction, statement: RequireStatement) { let list: Set<RequireStatement>; if (this.dependencies.has(file)) { list = this.dependencies.get(file); } else { list = new Set<RequireStatement>() this.dependencies.set(file, list); } list.add(statement) } public getDependencies() { return this.dependencies; } /** * Initiates with AST */ public loadAst(ast: any) { // fix the initial node ast.type = "Program" this.ast = ast; this.analyse(); } /** * Finds require statements with given mask */ public findRequireStatements(exp: RegExp): RequireStatement[] { let list: RequireStatement[] = []; this.requireStatements.forEach(statement => { if (exp.test(statement.value)) { list.push(statement); } }) return list; } public wrapWithFunction(args: string[]) { this.wrapperArguments = args; } /** * Return true if there is even a single require statement */ public isRequireStatementUsed() { return this.requireStatements.size > 0; } public isDirnameUsed() { return this.globalVariables.has("__dirname"); } public isFilenameUsed() { return this.globalVariables.has("__filename"); } public isExportStatementInUse() { return this.globalVariables.has("exports"); } public isModuleStatementInUse() { return this.globalVariables.has("module"); } public isExportInUse() { return this.globalVariables.has("exports") || this.globalVariables.has("module"); } public setEnryPoint(globalsName?: string) { this.isEntryPoint = true; this.globalsName = globalsName; this.treeShakingRestricted = true; } public generate(ensureEs5: boolean = false) { let code = escodegen.generate(this.ast); if (ensureEs5 && this.isEcmaScript6) { code = transpileToEs5(code); } //if (this.wrapperArguments) { let fn = ["function(", this.wrapperArguments ? this.wrapperArguments.join(",") : "", '){\n']; // inject __dirname if (this.isDirnameUsed()) { fn.push(`var __dirname = ${JSON.stringify(this.fuseBoxDir)};` + "\n"); } if (this.isFilenameUsed()) { fn.push(`var __filename = ${JSON.stringify(this.fuseBoxPath)};` + "\n"); } fn.push(code, '\n}'); code = fn.join(""); return code; } /** * * @param node * @param parent * @param prop * @param idx */ private onNode(node, parent, prop, idx) { // process.env if (this.core) { const processKeyInIfStatement = matchesIfStatementProcessEnv(node); const value = this.core.producer.userEnvVariables[processKeyInIfStatement]; if (processKeyInIfStatement) { const result = compareStatement(node, value); const processNode = new ReplaceableBlock(node.test, "left", node.test.left); this.processNodeEnv.add(processNode); return processNode.conditionalAnalysis(node, result); } else { const inlineProcessKey = matchesNodeEnv(node); if (inlineProcessKey) { const value = this.core.producer.userEnvVariables[inlineProcessKey]; const env = new ReplaceableBlock(parent, prop, node); value === undefined ? env.setUndefinedValue() : env.setValue(value); this.processNodeEnv.add(env); } } const isEnvName = matchesIfStatementFuseBoxIsEnvironment(node); if (isEnvName) { let value; if (isEnvName === "isServer") { value = this.core.opts.isTargetServer(); } if (isEnvName === "isBrowser") { value = this.core.opts.isTargetBrowser(); } if (!this.core.opts.isTargetUniveral()) { const isEnvNode = new ReplaceableBlock(node, "", node.test); isEnvNode.identifier = isEnvName; this.fuseboxIsEnvConditions.add(isEnvNode); return isEnvNode.conditionalAnalysis(node, value); } } if (matchesDoubleMemberExpression(node, "FuseBox")) { let envName = node.property.name; if (envName === "isServer" || envName === "isBrowser") { let value; if (envName === "isServer") { value = this.core.opts.isTargetServer(); } if (envName === "isBrowser") { value = this.core.opts.isTargetBrowser(); } const envNode = new ReplaceableBlock(parent, prop, node); envNode.identifier = envName; envNode.setValue(value); this.fuseboxIsEnvConditions.add(envNode); } } } // detecting es6 if (matchesEcmaScript6(node)) { this.isEcmaScript6 = true; } this.namedRequireStatements.forEach((statement, key) => { const importedName = trackRequireMember(node, key); if (importedName) { statement.usedNames.add(importedName); } }); // restrict tree shaking if there is even a hint on computed properties isExportComputed(node, (isComputed) => { if (isComputed) { this.restrictTreeShaking(); } }) // trying to match a case where an export is misused // for example exports.foo.bar.prototype // we can't tree shake this exports isExportMisused(node, name => { const createdExports = this.namedExports.get(name); if (createdExports) { createdExports.eligibleForTreeShaking = false; } }); /** * Matching how many times an export has been used within one file * For example * exports.createAction = () => { * return exports.createSomething(); * } * exports.createSomething = () => {} * The example above creates a conflicting situation if createSomething wasn't used externally */ const matchesExportIdentifier = matchesExportReference(node); if (matchesExportIdentifier) { let ref = this.localExportUsageAmount.get(matchesExportIdentifier) if (ref === undefined) { this.localExportUsageAmount.set(matchesExportIdentifier, 1) } else { this.localExportUsageAmount.set(matchesExportIdentifier, ++ref) } } matchNamedExport(node, (name) => { // const namedExport = new NamedExport(parent, prop, node); // namedExport.name = name; // this.namedExports.set(name, namedExport); let namedExport: NamedExport; //namedExport.name = name; if (!this.namedExports.get(name)) { namedExport = new NamedExport(); namedExport.name = name; this.namedExports.set(name, namedExport) } else { namedExport = this.namedExports.get(name); } namedExport.addNode(parent, prop, node); }); // require statements if (matchesSingleFunction(node, "require")) { // adding a require statement this.requireStatements.add(new RequireStatement(this, node)); } // Fusebox converts new imports to $fsmp$ if (matchesSingleFunction(node, "$fsmp$")) { // adding a require statement this.dynamicImportStatements.add(new RequireStatement(this, node)); } // typeof module if (matchesTypeOf(node, "module")) { this.typeofModulesKeywords.add(new TypeOfModuleKeyword(parent, prop, node)); } if (matchesTypeOf(node, "require")) { this.typeofRequireKeywords.add(new GenericAst(parent, prop, node)); } // Object.defineProperty(exports, '__esModule', { value: true }); if (matcheObjectDefineProperty(node, "exports")) { if (!this.globalVariables.has("exports")) { this.globalVariables.add("exports"); } this.exportsInterop.add(new ExportsInterop(parent, prop, node)); } if (matchesAssignmentExpression(node, 'exports', '__esModule')) { if (!this.globalVariables.has("exports")) { this.globalVariables.add("exports"); } this.exportsInterop.add(new ExportsInterop(parent, prop, node)); } if (matchesTypeOf(node, "exports")) { this.typeofExportsKeywords.add(new TypeOfExportsKeyword(parent, prop, node)); } if (matchesLiteralStringExpression(node, "use strict")) { this.useStrict.add(new UseStrict(parent, prop, node)); } if (matchesTypeOf(node, "global")) { this.typeofGlobalKeywords.add(new GenericAst(parent, prop, node)) } if (matchesTypeOf(node, "define")) { this.typeofDefineKeywords.add(new GenericAst(parent, prop, node)) } // typeof window if (matchesTypeOf(node, "window")) { this.typeofWindowKeywords.add(new GenericAst(parent, prop, node)) } /** * Matching * var name = require('module') * Gethering identifiers name to do: * 1) Hoisting * 2) Detect which variables are used in exports to do tree shaking later on */ const requireIdentifier = matchRequireIdentifier(node); if (requireIdentifier) { const identifiedRequireStatement = new RequireStatement(this, node.init, node); identifiedRequireStatement.identifier = requireIdentifier; this.namedRequireStatements.set(requireIdentifier, identifiedRequireStatement); this.requireStatements.add(identifiedRequireStatement); return false; } // FuseBox features if (matchesDoubleMemberExpression(node, "FuseBox")) { if (node.property.name === "import") { // replace it right away with require statement parent.callee = { type: "Identifier", name: "require" } // treat it like any any other require statements this.requireStatements.add(new RequireStatement(this, parent)); } return false; } // global vars if (node && node.type === "Identifier") { let globalVariable; if (globalNames.has(node.name)) { globalVariable = node.name; } if (node.name === "global") { this.packageAbstraction.bundleAbstraction.globalVariableRequired = true; } if (globalVariable) { if (!this.globalVariables.has(globalVariable)) { this.globalVariables.add(globalVariable); } } } } public analyse() { // console.log(JSON.stringify(this.ast, null, 2)); ASTTraverse.traverse(this.ast, { pre: (node, parent, prop, idx) => this.onNode(node, parent, prop, idx) }); } }