Skip to content

Instantly share code, notes, and snippets.

@creationix
Last active December 27, 2015 19:29
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save creationix/7377834 to your computer and use it in GitHub Desktop.
Save creationix/7377834 to your computer and use it in GitHub Desktop.
Requirements for module compiler for browser envs.
var mine = require('./mine.js');
var join = require('./pathjoin.js');
// A simple function to get the dirname of a path
// Trailing slashes are ignored. Leading slash is preserved.
function dirname(path) {
return join(path, "..");
}
function compile(loader, initial, callback) {
var modules = {}; // compiled modules
var packagePaths = {}; // key is base + name , value is full path
var aliases = {}; // path aliases from the "browser" directive in package.json
resolveModule(".", initial, function (err, newPath) {
if (err) return callback(err);
callback(null, {
initial: newPath,
modules: modules
});
});
function resolvePath(path, callback) {
if (path in aliases) path = aliases[path];
if (path in modules) return callback(null, path);
if (/\.js$/.test(path)) {
return loader(path, false, onJavaScript);
}
if (/\.json$/.test(path)) {
return loader(path, false, onJson);
}
if (/#txt$/.test(path)) {
return loader(path.substr(0, path.length - 4), false, onText);
}
if (/#bin$/.test(path)) {
return loader(path.substr(0, path.length - 4), true, onBinary);
}
return callback(new Error("Invalid path extension: " + path));
function onJavaScript(err, js) {
if (err) return callback(err);
var deps = mine(js);
modules[path] = { type: "javascript", value: js, deps: deps };
next(0);
function next(index) {
var dep = deps[index];
if (!dep) return callback(null, path);
resolveModule(dirname(path), dep.name, function (err, newPath) {
if (err) return callback(err);
dep.newPath = newPath;
next(index + 1);
});
}
}
function onJson(err, json) {
if (json === undefined) return callback(err);
var value;
try { value = JSON.parse(json); }
catch (err) { return callback(err); }
modules[path] = { type: "json", value: value };
callback(null, path);
}
function onText(err, text) {
if (text === undefined) return callback(err);
modules[path] = { type: "text", value: text };
callback(null, path);
}
function onBinary(err, binary) {
if (binary === undefined) return callback(err);
modules[path] = { type: "binary", value: binary };
callback(null, path);
}
}
function resolveModule(base, path, callback) {
if (path[0] === ".") {
return resolvePath(join(base, path), callback);
}
// non-local requires are assumed to belong to packages
var index = path.indexOf("/");
var name = index < 0 ? path : path.substr(0, index);
return loadPackage(base, name, onPackage);
function onPackage(err, metaPath) {
if (metaPath === undefined) return callback(err);
if (index < 0) path = metaPath;
else path = join(metaPath, path.substr(index));
return resolvePath(path, callback);
}
}
function loadPackage(base, name, callback) {
var key = join(base, name);
if (key in packagePaths) return callback(null, packagePaths[key]);
var metaPath = join(base, "node_modules", name, "package.json");
loader(metaPath, false, function (err, json) {
if (err) return callback(err);
if (!json) {
if (base === "/" || base === ".") return callback();
return loadPackage(dirname(base), name, callback);
}
var meta;
try { meta = JSON.parse(json); }
catch (err) { return callback(err); }
base = dirname(metaPath);
packagePaths[key] = base;
if (meta.main) {
aliases[base] = join(base, meta.main);
}
if (meta.browser) {
for (var original in meta.browser) {
aliases[join(base, original)] = join(base, meta.browser[original]);
}
}
callback(null, base);
});
}
}

Modules are written in common.js format. This means they have the following syntax available to them:

require('package-name'); // Load a package by name using node_modules style path lookup (will resolve to the package's "main")
require('package-name/module.js'); // Load a specefic module within a node_modules style package
require('./module.js'); // Load a module in the current package relative to the calling file

require('./data.json'); // Load a JSON file as data.  The expression will be the JSON file already parsed
require('./some-file.css#txt'); // Load an arbitrary file as a unicode string assuming UTF-8 in the original file.
require('./some-file.png#bin'); // Load an arbitrary file as a "binary" encoded string

// These all also work as module sub-paths
require('package-name/data.json');
require('package-name/some-file.css#txt');
require('package-name/some-file.png#bin');

exports.foo = 42; // Export a single value on the exports object
module.exports = 42; // Replace the entire exports object with a new value.

I don't support the following styles

require(random + expression); // Dynamic package paths are not allowed
require('./package-name'); // Relative package requires are not allowed either

Inside package.json, I support "main", and "browser". The "main" directive creates an alias for the package itself to route to a specefic file. The "browser" directive is a hash of old->new path aliases within the package. This is commonly used for multi-platform packages with code optimized for browser-style envirionments.

// Mine a string for require calls and export the module names
// Extract all require calls using a proper state-machine parser.
module.exports = mine;
function mine(js) {
var names = [];
var state = 0;
var ident;
var quote;
var name;
var start;
var isIdent = /[a-z0-9_.]/i;
var isWhitespace = /[ \r\n\t]/;
function $start(char) {
if (char === "/") {
return $slash;
}
if (char === "'" || char === '"') {
quote = char;
return $string;
}
if (isIdent.test(char)) {
ident = char;
return $ident;
}
return $start;
}
function $ident(char) {
if (isIdent.test(char)) {
ident += char;
return $ident;
}
if (char === "(" && ident === "require") {
ident = undefined;
return $call;
}
return $start(char);
}
function $call(char) {
if (isWhitespace.test(char)) return $call;
if (char === "'" || char === '"') {
quote = char;
name = "";
start = i + 1;
return $name;
}
return $start(char);
}
function $name(char) {
if (char === quote) {
return $close;
}
name += char;
return $name;
}
function $close(char) {
if (isWhitespace.test(char)) return $close;
if (char === ")" || char === ',') {
names.push({
name: name,
offset: start
});
}
name = undefined;
return $start(char);
}
function $string(char) {
if (char === "\\") {
return $escape;
}
if (char === quote) {
return $start;
}
return $string;
}
function $escape(char) {
return $string;
}
function $slash(char) {
if (char === "/") return $lineComment;
if (char === "*") return $multilineComment;
return $start(char);
}
function $lineComment(char) {
if (char === "\r" || char === "\n") return $start;
return $lineComment;
}
function $multilineComment(char) {
if (char === "*") return $multilineEnding;
return $multilineComment;
}
function $multilineEnding(char) {
if (char === "/") return $start;
if (char === "*") return $multilineEnding;
return $multilineComment;
}
var state = $start;
for (var i = 0, l = js.length; i < l; i++) {
state = state(js[i]);
}
return names;
}
// Joins path segments. Preserves initial "/" and resolves ".." and "."
// Does not support using ".." to go above/outside the root.
// This means that join("foo", "../../bar") will not resolve to "../bar"
module.exports = join;
function join(/* path segments */) {
// Split the inputs into a list of path commands.
var parts = [];
for (var i = 0, l = arguments.length; i < l; i++) {
parts = parts.concat(arguments[i].split("/"));
}
// Interpret the path commands to get the new resolved path.
var newParts = [];
for (i = 0, l = parts.length; i < l; i++) {
var part = parts[i];
// Remove leading and trailing slashes
// Also remove "." segments
if (!part || part === ".") continue;
// Interpret ".." to pop the last segment
if (part === "..") newParts.pop();
// Push new path segments.
else newParts.push(part);
}
// Preserve the initial slash if there was one.
if (parts[0] === "") newParts.unshift("");
// Turn back into a single string path.
return newParts.join("/") || (newParts.length ? "/" : ".");
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment