UNPKG

fuse-box

Version:

Fuse-Box a bundler that does it right

400 lines (358 loc) • 12.2 kB
import { File } from "./File"; import { PathMaster, IPackageInformation } from "./PathMaster"; import { WorkFlowContext } from "./WorkflowContext"; import { BundleData } from "../arithmetic/Arithmetic"; import { ensurePublicExtension, string2RegExp } from "../Utils"; import { each, utils } from "realm-utils"; /** * * * @export * @class ModuleCollection */ export class ModuleCollection { /** * * * @type {Map<string, ModuleCollection>} * @memberOf ModuleCollection */ public nodeModules: Map<string, ModuleCollection> = new Map(); public traversed = false; public acceptFiles = true; /** * * * @type {Map<string, File>} * @memberOf ModuleCollection */ public dependencies: Map<string, File> = new Map(); /** * * * @type {BundleData} * @memberOf ModuleCollection */ public bundle: BundleData; /** * * * * @memberOf ModuleCollection */ public entryResolved = false; /** * * * @type {PathMaster} * @memberOf ModuleCollection */ public pm: PathMaster; /** * * * @type {File} * @memberOf ModuleCollection */ public entryFile: File; /** * * * * @memberOf ModuleCollection */ public cached = false; /** * * * @type {string} * @memberOf ModuleCollection */ public cachedContent: string; /** * * * @type {string} * @memberOf ModuleCollection */ public cachedName: string; /** * * * @type {string} * @memberOf ModuleCollection */ public cacheFile: string; /** * * * @type {Map<string, string>} * @memberOf ModuleCollection */ public conflictingVersions: Map<string, string> = new Map(); /** * * * @private * @type {File[]} * @memberOf ModuleCollection */ private toBeResolved: File[] = []; /** * * * @private * * @memberOf ModuleCollection */ private delayedResolve = false; public isDefault = false; /** * Creates an instance of ModuleCollection. * * @param {WorkFlowContext} context * @param {string} name * @param {IPackageInformation} [info] * * @memberOf ModuleCollection */ constructor(public context: WorkFlowContext, public name: string, public info?: IPackageInformation) { this.isDefault = this.name === this.context.defaultPackageName; } /** * * * @param {File} file * * @memberOf ModuleCollection */ public setupEntry(file: File) { if (this.dependencies.has(file.info.absPath)) { this.dependencies.set(file.info.absPath, file); } file.isNodeModuleEntry = true; this.entryFile = file; } /** * * * @param {boolean} [shouldIgnoreDeps] * @returns * * @memberOf ModuleCollection */ public resolveEntry(shouldIgnoreDeps?: boolean) { if (this.entryFile && !this.entryResolved) { this.entryResolved = true; return this.resolve(this.entryFile, shouldIgnoreDeps); } } /** * Init plugins * Call "init" plugins with context * Inject dependencies as well * @memberOf ModuleCollection */ public initPlugins() { // allow easy regex this.context.plugins.forEach(plugin => { if (utils.isArray(plugin) && utils.isString(plugin[0])) { plugin.splice(0, 1, string2RegExp(plugin[0])); } else { if (plugin && utils.isString(plugin.test)) { plugin.test = string2RegExp(plugin.test); } } }); this.context.triggerPluginsMethodOnce("init", [this.context], (plugin) => { if (plugin.dependencies) { plugin.dependencies.forEach(mod => { this.toBeResolved.push( new File(this.context, this.pm.init(mod)) ); }); } }); } public resolveDepsOnly(depsOnly: Map<string, any>) { return each(depsOnly, (withDeps, modulePath) => { let file = new File(this.context, this.pm.init(modulePath)); return this.resolve(file); }).then(() => { // reset current dependencies // so they won't get bundled this.dependencies = new Map<string, File>(); }); } public collectBundle(data: BundleData): Promise<void> { this.bundle = data; this.delayedResolve = true; this.initPlugins(); if (this.context.defaultEntryPoint) { this.entryFile = File.createByName(this, ensurePublicExtension(this.context.defaultEntryPoint)); } return this.resolveDepsOnly(data.depsOnly).then(() => { return each(data.including, (withDeps, modulePath) => { let file = new File(this.context, this.pm.init(modulePath)); return this.resolve(file); }) .then(() => this.context.resolve()) .then(() => this.transformGroups()) .then(() => { return this.context.useCache ? this.context.cache.resolve(this.toBeResolved) : this.toBeResolved; }).then(toResolve => { return each(toResolve, (file: File) => this.resolveNodeModule(file)); }) // node modules might need to resolved asynchronously // like css plugins .then(() => this.context.resolve()) .then(() => this.context.cache && this.context.cache.buildMap(this)) .catch(e => { this.context.defer.unlock(); this.context.nukeCache(); console.error(e); }); }); } /** * * * @param {File} file * @returns * * @memberOf ModuleCollection */ public resolveNodeModule(file: File) { let info = file.info.nodeModuleInfo; if (this.context.isShimed(info.name)) { return; } let collection: ModuleCollection; // setting key for a module // It might be just lodash (for default version) or lodash@1.0.0 // We don't register and process node_modules twice // So for example, 2 modules have a custom dependency lodash@1.0.0 // In a nutshell we try to avoid grabbing the same source from different folders let moduleName = `${info.name}@${info.version}`; // Make sure it has not been mentioned ever befor if (!this.context.hasNodeModule(moduleName)) { collection = new ModuleCollection(this.context, info.custom ? moduleName : info.name, info); collection.pm = new PathMaster(this.context, info.root); if (info.entry) { // Some modules don't have entry files collection.setupEntry(new File(this.context, collection.pm.init(info.entry))); } this.context.addNodeModule(moduleName, collection); } else { collection = this.context.getNodeModule(moduleName); } // If we are using a custom version // THe source output should know about. // When compiling the ouput we will take it into a consideration if (info.custom) { this.conflictingVersions.set(info.name, info.version); } // Setting it a node dependency, so later on we could build a dependency tree // For caching this.nodeModules.set(moduleName, collection); // check for bundle data (in case of fuse.register) if (info.bundleData) { info.bundleData.including.forEach((inf, fname) => { const userFileInfo = collection.pm.init(fname); if (!userFileInfo.isNodeModule) { let userFile = new File(this.context, userFileInfo); userFile.consume(); collection.dependencies.set(userFileInfo.fuseBoxPath, userFile); } }) } // Handle explicit require differently // e.g require("lodash") - we require entry file // unlike require("requre/each") - points to an explicit file // So we might never resolve the entry (if only a partial require was mentioned) return file.info.nodeModuleExplicitOriginal && collection.pm ? collection.resolve(new File(this.context, collection.pm.init(file.info.absPath, file.info.fuseBoxAlias))) : collection.resolveEntry(); } public transformGroups() { const promises = []; this.context.fileGroups.forEach((group: File, name: string) => { this.dependencies.set(group.info.fuseBoxPath, group); if (group.groupHandler) { if (utils.isFunction(group.groupHandler.transformGroup)) { promises.push(new Promise((resolve, reject) => { const result = group.groupHandler.transformGroup(group); if (utils.isPromise(result)) { return result.then(resolve).catch(reject); } return resolve(); })); } } }); return Promise.all(promises); } public resolveSplitFiles(files: File[]): Promise<void> { return each(files, (file: File) => { this.dependencies.set(file.absPath, file); }); } /** * * * @param {File} file * @param {boolean} [shouldIgnoreDeps] * @returns * * @memberOf ModuleCollection */ public resolve(file: File, shouldIgnoreDeps?: boolean) { file.collection = this; if (this.bundle) { if (this.bundle.fileBlackListed(file)) { return; } if (shouldIgnoreDeps === undefined) { shouldIgnoreDeps = this.bundle.shouldIgnoreNodeModules(file.getCrossPlatormPath()); } } if (file.info.isNodeModule) { if (this.context.isGlobalyIgnored(file.info.nodeModuleName)) { return; } // Check if a module needs to ignored // It could be defined previosly (as in exluding all dependencies) // Of an explict exclusion if (shouldIgnoreDeps || this.bundle && this.bundle.shouldIgnore(file.info.nodeModuleName)) { return; } // If a collection a primary one (project "default") // We would like to collect dependencies first and resolve them later // In order to understand what is cached return this.delayedResolve ? this.toBeResolved.push(file) : this.resolveNodeModule(file); } else { if (this.dependencies.has(file.absPath)) { return; } // Consuming file // Here we read it and return a list of require statements file.consume(); // if a file belong to a split bundle, pipe it there if (this.isDefault && this.context.shouldSplit(file)) { return; } this.dependencies.set(file.absPath, file); let fileLimitPath; // Checking for the limits // we must have a workaround for ../ and if it goes beyond project limits if (this.entryFile && this.entryFile.isNodeModuleEntry) { fileLimitPath = this.entryFile.info.absPath; } // Process file dependencies recursively return each(file.analysis.dependencies, name => { return this.resolve(new File(this.context, this.pm.resolve(name, file.info.absDir, fileLimitPath)), shouldIgnoreDeps); }); } } }