system
Version:
Flexible module and resource system
734 lines (653 loc) • 23.9 kB
JavaScript
/*eslint no-console:[0]*/
/*global console*/
"use strict";
var URL = require("./url");
var Identifier = require("./identifier");
var Module = require("./module");
var Resource = require("./resource");
var parseDependencies = require("./parse-dependencies");
var compile = require("./compile");
var has = Object.prototype.hasOwnProperty;
module.exports = System;
function System(location, description, options) {
var self = this;
options = options || {};
description = description || {};
self.name = options.name || description.name || "";
self.location = location;
self.description = description;
self.dependencies = {};
self.main = null;
self.resources = options.resources || {}; // by system.name / module.id
self.modules = options.modules || {}; // by system.name/module.id
self.systemLocations = options.systemLocations || {}; // by system.name;
self.systems = options.systems || {}; // by system.name
self.systemLoadedPromises = options.systemLoadedPromises || {}; // by system.name
self.buildSystem = options.buildSystem; // or self if undefined
self.strategy = options.strategy || "nested";
self.analyzers = {js: self.analyzeJavaScript};
self.translators = {json: self.translateJson};
self.internalRedirects = {};
self.externalRedirects = {};
self.node = !!options.node;
self.browser = !!options.browser;
self.parent = options.parent;
self.root = options.root || self;
// TODO options.optimize
// TODO options.instrument
self.systems[self.name] = self;
self.systemLocations[self.name] = self.location;
self.systemLoadedPromises[self.name] = Promise.resolve(self);
if (options.name != null && description.name == null) {
console.warn(
"Package loaded by name " + JSON.stringify(options.name) +
" has no name"
);
} else if (options.name != null && options.name !== description.name) {
console.warn(
"Package loaded by name " + JSON.stringify(options.name) +
" has mismatched name " + JSON.stringify(description.name)
);
}
// The main property of the description can only create an internal
// redirect, as such it normalizes absolute identifiers to relative.
// All other redirects, whether from internal or external identifiers, can
// redirect to either internal or external identifiers.
self.main = description.main || "index.js";
self.internalRedirects[".js"] = "./" + Identifier.resolve(self.main, "");
// Overlays:
if (options.browser) { self.overlayBrowser(description); }
if (options.node) { self.overlayNode(description); }
// Dependencies:
if (description.dependencies) {
self.addDependencies(description.dependencies);
}
if (self.root === self && description.devDependencies) {
self.addDependencies(description.devDependencies);
}
// Local per-extension overrides:
if (description.extensions) { self.addExtensions(description.extensions); }
if (description.redirects) { self.addRedirects(description.redirects); }
}
System.load = function loadSystem(location, options) {
var self = this;
return self.prototype.loadSystemDescription(location, "<anonymous>")
.then(function (description) {
return new self(location, description, options);
});
};
System.prototype.import = function importModule(rel, abs) {
var self = this;
return self.load(rel, abs)
.then(function onModuleLoaded() {
self.root.main = self.lookup(rel, abs);
return self.require(rel, abs);
});
};
// system.require(rel, abs) must be called only after the module and its
// transitive dependencies have been loaded, as guaranteed by system.load(rel,
// abs)
System.prototype.require = function require(rel, abs) {
var self = this;
// Apart from resolving relative identifiers, this also normalizes absolute
// identifiers.
var res = Identifier.resolve(rel, abs);
if (Identifier.isAbsolute(rel)) {
if (self.externalRedirects[res] === false) {
return {};
}
if (self.externalRedirects[res]) {
return self.require(self.externalRedirects[res], res);
}
var head = Identifier.head(rel);
var tail = Identifier.tail(rel);
if (self.dependencies[head]) {
return self.getSystem(head, abs).requireInternalModule(tail, abs);
} else if (self.modules[head]) {
return self.requireInternalModule(rel, abs, self.modules[rel]);
} else {
var via = abs ? " via " + JSON.stringify(abs) : "";
throw new Error("Can't require " + JSON.stringify(rel) + via + " in " + JSON.stringify(self.name));
}
} else {
return self.requireInternalModule(rel, abs);
}
};
System.prototype.requireInternalModule = function requireInternalModule(rel, abs, module) {
var self = this;
var res = Identifier.resolve(rel, abs);
var id = self.normalizeIdentifier(res);
if (self.internalRedirects[id]) {
return self.require(self.internalRedirects[id], id);
}
module = module || self.lookupInternalModule(id);
// check for load error
if (module.error) {
var error = module.error;
var via = abs ? " via " + JSON.stringify(abs) : "";
error.message = (
"Can't require module " + JSON.stringify(module.id) +
via +
" in " + JSON.stringify(self.name || self.location) +
" because " + error.message
);
throw error;
}
// do not reinitialize modules
if (module.exports != null) {
return module.exports;
}
// do not initialize modules that do not define a factory function
if (typeof module.factory !== "function") {
throw new Error(
"Can't require module " + JSON.stringify(module.filename) +
". No exports. No exports factory."
);
}
module.require = self.makeRequire(module.id, self.root.main);
module.exports = {};
// Execute the factory function:
module.factory.call(
// in the context of the module:
null, // this (defaults to global, except in strict mode)
module.require,
module.exports,
module,
module.filename,
module.dirname
);
return module.exports;
};
System.prototype.makeRequire = function makeRequire(abs, main) {
var self = this;
function require(rel) {
return self.require(rel, abs);
}
require.main = main;
return require;
};
// System:
// Should only be called if the system is known to have already been loaded by
// system.loadSystem.
System.prototype.getSystem = function getSystem(rel, abs) {
var via;
var hasDependency = this.dependencies[rel];
if (!hasDependency) {
via = abs ? " via " + JSON.stringify(abs) : "";
throw new Error(
"Can't get dependency " + JSON.stringify(rel) +
" in package named " + JSON.stringify(this.name) + via
);
}
var dependency = this.systems[rel];
if (!dependency) {
via = abs ? " via " + JSON.stringify(abs) : "";
throw new Error(
"Can't get dependency " + JSON.stringify(rel) +
" in package named " + JSON.stringify(this.name) + via
);
}
return dependency;
};
System.prototype.loadSystem = function (name, abs) {
var self = this;
//var hasDependency = self.dependencies[name];
//if (!hasDependency) {
// var error = new Error("Can't load module " + JSON.stringify(name));
// error.module = true;
// throw error;
//}
var loadingSystem = self.systemLoadedPromises[name];
if (!loadingSystem) {
loadingSystem = self.actuallyLoadSystem(name, abs);
self.systemLoadedPromises[name] = loadingSystem;
}
return loadingSystem;
};
System.prototype.loadSystemDescription = function loadSystemDescription(location, name) {
var self = this;
var descriptionLocation = URL.resolve(location, "package.json");
return self.read(descriptionLocation, "utf-8", "application/json")
.then(function (json) {
try {
return JSON.parse(json);
} catch (error) {
error.message = error.message + " in " +
JSON.stringify(descriptionLocation);
throw error;
}
}, function (error) {
error.message = "Can't load package " + JSON.stringify(name) + " at " +
JSON.stringify(location) + " because " + error.message;
throw error;
});
};
System.prototype.actuallyLoadSystem = function (name, abs) {
var self = this;
var System = self.constructor;
var location = self.systemLocations[name];
if (!location) {
var via = abs ? " via " + JSON.stringify(abs) : "";
throw new Error(
"Can't load package " + JSON.stringify(name) + via +
" because it is not a declared dependency"
);
}
var buildSystem;
if (self.buildSystem) {
buildSystem = self.buildSystem.actuallyLoadSystem(name, abs);
}
return Promise.all([
self.loadSystemDescription(location, name),
buildSystem
]).then(function onDescriptionAndBuildSystem(args) {
var description = args[0];
var buildSystem = args[1];
var system = new System(location, description, {
parent: self,
root: self.root,
name: name,
resources: self.resources,
modules: self.modules,
systems: self.systems,
systemLocations: self.systemLocations,
systemLoadedPromises: self.systemLoadedPromises,
buildSystem: buildSystem,
browser: self.browser,
node: self.node,
strategy: inferStrategy(description)
});
self.systems[system.name] = system;
return system;
});
};
System.prototype.getBuildSystem = function getBuildSystem() {
var self = this;
return self.buildSystem || self;
};
// Module:
System.prototype.normalizeIdentifier = function (id) {
var self = this;
var extension = Identifier.extension(id);
if (
!has.call(self.translators, extension) &&
!has.call(self.analyzers, extension) &&
extension !== "js" &&
extension !== "json"
) {
id += ".js";
}
return id;
};
System.prototype.load = function load(rel, abs) {
var self = this;
return self.deepLoad(rel, abs)
.then(function () {
return self.deepCompile(rel, abs, {});
});
};
System.prototype.deepCompile = function deepCompile(rel, abs, memo) {
var self = this;
var res = Identifier.resolve(rel, abs);
if (Identifier.isAbsolute(rel)) {
if (self.externalRedirects[res]) {
return self.deepCompile(self.externalRedirects[res], res, memo);
}
var head = Identifier.head(rel);
var tail = Identifier.tail(rel);
if (self.dependencies[head]) {
var system = self.getSystem(head, abs);
return system.compileInternalModule(tail, "", memo);
} else {
// XXX no clear idea what to do in this load case.
// Should never reject, but should cause require to produce an
// error.
return Promise.resolve();
}
} else {
return self.compileInternalModule(rel, abs, memo);
}
};
System.prototype.compileInternalModule = function compileInternalModule(rel, abs, memo) {
var self = this;
var res = Identifier.resolve(rel, abs);
var id = self.normalizeIdentifier(res);
if (self.internalRedirects[id]) {
return self.deepCompile(self.internalRedirects[id], "", memo);
}
var module = self.lookupInternalModule(id, abs);
// Break the cycle of violence
if (memo[module.key]) {
return Promise.resolve();
}
memo[module.key] = true;
if (module.compiled) {
return Promise.resolve();
}
module.compiled = true;
return Promise.resolve().then(function () {
return Promise.all(module.dependencies.map(function (dependency) {
return self.deepCompile(dependency, module.id, memo);
}));
}).then(function () {
return self.translate(module);
}).then(function () {
return self.compile(module);
}).catch(function (error) {
module.error = error;
});
};
// Loads a module and its transitive dependencies.
System.prototype.deepLoad = function deepLoad(rel, abs, memo) {
var self = this;
var res = Identifier.resolve(rel, abs);
if (Identifier.isAbsolute(rel)) {
if (self.externalRedirects[res]) {
return self.deepLoad(self.externalRedirects[res], res, memo);
}
var head = Identifier.head(rel);
var tail = Identifier.tail(rel);
if (self.dependencies[head]) {
return self.loadSystem(head, abs)
.then(function (system) {
return system.loadInternalModule(tail, "", memo);
});
} else {
// XXX no clear idea what to do in this load case.
// Should never reject, but should cause require to produce an
// error.
return Promise.resolve();
}
} else {
return self.loadInternalModule(rel, abs, memo);
}
};
System.prototype.loadInternalModule = function loadInternalModule(rel, abs, memo) {
var self = this;
var res = Identifier.resolve(rel, abs);
var id = self.normalizeIdentifier(res);
if (self.internalRedirects[id]) {
return self.deepLoad(self.internalRedirects[id], "", memo);
}
// Extension must be captured before normalization since it is used to
// determine whether to attempt to fallback to index.js for identifiers
// that might refer to directories.
var extension = Identifier.extension(res);
var module = self.lookupInternalModule(id, abs);
// Break the cycle of violence
memo = memo || {};
if (memo[module.key]) {
return Promise.resolve();
}
memo[module.key] = true;
// Return a memoized load
if (module.loadedPromise) {
return module.loadedPromise;
}
module.loadedPromise = Promise.resolve()
.then(function () {
if (module.factory == null && module.exports == null) {
return self.read(module.location, "utf-8")
.then(function (text) {
module.text = text;
return self.finishLoadingModule(module, memo);
}, fallback);
}
});
function fallback(error) {
var redirect = Identifier.resolve("./index.js", res);
module.redirect = redirect;
if (!error || error.notFound && extension === "") {
return self.loadInternalModule(redirect, abs, memo)
.catch(function (fallbackError) {
module.redirect = null;
// Prefer the original error
module.error = error || fallbackError;
});
} else {
module.error = error;
}
}
return module.loadedPromise;
};
System.prototype.finishLoadingModule = function finishLoadingModule(module, memo) {
var self = this;
return Promise.resolve().then(function () {
return self.analyze(module);
}).then(function () {
return Promise.all(module.dependencies.map(function onDependency(dependency) {
return self.deepLoad(dependency, module.id, memo);
}));
});
};
System.prototype.lookup = function lookup(rel, abs) {
var self = this;
var res = Identifier.resolve(rel, abs);
if (Identifier.isAbsolute(rel)) {
if (self.externalRedirects[res]) {
return self.lookup(self.externalRedirects[res], res);
}
var head = Identifier.head(res);
var tail = Identifier.tail(res);
if (self.dependencies[head]) {
return self.getSystem(head, abs).lookupInternalModule(tail, "");
} else if (self.modules[head] && !tail) {
return self.modules[head];
} else {
var via = abs ? " via " + JSON.stringify(abs) : "";
throw new Error(
"Can't look up " + JSON.stringify(rel) + via +
" in " + JSON.stringify(self.location) +
" because there is no external module or dependency by that name"
);
}
} else {
return self.lookupInternalModule(rel, abs);
}
};
System.prototype.lookupInternalModule = function lookupInternalModule(rel, abs) {
var self = this;
var res = Identifier.resolve(rel, abs);
var id = self.normalizeIdentifier(res);
if (self.internalRedirects[id]) {
return self.lookup(self.internalRedirects[id], res);
}
var filename = self.name + "/" + id;
// This module system is case-insensitive, but mandates that a module must
// be consistently identified by the same case convention to avoid problems
// when migrating to case-sensitive file systems.
var key = filename.toLowerCase();
var module = self.modules[key];
if (module && module.redirect && module.redirect !== module.id) {
return self.lookupInternalModule(module.redirect);
}
if (!module) {
module = new Module();
module.id = id;
module.extension = Identifier.extension(id);
module.location = URL.resolve(self.location, id);
module.filename = filename;
module.dirname = Identifier.dirname(filename);
module.key = key;
module.system = self;
module.modules = self.modules;
self.modules[key] = module;
}
if (module.filename !== filename) {
module.error = new Error(
"Can't refer to single module with multiple case conventions: " +
JSON.stringify(filename) + " and " +
JSON.stringify(module.filename)
);
}
return module;
};
System.prototype.addExtensions = function (map) {
var extensions = Object.keys(map);
for (var index = 0; index < extensions.length; index++) {
var extension = extensions[index];
var id = map[extension];
this.analyzers[extension] = this.makeLoadStep(id, "analyze");
this.translators[extension] = this.makeLoadStep(id, "translate");
}
};
System.prototype.makeLoadStep = function makeLoadStep(id, name) {
var self = this;
return function moduleLoaderStep(module) {
return self.getBuildSystem()
.import(id)
.then(function (exports) {
if (exports[name]) {
return exports[name](module);
}
});
};
};
// Translate:
System.prototype.translate = function translate(module) {
var self = this;
if (
module.text != null &&
module.extension != null &&
self.translators[module.extension]
) {
return self.translators[module.extension](module);
}
};
System.prototype.translateJson = function translateJson(module) {
module.text = "module.exports = " + module.text.trim() + ";\n";
};
// Analyze:
System.prototype.analyze = function analyze(module) {
if (
module.text != null &&
module.extension != null &&
this.analyzers[module.extension]
) {
return this.analyzers[module.extension](module);
}
};
System.prototype.analyzeJavaScript = function analyzeJavaScript(module) {
module.dependencies.push.apply(module.dependencies, parseDependencies(module.text));
};
// Compile:
System.prototype.compile = function (module) {
if (
module.factory == null &&
module.redirect == null &&
module.exports == null
) {
compile(module);
}
};
// Resource:
System.prototype.getResource = function getResource(rel, abs) {
var self = this;
if (Identifier.isAbsolute(rel)) {
var head = Identifier.head(rel);
var tail = Identifier.tail(rel);
return self.getSystem(head, abs).getInternalResource(tail);
} else {
return self.getInternalResource(Identifier.resolve(rel, abs));
}
};
System.prototype.locateResource = function locateResource(rel, abs) {
var self = this;
if (Identifier.isAbsolute(rel)) {
var head = Identifier.head(rel);
var tail = Identifier.tail(rel);
return self.loadSystem(head, abs)
.then(function onSystemLoaded(subsystem) {
return subsystem.getInternalResource(tail);
});
} else {
return Promise.resolve(self.getInternalResource(Identifier.resolve(rel, abs)));
}
};
System.prototype.getInternalResource = function getInternalResource(id) {
var self = this;
// TODO redirects
var filename = self.name + "/" + id;
var key = filename.toLowerCase();
var resource = self.resources[key];
if (!resource) {
resource = new Resource();
resource.id = id;
resource.filename = filename;
resource.dirname = Identifier.dirname(filename);
resource.key = key;
resource.location = URL.resolve(self.location, id);
resource.system = self;
self.resources[key] = resource;
}
return resource;
};
// Dependencies:
System.prototype.addDependencies = function addDependencies(dependencies) {
var self = this;
var names = Object.keys(dependencies);
for (var index = 0; index < names.length; index++) {
var name = names[index];
self.dependencies[name] = true;
if (!self.systemLocations[name]) {
var location;
if (this.strategy === "flat") {
location = URL.resolve(self.root.location, "node_modules/" + name + "/");
} else {
location = URL.resolve(self.location, "node_modules/" + name + "/");
}
self.systemLocations[name] = location;
}
}
};
// introduce allows an analyzer module to introduce a package to a dependency
// of the analyzer's package.
System.prototype.introduce = function introduce(system, name) {
if (!this.dependencies[name]) {
throw new Error("Extension package cannot introduce a module to a package that the analyzer does not directly depend upon.");
}
system.dependencies[name] = true;
if (!system.systemLocations[name]) {
system.systemLocations[name] = this.systemLocations[name];
}
};
// Redirects:
System.prototype.addRedirects = function addRedirects(redirects) {
var self = this;
var sources = Object.keys(redirects);
for (var index = 0; index < sources.length; index++) {
var source = sources[index];
var target = redirects[source];
self.addRedirect(source, target);
}
};
System.prototype.addRedirect = function addRedirect(source, target) {
var self = this;
if (Identifier.isAbsolute(source)) {
self.externalRedirects[source] = target;
} else {
source = self.normalizeIdentifier(Identifier.resolve(source));
self.internalRedirects[source] = target;
}
};
// Etc:
System.prototype.overlayBrowser = function overlayBrowser(description) {
var self = this;
if (typeof description.browser === "string") {
self.addRedirect("", description.browser);
} else if (description.browser && typeof description.browser === "object") {
self.addRedirects(description.browser);
}
};
System.prototype.inspect = function () {
var self = this;
return {type: "system", location: self.location};
};
function inferStrategy(description) {
// The existence of an _args property in package.json distinguishes
// packages that were installed with npm version 3 or higher.
if (description._args) {
return "flat";
}
return "nested";
}