UNPKG

@adpt/core

Version:
372 lines 14.2 kB
"use strict"; /* * 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