fuse-box
Version:
Fuse-Box a bundler that does it right
206 lines (181 loc) • 7.18 kB
text/typescript
import { Bundle } from "./Bundle";
import { FuseBox } from "./FuseBox";
import { string2RegExp, ensureUserPath, ensureFuseBoxPath } from "../Utils";
import { EventEmitter } from "events";
import { Arithmetic, BundleData } from "../arithmetic/Arithmetic";
import { SharedCustomPackage } from "./SharedCustomPackage";
import { BundleRunner } from "./BundleRunner";
import { ServerOptions } from "../devServer/Server";
import * as chokidar from "chokidar";
import { utils, each } from "realm-utils";
import { ProducerAbstraction, ProducerAbtractionOptions } from "../quantum/core/ProducerAbstraction";
import { BundleAbstraction } from "../quantum/core/BundleAbstraction";
export class BundleProducer {
public bundles = new Map<string, Bundle>();
public hmrInjected = false;
public hmrAllowed = true;
public sharedEvents = new EventEmitter();
public writeBundles = true;
public sharedCustomPackages: Map<string, SharedCustomPackage>;
public runner: BundleRunner;
public userEnvVariables: any = Object.assign(process.env, { NODE_ENV: "production" });
public devServerOptions: ServerOptions;
public entryPackageName: string;
public entryPackageFile: string;
private injectedCode = new Map<string, string>();
private chokidarOptions: any;
private warnings = new Map<string, string[]>();
constructor(public fuse: FuseBox) {
this.runner = new BundleRunner(this.fuse);
}
public run(opts: { chokidar?: any, runType?: string }): Promise<BundleProducer> {
if (opts) {
this.chokidarOptions = opts.chokidar;
}
/** Collect information about watchers and start watching */
this.watch();
return this.runner.run(opts).then(() => {
this.sharedEvents.emit("producer-done");
this.printWarnings();
return each(this.fuse.context.plugins, plugin => {
if (plugin && utils.isFunction(plugin.producerEnd)) {
return plugin.producerEnd(this);
}
});
}).then(() => this);
}
public addUserProcessEnvVariables(data: any) {
this.userEnvVariables = Object.assign(this.userEnvVariables, data);
}
public printWarnings() {
if (this.warnings.size > 0) {
this.fuse.context.log.echoBreak();
this.warnings.forEach(warnings => {
warnings.forEach(list => {
this.fuse.context.log.echoWarning(list);
});
});
this.fuse.context.log.echoBreak();
}
}
public addWarning(key: string, message: string) {
let list;
if (!this.warnings.has(key)) {
list = []
this.warnings.set(key, list)
} else {
list = this.warnings.get(key)
}
list.push(message);
}
public devCodeHasBeenInjected(key: string) {
return this.injectedCode.has(key);
}
public getDevInjections(): Map<string, string> {
return this.injectedCode;
}
public injectDevCode(key: string, code: string) {
if (!this.injectedCode.has(key)) {
this.injectedCode.set(key, code)
}
}
public sortBundles(): Bundle[] {
let bundles = [...this.bundles.values()];
bundles = bundles.sort((a, b) => {
if (a.webIndexPriority < b.webIndexPriority) {
return 1;
}
if (a.webIndexPriority > b.webIndexPriority) {
return -1;
}
return 0;
});
return bundles;
}
public generateAbstraction(opts?: ProducerAbtractionOptions): Promise<ProducerAbstraction> {
const abstraction = new ProducerAbstraction(opts);
return each(this.bundles, (bundle: Bundle) => {
const bundleAbstraction = new BundleAbstraction(bundle.name);
abstraction.registerBundleAbstraction(bundleAbstraction);
return bundleAbstraction.parse(bundle.generatedCode.toString());
}).then(() => {
return abstraction;
});
}
public register(packageName: string, opts: any) {
let instructions = opts.instructions;
if (!packageName) {
throw new Error("Package name is required");
}
if (!opts.homeDir) {
throw new Error("Register requires homeDir!");
}
let homeDir = ensureUserPath(opts.homeDir);
if (!instructions) {
throw new Error("Register requires opts.instructions!");
}
let parser = Arithmetic.parse(instructions);
// doing the arithmetic magic here
if (!this.sharedCustomPackages) {
this.sharedCustomPackages = new Map<string, SharedCustomPackage>();
}
return Arithmetic.getFiles(parser, false, homeDir).then((data: BundleData) => {
let pkg = new SharedCustomPackage(packageName, data);
pkg.init(homeDir, opts.main || "index.js");
this.sharedCustomPackages.set(packageName, pkg);
});
}
public isShared(name: string) {
return this.sharedCustomPackages && this.sharedCustomPackages.get(name);
}
public getSharedPackage(name: string): SharedCustomPackage {
return this.sharedCustomPackages.get(name);
}
public add(name: string, bundle: Bundle) {
this.bundles.set(name, bundle);
/** Add bundle to the runner */
this.runner.bundle(bundle);
}
public watch() {
let settings = new Map<string, RegExp>();
let isRequired = false;
// collecting paths and bundles name
this.bundles.forEach(bundle => {
if (bundle.watchRule) {
isRequired = true;
settings.set(bundle.name, string2RegExp(bundle.watchRule));
}
});
// Initiate watch if any of the bundles within producer requires a watcher
if (!isRequired) {
return;
}
let ready = false;
chokidar.watch(this.fuse.context.homeDir, this.chokidarOptions || {})
.on('all', (event, fp) => {
if (ready) {
this.onChanges(settings, fp)
}
}).on('ready', () => {
ready = true;
});
}
/** Trigger bundles that are affected */
protected onChanges(settings: Map<string, RegExp>, path: string) {
path = ensureFuseBoxPath(path);
settings.forEach((expression, bundleName) => {
if (expression.test(path)) {
const bundle = this.bundles.get(bundleName);
const defer = bundle.fuse.context.defer;
bundle.lastChangedFile = path;
// to ensure new process is not kicked in before the previous has completed
defer.queue(bundleName, () => {
return bundle.exec().then(result => {
this.sharedEvents.emit("file-changed", [bundle, path]);
return result;
});
});
}
});
}
}