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