@adpt/core
Version:
AdaptJS core library
372 lines • 14.2 kB
JavaScript
;
/*
* Copyright 2018-2019 Unbounded Systems, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const utils_1 = require("@adpt/utils");
const callsites = require("callsites");
const json_stable_stringify_1 = tslib_1.__importDefault(require("json-stable-stringify"));
const path = tslib_1.__importStar(require("path"));
const URN = require("urn-lib");
const util_1 = require("util");
const error_1 = require("../error");
const packageinfo_1 = require("../packageinfo");
let trace;
try {
// tslint:disable-next-line:no-var-requires
const utilsMod = require("../utils");
trace = utilsMod.trace;
}
catch (_a) {
// No tracing if utils is unavailable (e.g. in certain unit tests).
trace = () => undefined;
}
const debugReanimate = false;
// Exported for testing only
class MummyRegistry {
constructor() {
this.jsonToObj = new Map();
this.objToJson = new Map();
try {
this.packageRegistry = utils_1.createPackageRegistry(".");
}
catch (err) {
err = utils_1.ensureError(err);
if (!err.message.includes("does not contain a node_modules folder")) {
throw err;
}
// We don't have a project directory, so use the parent of the
// node_modules directory this file is in.
const dir = packageinfo_1.findNodeModulesParent(__dirname);
if (dir)
this.packageRegistry = utils_1.createPackageRegistry(dir);
}
if (!this.packageRegistry) {
throw new error_1.InternalError(`Unable to find a project folder or containing node_modules directory`);
}
}
async awaken(mummyJson) {
let obj = this.jsonToObj.get(mummyJson);
if (obj !== undefined)
return obj;
const mummy = JSON.parse(mummyJson);
if (!isMummy(mummy))
throw new Error(`Invalid mummy JSON`);
let pkgPath = await this.packageRegistry.findPath(mummy.pkgName, mummy.pkgVersion);
if (pkgPath == null) {
// We can't find an EXACT match for the package ID from mummy
// (package name and exact version). This typically happens for
// two reasons: 1) The version of the package in question has
// changed (e.g. updated to newer version) or 2) the package
// is located in a node search path, but is not in the node_modules
// directory for THIS package (e.g. it's in a parent node_modules
// directory like happens in the adapt repo).
// Both of these things could be fixed by webpacking/zipping the
// current Adapt project and all dependencies.
trace(debugReanimate, `WARN: Unable to find package ${packageId(mummy)} in module tree`);
pkgPath = mummy.pkgName;
}
let mainFile;
try {
mainFile = require.resolve(pkgPath);
}
catch (err) {
throw new Error(`Unable to locate installed package ${mummy.pkgName} version ` +
`${mummy.pkgVersion})}`);
}
const modPath = path.join(path.dirname(mainFile), mummy.relFilePath);
// This should cause the module to initialize and call registerObject.
const exp = require(modPath);
// Try the lookup again
obj = this.jsonToObj.get(mummyJson);
if (obj !== undefined)
return obj;
// We get here if the call to registerObject is not done at the top
// level module scope. We can still find the object we're looking for
// as long as it gets exported and that export happens at the top
// level module scope.
trace(debugReanimate, `\n**** Searching exports for:`, mummy, `\nExports:`, exp);
this.print();
let parent = exp;
if (mummy.namespace !== "")
parent = parent && parent[mummy.namespace];
obj = parent && parent[mummy.name];
trace(debugReanimate, `Exports lookup returned:`, obj);
// NOTE(mark): I think we can remove namespace, as long as this error
// never triggers.
if (mummy.namespace !== "" && obj != null) {
throw new Error(`**** Used non-default namespace to successfully find ${mummyJson}`);
}
if (obj === undefined) {
throw new Error(`Unable to reanimate ${mummyJson}`);
}
this.entomb(obj, mummyJson);
return obj;
}
findMummy(obj) {
if (obj == null)
throw new Error(`Can't get JSON representation of ${obj}`);
const mj = this.objToJson.get(obj);
if (mj !== undefined)
return mj;
throw new Error(`Unable to look up JSON representation for '${obj}'`);
}
entomb(obj, mummyJson) {
if (obj == null) {
throw new Error(`Unable to store ${obj} for later reanimation`);
}
this.jsonToObj.set(mummyJson, obj);
const existing = this.objToJson.get(obj);
if (existing !== undefined && existing !== mummyJson) {
trace(debugReanimate, `WARN: reanimate: object '${obj}' already stored`);
trace(debugReanimate, `Existing:`, existing, `New:`, mummyJson);
}
else {
this.objToJson.set(obj, mummyJson);
}
}
print() {
if (!debugReanimate)
return;
trace(debugReanimate, "Registry - jsonToObj:");
this.jsonToObj.forEach((key, val) => {
trace(debugReanimate, ` ${key} -> ${val}`);
});
trace(debugReanimate, "\nRegistry - objToJson:");
this.objToJson.forEach((key, val) => {
trace(debugReanimate, ` ${key} -> ${val}`);
});
}
}
exports.MummyRegistry = MummyRegistry;
let registry = new MummyRegistry();
function resetRegistry() {
registry = new MummyRegistry();
}
const mummyProps = ["name", "namespace", "pkgName", "pkgVersion", "relFilePath"];
function isMummy(val) {
if (val == null || typeof val !== "object") {
throw new Error(`Invalid represenation of object`);
}
for (const prop of mummyProps) {
const t = typeof val[prop];
if (t !== "string") {
throw new Error(`Invalid property '${prop}' type '${t}' in representation of object`);
}
}
return true;
}
function enbalm(obj, name, namespace, module) {
const pkgInfo = packageinfo_1.findPackageInfo(path.dirname(module.filename));
const m = {
name,
namespace,
pkgName: pkgInfo.name,
pkgVersion: pkgInfo.version,
relFilePath: path.relative(path.dirname(pkgInfo.main), module.filename),
};
trace(debugReanimate, "mainFile:", pkgInfo.main, "\nmummy:", m);
const s = json_stable_stringify_1.default(m);
trace(debugReanimate, "JSON value:", s);
return s;
}
function registerObject(obj, name, modOrCallerNum = 0, altNamespace = "$adaptExports") {
if (obj == null)
throw new Error(`Cannot register null or undefined`);
const mod = findModule(modOrCallerNum);
if (mod.exports == null) {
throw new error_1.InternalError(`exports unexpectedly null for ` +
`${mod.id}\n${util_1.inspect(mod)}`);
}
// FIXME(mark): we should wait to run findExportName until
// module.loaded === true. To do that, we should create a Promise, but
// store it rather than returning it, to keep this function sync. Then
// both reanimate and findMummy should ensure all promises are resolved before
// continuing operation. That should allow us to remove the namespace
// stuff.
const exportName = findExportName(obj, name, mod);
registry.entomb(obj, enbalm(obj, exportName || name, exportName ? "" : altNamespace, mod));
if (!exportName) {
let exp = mod.exports[altNamespace];
if (exp == null) {
exp = Object.create(null);
mod.exports[altNamespace] = exp;
}
exp[name] = obj;
}
}
exports.registerObject = registerObject;
// tslint:disable-next-line:ban-types
function registerConstructor(ctor) {
registerObject(ctor, ctor.name, findConstructorModule(ctor.name, ctor.displayName));
}
exports.registerConstructor = registerConstructor;
function findExportName(obj, defaultName, module) {
// Try preferred first, in case this obj is exported under multiple
// names.
if (module.exports[defaultName] === obj)
return defaultName;
// obj is not exported as that name
for (const k of Object.keys(module.exports)) {
if (module.exports[k] === obj)
return k;
}
return undefined;
}
function findModule(modOrCallerNum) {
let mod;
if (typeof modOrCallerNum === "number") {
if (modOrCallerNum < 0) {
throw new Error(`registerObject: callerNum must be >= 0`);
}
// Back up the stack to caller of registerObject
mod = callerModule(modOrCallerNum + 3);
}
else {
mod = modOrCallerNum;
}
return mod;
}
// Exported for testing
function findConstructorModule(ctorName, displayName) {
const stack = callsites();
let candidateFrame;
// Skip over first entry..that's this function
let frame = 1;
// Find the first frame inside a constructor
while (frame < stack.length) {
if (stack[frame].isConstructor())
break;
frame++;
}
if (frame === stack.length)
throw new Error(`Unable to find constructor on stack`);
// getTypeName for the first constructor frame appears to be one of the
// few frames that has a non-null this type name. Use as a double check.
const thisType = stack[frame].getTypeName();
if (typeof thisType !== "string") {
throw new Error(`Uncertain about constructor stack frame structure for ${ctorName}`);
}
let lastConstructor = frame;
while (frame < stack.length) {
// Stop when we reach a frame not inside a constructor
if (!stack[frame].isConstructor())
break;
lastConstructor = frame;
// With Node v12+, the name in the backtrace is constructor.displayName if
// present.
if (stack[frame].getFunctionName() === ctorName ||
(displayName != null && stack[frame].getFunctionName() === displayName)) {
if (candidateFrame !== undefined) {
throw new Error(`Found two candidate constructor frames`);
}
candidateFrame = frame;
}
frame++;
}
if (candidateFrame === undefined) {
throw new Error(`Unable to find constructor with correct name. Outer ` +
`constructor is: ${stack[lastConstructor].getFunctionName()}`);
}
const filename = stack[candidateFrame].getFileName();
if (!filename)
throw new Error(`stack frame has no filename`);
const mod = require.cache[filename];
if (!mod)
throw new Error(`Unable to find module for file ${filename}`);
return mod;
}
exports.findConstructorModule = findConstructorModule;
// Exported for testing
function callerModule(callerNum) {
if (!Number.isInteger(callerNum) || callerNum < 0) {
throw new Error(`callerModule: invalid callerNum: ${callerNum}`);
}
const stack = callsites();
if (callerNum >= stack.length) {
throw new Error(`callerModule: callerNum too large: ${callerNum}, max: ${stack.length - 1}`);
}
const fileName = stack[callerNum].getFileName();
if (fileName == null) {
throw new Error(`callerModule: unable to get filename`);
}
const mod = require.cache[fileName];
if (mod == null) {
throw new Error(`callerModule: file ${fileName} not in cache`);
}
return mod;
}
exports.callerModule = callerModule;
function reanimate(mummy) {
return registry.awaken(mummy);
}
exports.reanimate = reanimate;
function findMummy(obj) {
return registry.findMummy(obj);
}
exports.findMummy = findMummy;
// Exported for testing
function mockRegistry_(newRegistry) {
const oldRegistry = registry;
if (newRegistry === null)
resetRegistry();
else if (newRegistry !== undefined)
registry = newRegistry;
return oldRegistry;
}
exports.mockRegistry_ = mockRegistry_;
/*
* URNs
*/
const urnDomain = "Adapt";
const encoder = URN.create("urn", {
components: [
"domain",
"pkgName",
"pkgVersion",
"namespace",
"relFilePath",
"name",
],
separator: ":",
allowEmpty: true,
});
function findMummyUrn(obj) {
const mummyJson = registry.findMummy(obj);
const mummy = JSON.parse(mummyJson);
return encoder.format(Object.assign({ domain: urnDomain }, mummy));
}
exports.findMummyUrn = findMummyUrn;
function reanimateUrn(mummyUrn) {
const parsedUrn = encoder.parse(mummyUrn);
if (parsedUrn == null)
throw new Error(`Cannot reanimate urn: "${mummyUrn}"`);
const { domain, protocol } = parsedUrn, mummy = tslib_1.__rest(parsedUrn, ["domain", "protocol"]);
if (protocol !== "urn") {
throw new Error(`Invalid protocol in URN '${mummyUrn}'`);
}
if (domain !== urnDomain) {
throw new Error(`Invalid domain in URN '${mummyUrn}'`);
}
if (!isMummy(mummy))
throw new error_1.InternalError(`isMummy returned false`);
return registry.awaken(json_stable_stringify_1.default(mummy));
}
exports.reanimateUrn = reanimateUrn;
function packageId(pkg) {
return `${pkg.pkgName}@${pkg.pkgVersion}`;
}
//# sourceMappingURL=reanimate.js.map