fuse-box
Version:
Fuse-Box a bundler that does it right
667 lines (540 loc) • 19.2 kB
text/typescript
import * as path from "path";
import * as escodegen from "escodegen";
import { BundleSource } from "../BundleSource";
import { File } from "./File";
import { Log } from "../Log";
import { IPackageInformation, IPathInformation, AllowedExtenstions } from "./PathMaster";
import { ModuleCollection } from "./ModuleCollection";
import { ModuleCache } from "../ModuleCache";
import { EventEmitter } from "../EventEmitter";
import { utils } from "realm-utils";
import { ensureUserPath, findFileBackwards, ensureDir, removeFolder } from "../Utils";
import { SourceChangedEvent } from "../devServer/Server";
import { registerDefaultAutoImportModules, AutoImportedModule } from "./AutoImportedModule";
import { Defer } from "../Defer";
import { UserOutput } from "./UserOutput";
import { FuseBox } from "./FuseBox";
import { Bundle } from "./Bundle";
import { BundleProducer } from "./BundleProducer";
import { QuantumSplitConfig, QuantumItem, QuantumSplitResolveConfiguration } from "../quantum/plugin/QuantumSplit";
import { isPolyfilledByFuseBox } from "./ServerPolyfillList";
import { CSSDependencyExtractor, ICSSDependencyExtractorOptions } from "../lib/CSSDependencyExtractor";
const appRoot = require("app-root-path");
/**
* All the plugin method names
*/
export type PluginMethodName =
"init"
| "preBuild"
| "preBundle"
| "bundleStart"
| "bundleEnd"
| "postBundle"
| "postBuild";
/**
* Interface for a FuseBox plugin
*/
export interface Plugin {
test?: RegExp;
options?: any;
init?(context: WorkFlowContext): any;
transform?(file: File, ast?: any): any;
transformGroup?(file: File): any;
onTypescriptTransform?(file: File): any;
bundleStart?(context: WorkFlowContext): any;
bundleEnd?(context: WorkFlowContext): any;
producerEnd?(producer: BundleProducer): any;
onSparky?(): any;
/**
* If provided then the dependencies are loaded on the client
* before the plugin is invoked
*/
dependencies?: string[];
}
/**
* Gets passed to each plugin to track FuseBox configuration
*/
export class WorkFlowContext {
/**
* defaults to app-root-path, but can be set by user
* @see FuseBox
*/
public appRoot: any = appRoot.path;
public shim: any;
public writeBundles = true;
public fuse: FuseBox;
public useTypescriptCompiler = false;
public userWriteBundles = true;
public showWarnings = true;
public useJsNext: boolean | string[] = false;
public sourceChangedEmitter = new EventEmitter<SourceChangedEvent>();
/**
* The default package name or the package name configured in options
*/
public defaultPackageName = "default";
public transformTypescript?: (contents: string) => string;
public ignoreGlobal: string[] = [];
public pendingPromises: Promise<any>[] = [];
public polyfillNonStandardDefaultUsage: boolean | string[] = false;
public customAPIFile: string;
public experimentalFeaturesEnabled = false;
public defaultEntryPoint: string;
public rollupOptions: any;
public output: UserOutput;
public hash: string | Boolean;
public target: string = "universal";
/**
* Explicitly target bundle to server
*/
public serverBundle = false;
public nodeModules: Map<string, ModuleCollection> = new Map();
public libPaths: Map<string, IPackageInformation> = new Map();
public homeDir: string;
public printLogs = true;
public runAllMatchedPlugins = false;
public plugins: Plugin[];
public fileGroups: Map<string, File>;
public useCache = true;
public doLog = true;
public cache: ModuleCache;
public tsConfig: any;
public customModulesFolder: string;
public tsMode = false;
public loadedTsConfig: string;
public globals: { [packageName: string]: /** Variable name */ string };
public standaloneBundle: boolean = true;
public source: BundleSource;
public sourceMapsProject: boolean = false;
public sourceMapsVendor: boolean = false;
public inlineSourceMaps: boolean = true;
public sourceMapsRoot: string = "/src";
public useSourceMaps = false;
public initialLoad = true;
public debugMode = false;
public quantumSplitConfig: QuantumSplitConfig;
public log: Log = new Log(this);
public pluginTriggers: Map<string, Set<String>>;
public natives = {
process: true,
stream: true,
Buffer: true,
http: true,
}
public autoImportConfig = {};
public bundle: Bundle;
public storage: Map<string, any>;
public aliasCollection: any[];
public experimentalAliasEnabled = false;
public customCodeGenerator: any;
public defer = new Defer;
public initCache() {
this.cache = new ModuleCache(this);
}
public resolve() {
return Promise.all(this.pendingPromises).then(() => {
this.pendingPromises = [];
});
}
public queue(obj: any) {
this.pendingPromises.push(obj);
}
public convertToFuseBoxPath(name: string) {
let root = this.homeDir;
name = name.replace(/\\/g, "/");
root = root.replace(/\\/g, "/");
name = name.replace(root, "").replace(/^\/|\\/, "");
return name;
}
public isBrowserTarget() {
return this.target === "browser";
}
public shouldPolyfillNonStandardDefault(file: File) {
if (file.belongsToProject()) {
return false;
}
let collectionName = file.collection && file.collection.name;
if (collectionName === "fuse-heresy-default") {
return false;
}
if (this.polyfillNonStandardDefaultUsage === true) {
return true;
}
if (Array.isArray(this.polyfillNonStandardDefaultUsage)) {
return this.polyfillNonStandardDefaultUsage.indexOf(collectionName) > -1
}
}
public shouldUseJsNext(libName: string) {
if (this.useJsNext === true) {
return true;
}
if (Array.isArray(this.useJsNext)) {
return this.useJsNext.indexOf(libName) > -1
}
}
public quantumSplit(rule: string, bundleName: string, entryFile: string) {
if (!this.quantumSplitConfig) {
this.quantumSplitConfig = new QuantumSplitConfig(this);
}
this.quantumSplitConfig.register(rule, bundleName, entryFile);
}
public configureQuantumSplitResolving(opts: QuantumSplitResolveConfiguration) {
if (!this.quantumSplitConfig) {
this.quantumSplitConfig = new QuantumSplitConfig(this);
}
this.quantumSplitConfig.resolveOptions = opts;
}
public getQuantumDevelepmentConfig() {
if (this.quantumSplitConfig) {
let opts: any = this.quantumSplitConfig.resolveOptions;
opts.bundles = {};
this.quantumSplitConfig.getItems().forEach(item => {
opts.bundles[item.name] = { main: item.entry };
});
return opts;
}
}
public requiresQuantumSplitting(path: string): QuantumItem {
if (!this.quantumSplitConfig) {
return;
}
return this.quantumSplitConfig.matches(path);
}
public setCodeGenerator(fn: any) {
this.customCodeGenerator = fn;
}
public generateCode(ast: any, opts?: any) {
if (this.customCodeGenerator) {
try {
return this.customCodeGenerator(ast);
} catch (e) { }
}
return escodegen.generate(ast, opts);
}
public emitJavascriptHotReload(file: File) {
let content = file.contents;
if (file.headerContent) {
content = file.headerContent.join("\n") + "\n" + content;
}
this.sourceChangedEmitter.emit({
type: "js",
content,
path: file.info.fuseBoxPath,
});
}
public debug(group: string, text: string) {
if (this.debugMode) {
this.log.echo(`${group} : ${text}`);
}
}
public nukeCache() {
this.resetNodeModules();
if (this.cache) {
removeFolder(this.cache.cacheFolder);
this.cache.initialize();
}
}
public setSourceMapsProperty(params: any) {
if (typeof params === "boolean") {
this.sourceMapsProject = params;
} else {
if (utils.isPlainObject(params)) {
this.sourceMapsProject = params.project !== undefined ? params.project : true;
this.sourceMapsVendor = params.vendor === true;
if (params.inline !== undefined) {
this.inlineSourceMaps = params.inline;
}
if (params.sourceRoot) {
this.sourceMapsRoot = params.sourceRoot;
}
}
}
if (this.sourceMapsProject || this.sourceMapsVendor) {
this.useSourceMaps = true;
}
}
public warning(str: string) {
return this.log.echoWarning(str);
}
public fatal(str: string) {
throw new Error(str);
}
public debugPlugin(plugin: Plugin, text: string) {
const name = plugin.constructor && plugin.constructor.name ? plugin.constructor.name : "Unknown";
this.debug(name, text);
}
public isShimed(name: string): boolean {
if (!this.shim) {
return false;
}
return this.shim[name] !== undefined;
}
public isHashingRequired() {
const hashOption = this.hash;
let useHash = false;
if (typeof hashOption === "string") {
if (hashOption !== "md5") {
throw new Error(`Uknown algorythm ${hashOption}`)
}
useHash = true;
}
if (hashOption === true) {
useHash = true;
}
return useHash;
}
/**
* Resets significant class members
*/
public reset() {
this.log.reset();
this.storage = new Map();
this.source = new BundleSource(this);
this.nodeModules = new Map();
this.pluginTriggers = new Map();
this.fileGroups = new Map();
this.libPaths = new Map();
}
public initAutoImportConfig(userNatives, userImports) {
if (this.target !== "server") {
this.autoImportConfig = registerDefaultAutoImportModules(userNatives);
if (utils.isPlainObject(userImports)) {
for (let varName in userImports) {
this.autoImportConfig[varName] = new AutoImportedModule(varName, userImports[varName]);
}
}
}
}
public setItem(key: string, obj: any) {
this.storage.set(key, obj);
}
public getItem(key: string, defaultValue?: any): any {
return this.storage.get(key) !== undefined ? this.storage.get(key) : defaultValue;
}
public setCSSDependencies(file: File, userDeps: string[]) {
let collection = this.getItem("cssDependencies") || {};
collection[file.info.absPath] = userDeps;
this.setItem("cssDependencies", collection);
}
public extractCSSDependencies(file: File, opts: ICSSDependencyExtractorOptions): string[] {
const extractor = CSSDependencyExtractor.init(opts);
this.setCSSDependencies(file, extractor.getDependencies())
return extractor.getDependencies();
}
public getCSSDependencies(file: File): string[] {
let collection = this.getItem("cssDependencies") || {};
return collection[file.info.absPath];
}
/**
* Create a new file group
* Mocks up file
*/
public createFileGroup(name: string, collection: ModuleCollection, handler: Plugin): File {
let info = <IPathInformation>{
fuseBoxPath: name,
absPath: name,
};
let file = new File(this, info);
file.collection = collection;
file.contents = "";
file.groupMode = true;
// Pass it along
// Transformation might happen in a different plugin
file.groupHandler = handler;
this.fileGroups.set(name, file);
return file;
}
public getFileGroup(name: string): File {
return this.fileGroups.get(name);
}
public allowExtension(ext: string) {
if (!AllowedExtenstions.has(ext)) {
AllowedExtenstions.add(ext);
}
}
public addAlias(obj: any, value?: any) {
const aliases = [];
if (!value) {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
if (path.isAbsolute(key)) {
// dying in agony
this.fatal(`Can't use absolute paths with alias "${key}"`);
}
aliases.push({ expr: new RegExp(`^(${key})(/|$)`), replacement: obj[key] });
}
}
} else {
aliases.push({ expr: new RegExp(`^(${obj})(/|$)`), replacement: value });
}
this.aliasCollection = this.aliasCollection || [];
this.aliasCollection = this.aliasCollection.concat(aliases)
this.experimentalAliasEnabled = true;
}
public setHomeDir(dir: string) {
this.homeDir = ensureDir(dir);
}
public setLibInfo(name: string, version: string, info: IPackageInformation) {
let key = `${name}@${version}`;
if (!this.libPaths.has(key)) {
return this.libPaths.set(key, info);
}
}
/** Converts the file extension from `.ts` to `.js` */
public convert2typescript(name: string) {
return name.replace(/\.ts$/, ".js");
}
public getLibInfo(name: string, version: string): IPackageInformation {
let key = `${name}@${version}`;
if (this.libPaths.has(key)) {
return this.libPaths.get(key);
}
}
public setPrintLogs(printLogs: boolean) {
this.printLogs = printLogs;
}
public setUseCache(useCache: boolean) {
this.useCache = useCache;
}
public hasNodeModule(name: string): boolean {
return this.nodeModules.has(name);
}
public isGlobalyIgnored(name: string): boolean {
if (this.ignoreGlobal.indexOf(name) > -1) {
return true;
}
if (this.target === "server") {
return isPolyfilledByFuseBox(name)
}
}
public resetNodeModules() {
this.nodeModules = new Map<string, ModuleCollection>();
}
public addNodeModule(name: string, collection: ModuleCollection) {
this.nodeModules.set(name, collection);
}
/**
* Retuns the parsed `tsconfig.json` contents
*/
public getTypeScriptConfig() {
if (this.loadedTsConfig) {
return this.loadedTsConfig;
}
let url, configFile;
let config: any = {
compilerOptions: {},
};;
if (this.tsConfig) {
configFile = ensureUserPath(this.tsConfig);
} else {
url = path.join(this.homeDir, "tsconfig.json");
let tsconfig = findFileBackwards(url, this.appRoot);
if (tsconfig) {
configFile = tsconfig;
}
}
if (configFile) {
this.log.echoStatus(`Typescript config: ${configFile.replace(this.appRoot, "")}`);
config = require(configFile);
} else {
this.log.echoStatus(`Typescript config file was not found. Improvising`);
}
config.compilerOptions.module = "commonjs";
if (this.useSourceMaps) {
config.compilerOptions.sourceMap = true;
config.compilerOptions.inlineSources = true;
}
// switch to target es6
if (this.rollupOptions) {
this.debug("Typescript", "Forcing es6 output for typescript. Rollup deteced");
config.compilerOptions.module = "es6";
config.compilerOptions.target = "es6";
}
this.loadedTsConfig = config;
return config;
}
public isFirstTime() {
return this.initialLoad === true;
}
public writeOutput(outFileWritten?: () => any) {
this.initialLoad = false;
const res = this.source.getResult();
if (this.bundle) {
this.bundle.generatedCode = res.content;
}
if (this.output && (!this.bundle || this.bundle && this.bundle.producer.writeBundles)) {
this.output.writeCurrent(res.content).then(() => {
this.writeSourceMaps(res);
this.defer.unlock();
if (utils.isFunction(outFileWritten)) {
outFileWritten();
}
});
} else {
this.defer.unlock();
outFileWritten();
}
}
protected writeSourceMaps(result: any) {
// Writing sourcemaps
if (this.sourceMapsProject || this.sourceMapsVendor) {
this.output.write(`${this.output.filename}.js.map`, result.sourceMap, true);
}
}
public shouldSplit(file: File): boolean {
if (!this.experimentalFeaturesEnabled) {
if (this.bundle && this.bundle.bundleSplit) {
return this.bundle.bundleSplit.verify(file);
}
}
return false;
}
public getNodeModule(name: string): ModuleCollection {
return this.nodeModules.get(name);
}
/**
* @param fn if provided, its called once the plugin method has been triggered
*/
public triggerPluginsMethodOnce(name: PluginMethodName, args: any, fn?: { (plugin: Plugin): void }) {
this.plugins.forEach(plugin => {
if (Array.isArray(plugin)) {
plugin.forEach(p => {
if (utils.isFunction(p[name])) {
if (this.pluginRequiresTriggering(p, name)) {
p[name].apply(p, args);
if (fn) {
fn(p);
}
}
}
});
}
if (plugin && utils.isFunction(plugin[name])) {
if (this.pluginRequiresTriggering(plugin, name)) {
plugin[name].apply(plugin, args);
if (fn) {
fn(plugin);
}
}
}
});
}
/**
* Makes sure plugin method is triggered only once
* @returns true if the plugin needs triggering
*/
private pluginRequiresTriggering(cls: any, method: PluginMethodName) {
if (!cls.constructor) {
return true;
}
let name = cls.constructor.name;
if (!this.pluginTriggers.has(name)) {
this.pluginTriggers.set(name, new Set());
}
let items = this.pluginTriggers.get(name);
if (!items.has(method)) {
items.add(method);
return true;
}
return false;
}
}