UNPKG

fuse-box

Version:

Fuse-Box a bundler that does it right

535 lines (476 loc) • 19 kB
import { IPackageInformation, IPathInformation } from "./PathMaster"; import { WorkFlowContext } from "./WorkflowContext"; import { ensurePublicExtension, ensureFuseBoxPath } from "../Utils"; import { Config } from "../Config"; import * as path from "path"; import * as fs from "fs"; import { BundleData } from "../arithmetic/Arithmetic"; import { File } from "./File"; /** * 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@](?!:).*)$/; const isRelative = /^[\.\/\\]+$/ const jsExtensions = ['js', 'jsx']; const tsExtensions = jsExtensions.concat(['ts', 'tsx']); export interface INodeModuleRequire { name: string; fuseBoxPath?: string; target?: string; } export interface IPathInformation { fuseBoxAlias?: string; overrideStatement?: { key: string, value: string } isRemoteFile?: boolean; remoteURL?: string; tsMode?: boolean; 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; jsNext?: boolean; 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", ".jsx", ".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, parent?: File): 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) { // console.log(name, nodeModuleInfo); // nodeModuleInfo.browserOverrides = {} // nodeModuleInfo.browserOverrides[name] = info.name + "/" + absPath.alias; if (parent) { parent.analysis. replaceAliases(new Set([{from : name, to : info.name + "/" + 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; if (parent) { parent.analysis. replaceAliases(new Set([{from : name, to : `~/` + absPath.alias }])) } } data.absPath = absPath.resolved; data.absDir = path.dirname(data.absPath); data.fuseBoxPath = this.getFuseBoxPath(data.absPath, this.rootPackagePath); if (path.relative(this.rootPackagePath, data.absPath).match(/^\.\.(\\|\/)/)) { this.context.fuse.producer.addWarning('unresolved', `File "${data.absPath}" cannot be bundled because it's out of your project directory (homeDir)`); } } } if (data.fuseBoxAlias) { data.overrideStatement = { key: name, value: data.fuseBoxAlias } } return data; } public getFuseBoxPath(name: string, root: string) { if (!root) { return; } name = name.replace(/\\/g, "/"); root = root.replace(/\\/g, "/"); name = name.replace(root, "").replace(/^\/|\\/, ""); 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) { let extensions = jsExtensions; if (this.tsMode) { extensions = tsExtensions } if (fs.existsSync(folder)) { for (let i = 0; i < extensions.length; i++) { const index = "index." + extensions[i] if (fs.existsSync(path.join(folder, index))) { const result = path.join(name, index); const [a, b] = name if (a === "." && b !== ".") { //add relative './' from `name`, back onto joined path return "./" + result; } return result; } } } } private checkFileName(root: string, name: string) { let extensions = jsExtensions; if (this.tsMode) { extensions = tsExtensions; } 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 && !isRelative.test(input)) { return input + ".js"; } return input; } private extractRelativeFuseBoxPath(root: string, target: string): string { root = ensureFuseBoxPath(root) target = ensureFuseBoxPath(target); const sResult = target.split(root) if (sResult[1]) { let fusePath = sResult[1]; if (fusePath[0] === "/") { fusePath = fusePath.slice(1) } return this.ensureNodeModuleExtension(fusePath); } } private ensureFolderAndExtensions(name: string, root: string, explicit = false): { resolved: string, alias?: string } { let ext = path.extname(name); if (ext === ".ts") { this.tsMode = true; } 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 (!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"); //1 if (fs.existsSync(folderJsonPath)) { const folderJSON = require(folderJsonPath); if (folderJSON.main) { const resolved = path.resolve(root, name, folderJSON.main); const opts = { resolved: resolved, alias: this.extractRelativeFuseBoxPath(root, resolved) } return opts; } } } if (!AllowedExtenstions.has(ext)) { let fileNameCheck = this.checkFileName(root, name); if (fileNameCheck) { return { resolved: fileNameCheck }; } else { let folder = path.isAbsolute(name) ? name : path.join(root, name); const folderPath = this.testFolder(folder, name); if (folderPath) { return { resolved: folderPath } } else { name += fileExt; return { resolved: name }; } } } 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 fixBrowserOverrides(browserOverrides: { [key: string]: string }): { [key: string]: string } { let newOverrides = {}; for (let key in browserOverrides) { let value = browserOverrides[key]; if (typeof value === "string") { if (/\.\//.test(key)) { key = key.slice(2); } if (/\.\//.test(value)) { value = "~/" + value.slice(2); } else { value = "~/" + value; } if (!/.js$/.test(value)) { value = value + ".js"; } } newOverrides[key] = value; } return newOverrides; } 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 jsNext = false; let browserOverrides; if (this.context.target !== "server") { 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 = this.fixBrowserOverrides(json.browser); if (json.browser[json.main]) { entryFile = json.browser[json.main]; } } if (typeof json.browser === "string") { entryFile = json.browser; } } if (this.context.shouldUseJsNext(name) && (json["jsnext:main"] || json.module)) { jsNext = true; entryFile = path.join(folder, json["jsnext:main"] || json.module); } else { entryFile = path.join(folder, entryFile || json.main || "index.js"); } if (json["ts:main"]) { entryFile = json["ts:main"]; if (entryFile[0] !== ".") { // safety check to avoid consfusion with node_module entryFile = `./${entryFile}` } } entryRoot = path.dirname(entryFile); const ext = path.extname(entryFile); return { browserOverrides: browserOverrides, name, tsMode: ext === ".ts", jsNext, custom: isCustom, root: folder, missing: false, entryRoot, entry: entryFile, version: json.version || "1.0.0", }; } 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 (path.dirname(upperNodeModule) !== Config.NODE_MODULES_DIR) { if (fs.existsSync(upperNodeModule)) { let isCustom = false; if (path.dirname(upperNodeModule).match(/node_modules$/)) { isCustom = path.dirname(this.rootPackagePath) !== Config.NODE_MODULES_DIR; return readMainFile(upperNodeModule, isCustom); } } } } } return readMainFile(modulePath, false); } }