Skip to content

Instantly share code, notes, and snippets.

@futurist
Created December 2, 2015 10:19
Show Gist options
  • Save futurist/109ea590d7d2f64d8278 to your computer and use it in GitHub Desktop.
Save futurist/109ea590d7d2f64d8278 to your computer and use it in GitHub Desktop.
Mini module system for the browser with hot swapping and remote loading support, 750 bytes minified+gzipped
/**
* This file contains a primitive JS/resource loader and resolver that can load
* both local and remote files.
*
* API usage:
*
* ```js
* r.define("foo", function () { return "default export" })
*
* r.define("assert", function () {
* return function assert(condition, message) {
* if (!condition) throw new Error(message)
* }
* })
*
* r.define("main", function (require) {
* var assert = require("assert")
*
* assert(require("foo") === "default export",
* "default exports are read correctly")
*
* r.load("page/base", "/page.js", function (err, BaseComponent) {
* if (err != null) return displayError(err)
* renderComponent(document.getElementById("body"), BaseComponent)
* })
*
* r.redefine("assert", function () {
* return function assert() {
* return true
* }
* })
* })
* ```
*/
!(function (r, window) { // eslint-disable-line
"use strict"
// Object.create(null) is used to avoid read-only Object.prototype methods
var cache = Object.create(null)
var factories = Object.create(null)
function init(name) {
var res = factories[name](require, cache[name])
if (res != null) cache[name] = res
delete factories[name]
}
function require(name) {
if (name in factories) init(name)
return cache[name]
}
// Normalize the path, resolving . and .. elements. It retains trailing
// slashes.
function resolve(path) {
var res = []
path.split("/").map(function (p) {
if (p === "..") {
// Don't go above the root - this is a noop if `res` is empty
res.pop()
} else if (p && p !== ".") {
// Ignore empty parts
res.push(p)
}
})
return "/" + res.join("/") + (path.slice(-1) === "/" ? "/" : "")
}
function load(name, callback, errback) {
var el = document.createElement("script")
el.async = el.defer = true
el.src = name
el.onload = function (e) {
e.preventDefault()
e.stopPropogation()
// Remove the node after it loads.
document.body.removeChild(el)
callback()
}
el.onerror = function (e) {
e.preventDefault()
e.stopPropogation()
// Remove the node after it loads.
document.body.removeChild(el)
var xhr = new XMLHttpRequest()
xhr.open("GET", name.replace(/\..*?\/?$/, "/modules.json"))
xhr.onreadystatechange = function (data, len) {
if (this.readyState === 4) {
if (!(len = Object.keys(data = this.response)
.map(function (key, value) {
if (typeof value !== "string") {
throw new TypeError("Expected key " + key + " in " +
name.replace(/\..*?\/?$/, "/modules.json") +
" to be a string, but found " + data[key])
}
return load(resolve(name + "/" + data[key]),
function () {
if (len && --len) callback()
},
function (err) {
if (len) errback((len = 0, err))
})
}).length)) {
callback()
}
}
}
xhr.onerror = function (e) {
errback(new Error(e.type))
}
xhr.send()
}
document.body.appendChild(el)
}
r.define = function (name, impl) {
if (name in cache) {
throw new TypeError("Module already defined: " + name)
}
if (typeof impl !== "function") {
throw new TypeError("Expected body to be a function")
}
cache[name] = {}
factories[name] = impl
// For convenience
if (name === "main") {
setTimeout(function () {
init(name)
})
}
}
r.redefine = function (name, impl) {
if (typeof impl !== "function") {
throw new TypeError("Expected body to be a function")
}
cache[name] = {}
factories[name] = impl
setTimeout(function () {
init(name)
})
}
/*
* Note that this assumes JavaScript
*/
r.load = function (ns, name, callback) {
if (typeof type === "function") {
callback = name
name = ns
ns = null
}
load(resolve(name), function () {
if (ns == null) return callback()
return callback(null, require(ns))
}, callback)
}
})(window.r = {}, window)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment