@meteorjs/reify
Version:
Enable ECMAScript 2015 modules in Node today. No caveats. Full stop.
596 lines (492 loc) • 18.4 kB
JavaScript
"use strict";
// This module should be compatible with PhantomJS v1, just like the other files
// in reify/lib/runtime. Node 4+ features like const/let and arrow functions are
// not acceptable here, and importing any npm packages should be contemplated
// with extreme skepticism.
var utils = require("./utils.js");
var GETTER_ERROR = {};
var NAN = {};
var UNDEFINED = {};
var hasOwn = Object.prototype.hasOwnProperty;
var keySalt = 0;
var nextEvaluationIndex = 0;
function Entry(id) {
// The canonical absolute module ID of the module this Entry manages.
this.id = id;
// The Module object this Entry manages, unknown until module.export or
// module.link is called for the first time.
this.module = null;
// The normalized namespace object that importers receive when they use
// `import * as namespace from "..."` syntax.
this.namespace = utils.createNamespace();
// Getters for local variables exported from the managed module.
this.getters = Object.create(null);
// Setters for assigning to local variables in parent modules.
this.setters = Object.create(null);
// Map of setters added since the last broadcast (in the same shape as
// entry.setters[name][key]), which should receive a broadcast the next time
// entry.runSetters() is called, regardless of whether entry.snapshots[name]
// has changed or not. Once called, setters are removed from this.newSetters,
// but remain in this.setters.
this.newSetters = Object.create(null);
// Map from local names to snapshots of the corresponding local values, used
// to determine when local values have changed and need to be re-broadcast.
this.snapshots = Object.create(null);
// State for top level await
// uses top level await
this.hasTLA = false;
// is an async module (uses top level await, or has deps that do)
this.asyncEvaluation = false;
this.asyncEvaluationIndex = null;
this.pendingAsyncDeps = 0;
this._onEvaluating = [];
this._onEvaluated = [];
// Statuses don't exactly match the ECMAScript spec
// linking - until module starts evaluating
// evaluating - module code is being run
// evaluated - after module finished evaluating. If the module does not use
// TLA, its status becomes evaluated before it actually
// finishes evaluating
// If top level await is not enabled, this will always be linking
this.status = 'linking';
this.allAsyncParents = [];
this.pendingAsyncParents = [];
this.evaluationError = null;
}
var Ep = utils.setPrototypeOf(Entry.prototype, null);
var entryMap = Object.create(null);
Entry.getOrNull = function (id) {
if (hasOwn.call(entryMap, id)) {
return entryMap[id];
}
return null;
}
Entry.getOrCreate = function (id, mod) {
var entry = hasOwn.call(entryMap, id)
? entryMap[id]
: entryMap[id] = new Entry(id);
if (utils.isObject(mod) &&
mod.id === entry.id) {
entry.module = mod;
}
return entry;
};
function safeKeys(obj) {
var keys = Object.keys(obj);
var esModuleIndex = keys.indexOf("__esModule");
if (esModuleIndex >= 0) {
keys.splice(esModuleIndex, 1);
}
return keys;
}
Ep.addGetters = function (getters, constant) {
var names = safeKeys(getters);
var nameCount = names.length;
constant = !! constant;
for (var i = 0; i < nameCount; ++i) {
var name = names[i];
var getter = getters[name];
if (typeof getter === "function" &&
// Should this throw if this.getters[name] exists?
! (name in this.getters)) {
this.getters[name] = getter;
getter.constant = constant;
getter.runCount = 0;
}
}
};
Ep.addSetters = function (parent, setters, key) {
var names = safeKeys(setters);
var nameCount = names.length;
if (! nameCount) {
return;
}
// If no key is provided, make a unique key. Otherwise, make sure the key is
// distinct from keys provided by other parent modules.
key = key === void 0
? makeUniqueKey()
: parent.id + ":" + key;
var entry = this;
for (var i = 0; i < nameCount; ++i) {
var name = names[i];
var setter = normalizeSetterValue(parent, setters[name]);
if (typeof setter === "function") {
setter.parent = parent;
// Store the setter as entry.setters[name][key], and also record it
// temporarily in entry.newSetters, so we can be sure to run it when we
// call entry.runSetters(names) below, even though entry.snapshots[name]
// likely will not have changed for this name.
utils.ensureObjectProperty(entry.setters, name)[key] = setter;
utils.ensureObjectProperty(entry.newSetters, name)[key] = setter;
}
}
entry.runSetters(names);
};
function normalizeSetterValue(module, setter) {
if (typeof setter === "function") {
return setter;
}
if (typeof setter === "string") {
// If the value of the setter property is a string, the setter will
// re-export the imported value using that string as the name of the
// exported value. If the string is "*", all properties of the value
// object will be re-exported as individual exports, except for the
// "default" property (use "*+" instead of "*" to include it).
return module.exportAs(setter);
}
if (Array.isArray(setter)) {
switch (setter.length) {
case 0: return null;
case 1: return normalizeSetterValue(module, setter[0]);
default:
var setterFns = setter.map(function (elem) {
return normalizeSetterValue(module, elem);
});
// Return a combined function that calls all of the nested setter
// functions with the same value.
return function (value) {
setterFns.forEach(function (fn) {
fn(value);
});
};
}
}
return null;
}
Ep.runGetters = function (names) {
// Before running getters, copy anything added to the exports object
// over to the namespace. Values returned by getters take precedence
// over these values, but we don't want to miss anything.
syncExportsToNamespace(this, names);
if (names === void 0 ||
names.indexOf("*") >= 0) {
names = Object.keys(this.getters);
}
var nameCount = names.length;
for (var i = 0; i < nameCount; ++i) {
var name = names[i];
var value = runGetter(this, name);
// If the getter is run without error, update both entry.namespace and
// module.exports with the current value so that CommonJS require
// calls remain consistent with module.watch.
if (value !== GETTER_ERROR) {
this.namespace[name] = value;
this.module.exports[name] = value;
}
}
};
function syncExportsToNamespace(entry, names) {
var setDefault = false;
if (entry.module === null) return;
var exports = entry.module.exports;
if (! utils.getESModule(exports)) {
// If the module entry is managing overrides module.exports, that
// value should be exposed as the .default property of the namespace,
// unless module.exports is marked as an ECMASCript module.
entry.namespace.default = exports;
setDefault = true;
}
if (! utils.isObjectLike(exports)) {
return;
}
if (names === void 0 ||
names.indexOf("*") >= 0) {
names = Object.keys(exports);
}
names.forEach(function (key) {
// Don't set any properties for which a getter function exists in
// entry.getters, don't accidentally override entry.namespace.default,
// and only copy own properties from entry.module.exports.
if (! hasOwn.call(entry.getters, key) &&
! (setDefault && key === "default") &&
hasOwn.call(exports, key)) {
utils.copyKey(key, entry.namespace, exports);
}
});
}
// Called whenever module.exports might have changed, to trigger any
// setters associated with the newly exported values. The names parameter
// is optional; without it, all getters and setters will run.
// If the '*' setter needs to be run, but not the '*' getter (names includes
// all exports/getters that changed), the runNsSetter option can be enabled.
Ep.runSetters = function (names, runNsSetter) {
// Make sure entry.namespace and module.exports are up to date before we
// call getExportByName(entry, name).
this.runGetters(names);
if (runNsSetter && names !== void 0) {
names.push('*');
}
// Lazily-initialized object mapping parent module identifiers to parent
// module objects whose setters we might need to run.
var parents;
var parentNames;
forEachSetter(this, names, function (setter, name, value) {
if (parents === void 0) {
parents = Object.create(null);
}
if (parentNames === void 0) {
parentNames = Object.create(null);
}
var parentId = setter.parent.id;
// When setters use the shorthand for re-exporting values, we know
// which exports in the parent module were modified, and can do less work
// when running the parent setters.
// parentNames[parentId] is set to false if there are any setters that we do
// not know which exports they modify
if (setter.exportAs !== void 0 && parentNames[parentId] !== false) {
parentNames[parentId] = parentNames[parentId] || [];
parentNames[parentId].push(setter.exportAs);
} else if (parentNames[parentId] !== false) {
parentNames[parentId] = false;
}
parents[parentId] = setter.parent;
// The param order for setters is `value` then `name` because the `name`
// param is only used by namespace exports.
setter(value, name);
});
if (! parents) {
return;
}
// If any of the setters updated the module.exports of a parent module,
// or updated local variables that are exported by that parent module,
// then we must re-run any setters registered by that parent module.
var parentIDs = Object.keys(parents);
var parentIDCount = parentIDs.length;
for (var i = 0; i < parentIDCount; ++i) {
// What happens if parents[parentIDs[id]] === module, or if
// longer cycles exist in the parent chain? Thanks to our snapshot
// bookkeeping above, the runSetters broadcast will only proceed
// as far as there are any actual changes to report.
var parent = parents[parentIDs[i]];
var parentEntry = entryMap[parent.id];
if (parentEntry) {
parentEntry.runSetters(
parentNames[parentIDs[i]] || void 0,
!!parentNames[parentIDs[i]]
);
}
}
};
Ep.addAsyncDep = function (childEntry) {
if (childEntry.status !== 'evaluated') {
this.pendingAsyncDeps += 1;
childEntry.allAsyncParents.push(this);
childEntry.pendingAsyncParents.push(this);
}
if (childEntry.evaluationError) {
this.setEvaluationError(childEntry.evaluationError);
}
}
Ep.changeStatus = function (status) {
switch (status) {
case 'linking':
this.status = 'linking';
break;
case 'evaluating':
this.status = 'evaluating';
this._onEvaluating.forEach(function (callback) {
callback();
});
break;
case 'evaluated':
this.status = 'evaluated';
var toEvaluate = [];
this.gatherReadyAsyncParents(toEvaluate);
toEvaluate.sort(function (entryA, entryB) {
return entryA.asyncEvaluationIndex - entryB.asyncEvaluationIndex;
});
toEvaluate.forEach(function (parent) {
parent.changeStatus('evaluating');
});
var callbacks = this._onEvaluated;
this._onEvaluated = [];
callbacks.forEach(function (callback) {
callback();
});
break;
default:
throw new Error('Unrecognized module status: ' + status);
}
}
Ep.gatherReadyAsyncParents = function (readyList) {
this.pendingAsyncParents.forEach(function (parent) {
parent.pendingAsyncDeps -= 1;
if (parent.pendingAsyncDeps === 0) {
readyList.push(parent);
if (!parent.hasTLA) {
parent.gatherReadyAsyncParents(readyList);
}
}
});
this.pendingAsyncParents = [];
}
Ep.setAsyncEvaluation = function () {
if (this.asyncEvaluationIndex !== null) {
if (this.status === 'evaluated') {
return;
}
throw new Error('setAsyncEvaluation can only be called once');
}
this.asyncEvaluation = this.hasTLA || this.pendingAsyncDeps > 0;
this.asyncEvaluationIndex = nextEvaluationIndex++;
};
Ep.setEvaluationError = function (error) {
// If multiple deps were running in parallel, there could be
// multiple errors. Keep the first one.
if (!this.evaluationError) {
this.evaluationError = error;
}
this.allAsyncParents.forEach(function (parent) {
parent.setEvaluationError(error);
});
}
function createSnapshot(entry, name, newValue) {
var newSnapshot = Object.create(null);
var newKeys = [];
if (name === "*") {
safeKeys(newValue).forEach(function (keyOfValue) {
// Evaluating value[key] is risky because the property might be
// defined by a getter function that logs a deprecation warning (or
// worse) when evaluated. For example, Node uses this trick to display
// a deprecation warning whenever crypto.createCredentials is
// accessed. Fortunately, when value[key] is defined by a getter
// function, it's enough to check whether the getter function itself
// has changed, since we are careful elsewhere to preserve getters
// rather than prematurely evaluating them.
newKeys.push(keyOfValue);
newSnapshot[keyOfValue] = normalizeSnapshotValue(
utils.valueOrGetter(newValue, keyOfValue)
);
});
} else {
newKeys.push(name);
newSnapshot[name] = normalizeSnapshotValue(newValue);
}
var oldSnapshot = entry.snapshots[name];
if (
oldSnapshot &&
newKeys.every(function (key) {
return oldSnapshot[key] === newSnapshot[key]
}) &&
newKeys.length === Object.keys(oldSnapshot).length
) {
return oldSnapshot;
}
return newSnapshot;
}
function normalizeSnapshotValue(value) {
if (value === void 0) return UNDEFINED;
if (value !== value && isNaN(value)) return NAN;
return value;
}
// Obtain an array of keys in entry.setters[name] for which we need to run a
// setter function. If successful, entry.snapshot[name] will be updated and/or
// entry.newSetters[name] will be removed, so the returned keys will not be
// returned again until after the snapshot changes again. If the snapshot hasn't
// changed and there aren't any entry.newSetters[name] keys, this function
// returns undefined, to avoid allocating an empty array in the common case.
function consumeKeysGivenSnapshot(entry, name, snapshot) {
if (entry.snapshots[name] !== snapshot) {
entry.snapshots[name] = snapshot;
// Since the keys of entry.newSetters[name] are a subset of those of
// entry.setters[name], we can consume entry.newSetters[name] here too.
delete entry.newSetters[name];
return Object.keys(entry.setters[name]);
}
// If new setters have been added to entry.setters (and thus also to
// entry.newSetters) since we last recorded entry.snapshots[name], we need to
// run those setters (for the first time) in order to consider them up-to-date
// with respect to entry.snapshots[name].
var news = entry.newSetters[name];
var newKeys = news && Object.keys(news);
if (newKeys && newKeys.length) {
// Consume the new keys so we don't consider them again.
delete entry.newSetters[name];
return newKeys;
}
}
// Invoke the given callback once for every (setter, name, value) that needs to
// be called. Note that forEachSetter does not call any setters itself, only the
// given callback.
function forEachSetter(entry, names, callback) {
if (names === void 0) {
names = Object.keys(entry.setters);
}
names.forEach(function (name) {
// Ignore setters asking for module.exports.__esModule.
if (name === "__esModule") return;
var settersByKey = entry.setters[name];
if (!settersByKey) return;
var getter = entry.getters[name];
var alreadyCalledConstantGetter =
typeof getter === "function" &&
// Sometimes a getter function will throw because it's called
// before the variable it's supposed to return has been
// initialized, so we need to know that the getter function has
// run to completion at least once.
getter.runCount > 0 &&
getter.constant;
var value = getExportByName(entry, name);
// Although we may have multiple setter functions with different keys in
// settersByKey, we can compute a snapshot of value and check it against
// entry.snapshots[name] before iterating over the individual setter
// functions
var snapshot = createSnapshot(entry, name, value);
var keys = consumeKeysGivenSnapshot(entry, name, snapshot);
if (keys === void 0) return;
keys.forEach(function (key) {
var setter = settersByKey[key];
if (!setter) {
return;
}
// Invoke the setter function with the updated value.
callback(setter, name, value);
if (alreadyCalledConstantGetter) {
// If we happen to know this getter function has run successfully
// (getter.runCount > 0), and will never return a different value
// (getter.constant), then we can forget the corresponding setter,
// because we've already reported that constant value. Note that we
// can't forget the getter, because we need to remember the original
// value in case anyone tampers with entry.module.exports[name].
delete settersByKey[key];
}
});
});
}
function getExportByName(entry, name) {
if (name === "*") {
return entry.namespace;
}
if (hasOwn.call(entry.namespace, name)) {
return entry.namespace[name];
}
if (entry.module === null) return;
var exports = entry.module.exports;
if (name === "default" &&
! (utils.getESModule(exports) &&
"default" in exports)) {
return exports;
}
if (exports == null) {
return;
}
return exports[name];
}
function makeUniqueKey() {
return Math.random()
.toString(36)
// Add an incrementing salt to help track key ordering and also
// absolutely guarantee we never return the same key twice.
.replace("0.", ++keySalt + "$");
}
function runGetter(entry, name) {
var getter = entry.getters[name];
if (!getter) return GETTER_ERROR;
try {
var result = getter();
++getter.runCount;
return result;
} catch (e) {}
return GETTER_ERROR;
}
module.exports = Entry;