fuse-box
Version:
Fuse-Box a bundler that does it right
462 lines (403 loc) • 15.7 kB
text/typescript
import { IPackageInformation, IPathInformation } from "./PathMaster";
import { WorkFlowContext } from "./WorkflowContext";
import { ensurePublicExtension } from "../Utils";
import { Config } from "../Config";
import * as path from "path";
import * as fs from "fs";
import { BundleData } from "../arithmetic/Arithmetic";
/**
* If a import url isn't relative
* and only has ascii + @ in the name it is considered a node module
*/
const NODE_MODULE = /^([a-z@](?!:).*)$/;
export interface INodeModuleRequire {
name: string;
fuseBoxPath?: string;
target?: string;
}
export interface IPathInformation {
fuseBoxAlias?: string;
isRemoteFile?: boolean;
remoteURL?: string;
isNodeModule: boolean;
nodeModuleName?: string;
nodeModuleInfo?: IPackageInformation;
nodeModuleExplicitOriginal?: string;
absDir?: string;
fuseBoxPath?: string;
params?: Map<string, string>;
absPath?: string;
}
export interface IPackageInformation {
name: string;
missing?: boolean;
bundleData?: BundleData;
entry: string;
version: string;
root: string;
entryRoot: string,
custom: boolean;
browserOverrides?: any;
customBelongsTo?: string;
}
/**
* Manages the allowed extensions e.g.
* should user be allowed to do `require('./foo.ts')`
*/
export class AllowedExtenstions {
/**
* Users are allowed to require files with these extensions by default
**/
public static list: Set<string> = new Set([".js", ".ts", ".tsx", ".json", ".xml", ".css", ".html"]);
public static add(name: string) {
if (!this.list.has(name)) {
this.list.add(name);
}
}
public static has(name) {
return this.list.has(name);
}
}
/**
* PathMaster
*/
export class PathMaster {
private tsMode: boolean = false;
private fuseBoxAlias: string;
constructor(public context: WorkFlowContext, public rootPackagePath?: string) { }
public init(name: string, fuseBoxPath?: string) {
const resolved = this.resolve(name, this.rootPackagePath);
if (fuseBoxPath) {
resolved.fuseBoxPath = fuseBoxPath;
}
return resolved;
}
public setTypeScriptMode() {
this.tsMode = true;
}
public resolve(name: string, root?: string, rootEntryLimit?: string): IPathInformation {
let data = <IPathInformation>{};
if (/^(http(s)?:|\/\/)/.test(name)) {
data.isRemoteFile = true;
data.remoteURL = name;
data.absPath = name;
return data;
}
// if (/\?/.test(name)) {
// let paramsSplit = name.split(/\?(.+)/);
// name = paramsSplit[0];
// data.params = parseQuery(paramsSplit[1]);
// }
data.isNodeModule = NODE_MODULE.test(name);
if (data.isNodeModule) {
let info = this.getNodeModuleInfo(name);
data.nodeModuleName = info.name;
// A trick to avoid one nasty situation
// Imagine lodash@1.0.0 that is set as a custom depedency for 2 libraries
// We need to make sure there, that we use one source (either or)
// We don't want to take modules from 2 different places (in case if versions match)
let nodeModuleInfo = this.getNodeModuleInformation(info.name);
let cachedInfo = this.context.getLibInfo(nodeModuleInfo.name, nodeModuleInfo.version);
if (cachedInfo) { // Modules has been defined already
data.nodeModuleInfo = cachedInfo;
} else {
data.nodeModuleInfo = nodeModuleInfo; // First time that module is mentioned
// Caching module information
// Which will override paths
this.context.setLibInfo(nodeModuleInfo.name, nodeModuleInfo.version, nodeModuleInfo);
}
if (info.target) {
// Explicit require from a libary e.g "lodash/dist/each" -> "dist/each"
const absPath = this.getAbsolutePath(info.target, data.nodeModuleInfo.root, undefined, true);
if (absPath.alias) {
data.fuseBoxAlias = absPath.alias;
}
data.absPath = absPath.resolved;
data.absDir = path.dirname(data.absPath);
data.nodeModuleExplicitOriginal = info.target;
} else {
data.absPath = data.nodeModuleInfo.entry;
data.absDir = data.nodeModuleInfo.root;
}
if (data.absPath) {
data.fuseBoxPath = this.getFuseBoxPath(data.absPath, data.nodeModuleInfo.root);
}
if (this.fuseBoxAlias) {
data.fuseBoxPath = this.fuseBoxAlias;
}
} else {
if (root) {
const absPath = this.getAbsolutePath(name, root, rootEntryLimit);
if (absPath.alias) {
data.fuseBoxAlias = absPath.alias;
}
data.absPath = absPath.resolved;
data.absDir = path.dirname(data.absPath);
data.fuseBoxPath = this.getFuseBoxPath(data.absPath, this.rootPackagePath);
}
}
return data;
}
public getFuseBoxPath(name: string, root: string) {
if (!root) {
return;
}
name = name.replace(/\\/g, "/");
root = root.replace(/\\/g, "/");
name = name.replace(root, "").replace(/^\/|\\/, "");
if (this.tsMode) {
name = ensurePublicExtension(name);
}
// Some smart asses like "react-router"
// Skip .js for their main entry points.
let ext = path.extname(name);
if (!ext) {
name += ".js";
}
return name;
}
/**
* Make sure that all extensions are in place
* Returns a valid absolute path
*
* @param {string} name
* @param {string} root
* @returns
*
* @memberOf PathMaster
*/
public getAbsolutePath(name: string, root: string, rootEntryLimit?: string, explicit = false): {
resolved: string,
alias?: string
} {
const data = this.ensureFolderAndExtensions(name, root, explicit);
const url = data.resolved;
const alias = data.alias;
let result = path.resolve(root, url);
// Fixing node_modules package .json limits.
if (rootEntryLimit && name.match(/\.\.\/$/)) {
if (result.indexOf(path.dirname(rootEntryLimit)) < 0) {
return { resolved: rootEntryLimit, alias: alias };
}
}
const output = { resolved: result, alias: alias };
//RESOLUTION_CACHE.set(cacheKey, output);
return output;
}
public getParentFolderName(): string {
if (this.rootPackagePath) {
let s = this.rootPackagePath.split(/\/|\\/g);
return s[s.length - 1];
}
return "";
}
private testFolder(folder: string, name: string) {
const extensions = ["js", "jsx"];
if (this.tsMode) {
extensions.push("ts", "tsx");
}
if (fs.existsSync(folder)) {
for (let i = 0; i < extensions.length; i++) {
let ext = extensions[i];
const index = `index.${ext}`;
const target = path.join(folder, index);
if (fs.existsSync(target)) {
let result = path.join(name, index);
let startsWithDot = result[0] === "."; // After transformation we need to bring the dot back
if (startsWithDot) {
result = `./${result}`;
}
return result;
}
}
}
}
private checkFileName(root: string, name: string) {
const extensions = ["js", "jsx"];
if (this.tsMode) {
extensions.push("ts", "tsx");
}
for (let i = 0; i < extensions.length; i++) {
let ext = extensions[i];
let fileName = `${name}.${ext}`;
let target = path.isAbsolute(name) ? fileName : path.join(root, fileName);
if (fs.existsSync(target)) {
if (fileName[0] === ".") {
fileName = `./${fileName}`;
}
return fileName;
}
}
}
private ensureNodeModuleExtension(input: string) {
let ext = path.extname(input);
if (!ext) {
return input + ".js";
}
return input;
}
private ensureFolderAndExtensions(name: string, root: string, explicit = false):
{ resolved: string, alias?: string } {
let ext = path.extname(name);
let fileExt = this.tsMode && !explicit ? ".ts" : ".js";
if (name[0] === "~" && name[1] === "/" && this.rootPackagePath) {
name = "." + name.slice(1, name.length);
name = path.join(this.rootPackagePath, name);
}
if (explicit) {
if (!ext) {
// handle cases with
// require("@angular/platform-browser/animations");
// where animation contains package.json pointing to a different file
const folderJsonPath = path.join(root, name, "package.json");
if (fs.existsSync(folderJsonPath)) {
const folderJSON = require(folderJsonPath);
if (folderJSON.main) {
return {
resolved: path.resolve(root, name, folderJSON.main),
alias: this.ensureNodeModuleExtension(name)
}
}
}
}
}
if (!AllowedExtenstions.has(ext)) {
let folder = path.isAbsolute(name) ? name : path.join(root, name);
const folderPath = this.testFolder(folder, name);
if (folderPath) {
return { resolved: folderPath }
} else {
let fileNameCheck = this.checkFileName(root, name);
if (fileNameCheck) {
return { resolved: fileNameCheck };
} else {
name += fileExt;
}
}
}
return { resolved: name };
}
private getNodeModuleInfo(name: string): INodeModuleRequire {
// Handle scope requires
if (name[0] === "@") {
let s = name.split("/");
let target = s.splice(2, s.length).join("/");
let fuseBoxPath;
if (target) {
fuseBoxPath = this.ensureNodeModuleExtension(target);
}
return {
name: `${s[0]}/${s[1]}`,
target: target
};
}
let data = name.split(/\/(.+)?/);
return {
name: data[0],
target: data[1],
};
}
private getNodeModuleInformation(name: string): IPackageInformation {
const readMainFile = (folder, isCustom: boolean) => {
// package.json path
const packageJSONPath = path.join(folder, "package.json");
if (fs.existsSync(packageJSONPath)) {
// read contents
const json: any = require(packageJSONPath);
// Getting an entry point
let entryFile;
let entryRoot;
let browserOverrides;
if (json.browser && !this.context.isBrowserTarget()) {
this.context.fuse.producer.addWarning("json.browser",
`Library "${name}" contains "browser" field. Set .target("browser") to avoid problems with your browser build!`);
}
if (this.context.isBrowserTarget() && json.browser) {
if (typeof json.browser === "object") {
browserOverrides = json.browser;
if (json.browser[json.main]) {
entryFile = json.browser[json.main];
}
}
if (typeof json.browser === "string") {
entryFile = json.browser;
}
}
if (this.context.rollupOptions && json["jsnext:main"]) {
entryFile = path.join(folder, json["jsnext:main"]);
} else {
// if (json.module) {
// entryFile = path.join(folder, entryFile || json.module || "index.js");
// } else {
entryFile = path.join(folder, entryFile || json.main || "index.js");
//}
entryRoot = path.dirname(entryFile);
}
return {
browserOverrides: browserOverrides,
name,
custom: isCustom,
root: folder,
missing: false,
entryRoot,
entry: entryFile,
version: json.version,
};
}
let defaultEntry = path.join(folder, "index.js");
let entryFile = fs.existsSync(defaultEntry) ? defaultEntry : undefined;
let defaultEntryRoot = entryFile ? path.dirname(entryFile) : undefined;
let packageExists = fs.existsSync(folder);
return {
name,
missing: !packageExists,
custom: isCustom,
root: folder,
entry: entryFile,
entryRoot: defaultEntryRoot,
version: "0.0.0",
};
};
let localLib = path.join(Config.FUSEBOX_MODULES, name);
let modulePath = path.join(Config.NODE_MODULES_DIR, name);
// check for custom shared packages
const producer = this.context.bundle && this.context.bundle.producer
if (producer && producer.isShared(name)) {
let shared = producer.getSharedPackage(name);
return {
name,
custom: false,
bundleData: shared.data,
root: shared.homeDir,
entry: shared.mainPath,
entryRoot: shared.mainDir,
version: "0.0.0",
}
}
if (this.context.customModulesFolder) {
let customFolder = path.join(this.context.customModulesFolder, name);
if (fs.existsSync(customFolder)) {
return readMainFile(customFolder, false);
}
}
if (fs.existsSync(localLib)) {
return readMainFile(localLib, false);
}
// handle a conflicting library
if (this.rootPackagePath) {
let nodeModules = path.join(this.rootPackagePath, "node_modules");
let nestedNodeModule = path.join(nodeModules, name);
if (fs.existsSync(nestedNodeModule)) {
return readMainFile(nestedNodeModule, nodeModules !== Config.NODE_MODULES_DIR);
} else {
// climb up (sometimes it can be in a parent)
let upperNodeModule = path.join(this.rootPackagePath, "../", name);
if (fs.existsSync(upperNodeModule)) {
let isCustom = path.dirname(this.rootPackagePath) !== Config.NODE_MODULES_DIR;
return readMainFile(upperNodeModule, isCustom);
}
}
}
return readMainFile(modulePath, false);
}
}