fuse-box
Version:
Fuse-Box a bundler that does it right
649 lines (588 loc) • 18.1 kB
text/typescript
/**
* This whole file is wrapped in a function by our gulpfile.js
* The function is injected the global `this` as `__root__`
**/
const $isBrowser = typeof window !== "undefined" && window.navigator;
const g = $isBrowser ? window : global;
declare let __root__: any;
declare let __fbx__dnm__: any;
/**
* Package name to version
*/
type PackageVersions = {
[pkg: string]: /** version e.g. `1.0.0`` */string
};
/**
* Holds the details for a loaded package
*/
type PackageDetails = {
/** Holds the package scope */
s: {
entry?: string
file?: any
},
/** Holds package files */
f: {
[name: string]: {
fn: Function
/** Locals if any */
locals?: any
}
},
v: PackageVersions,
};
/**
* A runtime storage for FuseBox
*/
type FSBX = {
p?: {
[packageName: string]: PackageDetails;
},
/** FuseBox events */
e?: {
"after-import"?: any;
}
};
// Patching global variable
if ($isBrowser) {
g["global"] = window;
}
// Set root
// __fbx__dnm__ is a variable that is used in dynamic imports
// In order for dynamic imports to work, we need to switch window to module.exports
__root__ = !$isBrowser || typeof __fbx__dnm__ !== "undefined" ? module.exports : __root__;
/**
* A runtime storage for FuseBox
*/
const $fsbx: FSBX = $isBrowser ? (window["__fsbx__"] = window["__fsbx__"] || {})
: g["$fsbx"] = g["$fsbx"] || {}; // in case of nodejs
if (!$isBrowser) {
g["require"] = require;
}
/**
* All packages are here
* Used to reference to the outside world
*/
const $packages = $fsbx.p = $fsbx.p || {};
// A list of custom events
// For example "after-import"
const $events = $fsbx.e = $fsbx.e || {};
/**
* Reference interface
* Contain information about user import;
* Having FuseBox.import("./foo/bar") makes analysis on the string
* Detects if it's package or not, explicit references are given as well
*/
interface IReference {
file?: any;
/**
* serverReference is a result of nodejs require statement
* In case if module is not in a bundle
*/
server?: string;
/** Current package name */
pkgName?: string;
/** Custom version to take into a consideration */
versions?: any;
/** User path */
filePath?: string;
/**
* Converted valid path (with extension)
* That can be recognized by FuseBox
*/
validPath?: string;
/** Require with wildcards (e.g import("/lib/*")) */
wildcard?: string;
}
/**
* $getNodeModuleName
* Considers a partial request
* for Example
* require("lodash/dist/hello")
*/
function $getNodeModuleName(name: string) {
const n = name.charCodeAt(0);
const s = name.charCodeAt(1);
// basically a hack for windows to stop recognising
// c:\ as a valid node module
if (!$isBrowser && s === 58) {
return;
}
// https://www.cambiaresearch.com/articles/15/javascript-char-codes-key-codes
// basically lowcase alphabet starts with 97 ends with 122, and symbol @ is 64
// which 2x faster than /^([@a-z].*)$/
if (n >= 97 && n <= 122 || n === 64) {
if (n === 64) { // if it's "@" symbol
let s = name.split("/");
let target = s.splice(2, s.length).join("/");
return [`${s[0]}/${s[1]}`, target || undefined];
}
// this approach is 3x - 4x faster than
// name.split(/\/(.+)?/);
let index = name.indexOf("/");
if (index === -1) {
return [name];
}
let first = name.substring(0, index);
let second = name.substring(index + 1);
return [first, second];
}
};
/** Gets file directory */
function $getDir(filePath: string) {
return filePath.substring(0, filePath.lastIndexOf("/")) || "./";
};
/**
* Joins paths
* Works like nodejs path.join
*/
function $pathJoin(...string: string[]): string {
let parts: string[] = [];
for (let i = 0, l = arguments.length; i < l; i++) {
parts = parts.concat(arguments[i].split("/"));
}
let newParts = [];
for (let i = 0, l = parts.length; i < l; i++) {
let part = parts[i];
if (!part || part === ".") continue;
if (part === "..") {
newParts.pop();
} else {
newParts.push(part);
}
}
if (parts[0] === "") newParts.unshift("");
return newParts.join("/") || (newParts.length ? "/" : ".");
};
/**
* Adds javascript extension if no extension was spotted
*/
function $ensureExtension(name: string): string {
let matched = name.match(/\.(\w{1,})$/);
if (matched) {
// @NOTE: matched [1] is the `ext` we are looking for
//
// Adding extension if none was found
// Might ignore the case of weird convention like this:
// modules/core.object.define (core-js)
// Will be handled differently afterwards
if (!matched[1]) {
return name + ".js";
}
return name;
}
return name + ".js";
};
/**
* Loads a url
* inserts a script tag or a css link based on url extension
*/
function $loadURL(url: string) {
if ($isBrowser) {
let d = document;
var head = d.getElementsByTagName("head")[0];
var target;
if (/\.css$/.test(url)) {
target = d.createElement("link");
target.rel = "stylesheet";
target.type = "text/css";
target.href = url;
} else {
target = d.createElement("script");
target.type = "text/javascript";
target.src = url;
target.async = true;
}
head.insertBefore(target, head.firstChild);
}
};
/**
* Loop through an objects own keys and call a function with the key and value
*/
function $loopObjKey(obj: Object, func: Function) {
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
func(key, obj[key]);
}
}
};
function $serverRequire(path) {
return { server: require(path) };
};
type RefOpts = {
path?: string;
pkg?: string;
v?: PackageVersions;
};
function $getRef(name: string, o: RefOpts): IReference {
let basePath = o.path || "./";
let pkgName = o.pkg || "default";
let nodeModule = $getNodeModuleName(name);
if (nodeModule) {
// reset base path
basePath = "./";
pkgName = nodeModule[0];
// if custom version is detected
// We need to modify package path
// To look like pkg@1.0.0
if (o.v && o.v[pkgName]) {
pkgName = `${pkgName}@${o.v[pkgName]}`;
}
name = nodeModule[1];
}
// Tilde test
// Charcode is 2x faster
//if (/^~/.test(name)) {
if (name) {
if (name.charCodeAt(0) === 126) {
name = name.slice(2, name.length);
basePath = "./";
} else {
// check for absolute paths for nodejs
// either first one is / (47 for *nix) or second one : (58 for windows)
if (!$isBrowser && (name.charCodeAt(0) === 47 || name.charCodeAt(1) === 58)) {
return $serverRequire(name);
}
}
}
let pkg = $packages[pkgName];
if (!pkg) {
if ($isBrowser && FuseBox.target !== "electron") {
throw "Package not found " + pkgName;
} else {
// Return "real" node module
return $serverRequire(pkgName + (name ? "/" + name : ""));
}
}
name = name ? name : "./" + pkg.s.entry;
// get rid of options
// if (name.indexOf("?") > -1) {
// let paramsSplit = name.split(/\?(.+)/);
// name = paramsSplit[0];
// }
let filePath = $pathJoin(basePath, name);
// Try first adding .js if missing
let validPath = $ensureExtension(filePath);
let file = pkg.f[validPath];
let wildcard;
// Probing for wildcard
if (!file && validPath.indexOf("*") > -1) {
wildcard = validPath;
}
if (!file && !wildcard) {
// try folder index.js
validPath = $pathJoin(filePath, "/", "index.js");
file = pkg.f[validPath];
// last resort try adding .js extension
// Some libraries have a weired convention of naming file lile "foo.bar""
if (!file) {
validPath = filePath + ".js";
file = pkg.f[validPath];
}
// if file is not found STILL
// then we can try JSX
if (!file) {
// try for JSX one last time
file = pkg.f[filePath + ".jsx"];
}
if (!file) {
validPath = filePath + "/index.jsx";
file = pkg.f[validPath];
}
}
return {
file,
wildcard,
pkgName,
versions: pkg.v,
filePath,
validPath,
};
};
/**
* $async
* Async request
* Makes it possible to request files asynchronously
*/
function $async(file: string, cb: (imported?: any) => any, o: any = {}) {
if ($isBrowser) {
if (o && o.ajaxed === file) {
return console.error(file, 'does not provide a module');
}
var xmlhttp: XMLHttpRequest = new XMLHttpRequest();
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState == 4) {
if (xmlhttp.status == 200) {
let contentType = xmlhttp.getResponseHeader("Content-Type");
let content = xmlhttp.responseText;
if (/json/.test(contentType)) {
content = `module.exports = ${content}`;
} else {
if (!/javascript/.test(contentType)) {
content = `module.exports = ${JSON.stringify(content)}`;
}
}
let normalized = $pathJoin("./", file);
FuseBox.dynamic(normalized, content);
cb(FuseBox.import(file, { ajaxed: file }));
} else {
console.error(file, 'not found on request');
cb(undefined);
}
}
};
xmlhttp.open("GET", file, true);
xmlhttp.send();
} else {
if (/\.(js|json)$/.test(file)) return cb(g["require"](file));
return cb("");
}
};
/**
* Trigger events
* If a registered callback returns "false"
* We break the loop
*/
function $trigger(name: string, args: any) {
let e = $events[name];
if (e) {
for (let i in e) {
let res = e[i].apply(null, args);
if (res === false) {
return false;
}
}
;
}
};
/**
* Imports File
* With opt provided it's possible to set:
* 1) Base directory
* 2) Target package name
*/
function $import(name: string, o: any = {}) {
// Test for external URLS
// Basically : symbol can occure only at 4 and 5 position
// Cuz ":" is a not a valid symbol in filesystem
// Charcode test is 3-4 times faster than regexp
// 58 charCode is ":""
// console.log( ":".charCodeAt(0) )
// if (/^(http(s)?:|\/\/)/.test(name)) {
// return $loadURL(name);
// }
if (name.charCodeAt(4) === 58 || name.charCodeAt(5) === 58) {
return $loadURL(name);
}
let ref = $getRef(name, o);
if (ref.server) {
return ref.server;
}
let file = ref.file;
// Wild card reference
if (ref.wildcard) {
// Prepare wildcard regexp
let safeRegEx: RegExp = new RegExp(ref.wildcard
.replace(/\*/g, "@")
.replace(/[.?*+^$[\]\\(){}|-]/g, "\\$&")
.replace(/@@/g, ".*")
.replace(/@/g, "[a-z0-9$_-]+"), "i");
let pkg = $packages[ref.pkgName];
if (pkg) {
let batch = {};
for (let n in pkg.f) {
if (safeRegEx.test(n)) {
batch[n] = $import(`${ref.pkgName}/${n}`);
}
}
return batch;
}
}
if (!file) {
let asyncMode = typeof o === "function";
let processStopped = $trigger("async", [name, o]);
if (processStopped === false) {
return;
}
return $async(name, (result) => asyncMode ? o(result) : null, o);
// throw `File not found ${ref.validPath}`;
}
// pkgName
let pkg = ref.pkgName;
if (file.locals && file.locals.module) return file.locals.module.exports;
let locals: any = file.locals = {};
// @NOTE: is fuseBoxDirname
const path = $getDir(ref.validPath);
locals.exports = {};
locals.module = { exports: locals.exports };
locals.require = (name: string, optionalCallback: any) => {
return $import(name, {
pkg,
path,
v: ref.versions,
});
};
if ($isBrowser || !g["require"].main) {
locals.require.main = { filename: "./", paths: [] };
} else {
locals.require.main = g["require"].main;
}
let args = [locals.module.exports, locals.require, locals.module, ref.validPath, path, pkg];
$trigger("before-import", args);
file.fn.apply(0, args);
// fn(locals.module.exports, locals.require, locals.module, validPath, fuseBoxDirname, pkgName)
$trigger("after-import", args);
return locals.module.exports;
};
type SourceChangedEvent = {
type: "js" | "css",
content: string,
path: string
};
interface LoaderPlugin {
/**
* If true is returned by the plugin
* it means that module change has been handled
* by plugin and no special work is needed by FuseBox
**/
hmrUpdate?(evt: SourceChangedEvent): boolean;
}
/**
* The FuseBox client side loader API
*/
class FuseBox {
public static packages = $packages;
public static mainFile: string;
public static isBrowser = $isBrowser;
public static isServer = !$isBrowser;
public static global(key: string, obj?: any) {
if (obj === undefined) return g[key];
g[key] = obj;
}
/**
* Imports a module
*/
public static import(name: string, o?: any) {
return $import(name, o);
}
/**
* @param {string} n name
* @param {any} fn [description]
* @return void
*/
public static on(n: string, fn: any) {
$events[n] = $events[n] || [];
$events[n].push(fn);
}
/**
* Check if a file exists in path
*/
public static exists(path: string) {
try {
let ref = $getRef(path, {});
return ref.file !== undefined;
}
catch (err) {
return false;
}
}
/**
* Removes a module
*/
public static remove(path: string) {
let ref = $getRef(path, {});
let pkg = $packages[ref.pkgName];
if (pkg && pkg.f[ref.validPath]) {
delete pkg.f[ref.validPath];
}
}
public static main(name: string) {
this.mainFile = name;
return FuseBox.import(name, {});
}
public static expose(obj: any) {
for (let k in obj) {
let alias = obj[k].alias;
let xp = $import(obj[k].pkg);
if (alias === "*") {
$loopObjKey(xp, (exportKey, value) => __root__[exportKey] = value);
} else if (typeof alias === "object") {
$loopObjKey(alias, (exportKey, value) => __root__[value] = xp[exportKey]);
} else {
__root__[alias] = xp;
}
}
}
/**
* Registers a dynamic path
*
* @param str a function that is invoked with
* - `true, exports,require,module,__filename,__dirname,__root__`
*/
public static dynamic(path: string, str: string, opts?: {
/** The name of the package */
pkg: string
}) {
this.pkg(opts && opts.pkg || "default", {}, function (___scope___: any) {
___scope___.file(path, function (exports: any, require: any, module: any, __filename: string, __dirname: string) {
var res = new Function("__fbx__dnm__", "exports", "require", "module", "__filename", "__dirname", "__root__", str);
res(true, exports, require, module, __filename, __dirname, __root__);
});
});
}
/**
* Flushes the cache for the default package
* @param shouldFlush you get to chose if a particular file should be flushed from cache
*/
public static flush(
shouldFlush?: (fileName: string) => boolean
) {
let def = $packages["default"];
for (let fileName in def.f) {
if (!shouldFlush || shouldFlush(fileName)) {
delete def.f[fileName].locals;
}
}
}
/**
*
* Register a package
*/
public static pkg(name: string, v: PackageVersions, fn: Function) {
// Let's not register a package scope twice
if ($packages[name]) return fn($packages[name].s);
// create new package
let pkg = $packages[name] = {} as PackageDetails;
// file
pkg.f = {};
// storing v
pkg.v = v;
// scope
pkg.s = {
// Scope file
file: (name: string, fn: any) => pkg.f[name] = { fn },
};
return fn(pkg.s);
}
public static target: string;
/**
* Loader plugins
*/
public static plugins: LoaderPlugin[] = [];
/** Adds a Loader plugin */
public static addPlugin(plugin: LoaderPlugin) {
this.plugins.push(plugin);
}
}
if (!$isBrowser) {
g["FuseBox"] = FuseBox;
}
/**
* Injected into the global namespace by the fsbx-default-css-plugin
* Generates a tag with an `id` based on `__filename`
* If you call it it again with the same file name the same tag is patched
* @param __filename the name of the source file
* @param contents if provided creates a style tag
* otherwise __filename is added as a link tag
**/
declare var __fsbx_css: { (__filename: string, contents?: string): void };