fuse-box
Version:
Fuse-Box a bundler that does it right
482 lines (418 loc) • 17.4 kB
text/typescript
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)
});
}
}