import { createPackageRegistry , ensureError , PackageRegistry , posixPath } from "@adpt/utils" ; import callsites = require ( "callsites" ) ; import stringify from "json-stable-stringify" ; import * as path from "path" ; import URN = require ( "urn-lib" ) ; import { inspect } from "util" ; import { InternalError } from "../error" ; import { findNodeModulesParent , findPackageInfo } from "../packageinfo" ; import * as utilsTypes from "../utils" ; let trace ; try { const utilsMod = require ( "../utils" ) ; trace = utilsMod . trace ; } catch { trace = ( ) => undefined ; } const debugReanimate = false ; export type MummyJson = string ; export type MummyUrn = string ; type PackageId = string ; export class MummyRegistry { jsonToObj = new Map < MummyJson , any > ( ) ; objToJson = new Map < any , MummyJson > ( ) ; packageRegistry : PackageRegistry ; constructor ( projectRoot ) { try { this . packageRegistry = createPackageRegistry ( projectRoot ) ; } catch ( err ) { err = ensureError ( err ) ; if ( ! err . message . includes ( "does not contain a node_modules folder" ) ) { throw err ; } const dir = findNodeModulesParent ( __dirname ) ; if ( dir ) this . packageRegistry = createPackageRegistry ( dir ) ; } if ( ! this . packageRegistry ) { throw new 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 ) { 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 ) ; const exp = require ( modPath ) ; obj = this . jsonToObj . get ( mummyJson ) ; if ( obj !== undefined ) return obj ; 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 ) ; 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 }` ) ; } ) ; } } let _registry = initRegistry ( ) ; function registry ( ) { if ( ! _registry ) throw new InternalError ( `Reanimate registry not initialized` ) ; return _registry ; } function initRegistry ( ) { let projectRoot ; try { projectRoot = ( global as any ) . getAdaptContext ( ) . projectRoot ; } catch ( err ) { return undefined ; } return new MummyRegistry ( projectRoot ) ; } interface Mummy { name : string ; namespace : string ; pkgName : string ; pkgVersion : string ; relFilePath : string ; } 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 = findPackageInfo ( path . dirname ( module . filename ) ) ; const m = { name , namespace , pkgName : pkgInfo . name , pkgVersion : pkgInfo . version , relFilePath : posixPath ( path . relative ( path . dirname ( pkgInfo . main ) , module . filename ) ) , } ; trace ( debugReanimate , "mainFile:" , pkgInfo . main , "\nmummy:" , m ) ; const s = stringify ( m ) ; trace ( debugReanimate , "JSON value:" , s ) ; return s ; } export function registerObject ( obj , name , modOrCallerNum = 0 , altNamespace = "$adaptExports" ) { if ( ! _registry ) return ; if ( obj == null ) throw new Error ( `Cannot register null or undefined` ) ; const mod = findModule ( modOrCallerNum ) ; if ( mod . exports == null ) { throw new InternalError ( `exports unexpectedly null for ` + `${ mod . id }\n${ inspect ( mod ) }` ) ; } 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 ; } } export function registerConstructor ( ctor ) { registerObject ( ctor , ctor . name , findConstructorModule ( ctor . name , ctor . displayName ) ) ; } function findExportName ( obj , defaultName , module ) { if ( module . exports [ defaultName ] === obj ) return defaultName ; 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` ) ; } mod = callerModule ( modOrCallerNum + 3 ) ; } else { mod = modOrCallerNum ; } return mod ; } export function findConstructorModule ( ctorName , displayName ) { const stack = callsites ( ) ; let candidateFrame ; let frame = 1 ; while ( frame < stack . length ) { if ( stack [ frame ] . isConstructor ( ) ) break ; frame ++ ; } if ( frame === stack . length ) throw new Error ( `Unable to find constructor on stack` ) ; 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 ) { if ( ! stack [ frame ] . isConstructor ( ) ) break ; lastConstructor = frame ; 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 ; } export 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 ; } export function reanimate ( mummy ) { return registry ( ) . awaken ( mummy ) ; } export function findMummy ( obj ) { return registry ( ) . findMummy ( obj ) ; } export function mockRegistry_ ( newRegistry ? ) { const oldRegistry = _registry ; if ( newRegistry === null ) _registry = new MummyRegistry ( "." ) ; else if ( newRegistry !== undefined ) _registry = newRegistry ; return oldRegistry ; } const urnDomain = "Adapt" ; const encoder = URN . create ( "urn" , { components : [ "domain" , "pkgName" , "pkgVersion" , "namespace" , "relFilePath" , "name" , ] , separator : ":" , allowEmpty : true , } ) ; export function findMummyUrn ( obj ) { if ( ! _registry ) return "" ; const mummyJson = registry ( ) . findMummy ( obj ) ; const mummy = JSON . parse ( mummyJson ) ; return encoder . format ( { domain : urnDomain , ... mummy } ) ; } export function reanimateUrn ( mummyUrn ) { const parsedUrn = encoder . parse ( mummyUrn ) ; if ( parsedUrn == null ) throw new Error ( `Cannot reanimate urn: "${ mummyUrn }"` ) ; const { domain , protocol , ... mummy } = parsedUrn ; 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 InternalError ( `isMummy returned false` ) ; return registry ( ) . awaken ( stringify ( mummy ) ) ; } function packageId ( pkg ) { return `${ pkg . pkgName }@${ pkg . pkgVersion }` ; }