import { createMockLogger } from "@adpt/testutils" ; import { createTaskObserver , InternalError , messagesToString } from "@adpt/utils" ; import fs from "fs-extra" ; import { isFunction } from "lodash" ; import * as path from "path" ; import randomstring from "randomstring" ; import { ActComplete , Action , ChangeType , EPPrimitiveDependencies , ExecutionPlan , Plugin , PluginModule } from "../../src/deploy" ; import { DependencyType , isExecutionPlanImpl } from "../../src/deploy/execution_plan" ; import { createPluginManager , isPluginManagerImpl } from "../../src/deploy/plugin_support" ; import { noStateUpdates , ProcessStateUpdates } from "../../src/dom" ; import { domDiff } from "../../src/dom_utils" ; import { AdaptElement , AdaptMountedElement , FinalDomElement } from "../../src/jsx" ; import { Deployment } from "../../src/server/deployment" ; import { DeployOpID , DeployStepID } from "../../src/server/deployment_data" ; import { createStateStore , StateStore } from "../../src/state" ; import { dependencies , StringDependencies } from "../deploy/common" ; import { doBuild } from "./do_build" ; import { createMockDeployment } from "./server_mocks" ; export interface MockDeployOptions { deployOpID ? : DeployOpID ; pluginCreates : PluginModule [ "create" ] [ ] ; prevDom ? : FinalDomElement ; tmpDir : string ; uniqueDeployID ? : boolean ; } export interface DeployOptions { dryRun ? : boolean ; once ? : boolean ; debug ? : boolean ; logError ? : boolean ; pollDelayMs ? : number ; style ? : AdaptElement | null ; timeoutInMs ? : number ; } export interface GetExecutionPlanOptions { style ? : AdaptElement | null ; } interface DeployOptionsInternal extends DeployOptions { returnPlan ? : boolean ; } const defaultDeployOptions = { dryRun : false , once : false , debug : false , logError : true , pollDelayMs : 1000 , style : null , timeoutInMs : 10 * 60 * 1000 } ; export interface DeployOutput extends ActComplete { dom : FinalDomElement | null ; mountedOrig : AdaptMountedElement | null ; stepID : DeployStepID ; } export interface ExecutionPlanOutput { dependencies : Record < DependencyType , StringDependencies > ; dom : FinalDomElement | null ; mountedOrig : AdaptMountedElement | null ; plan : ExecutionPlan ; } export function makeDeployId ( prefix ) { const rand = randomstring . generate ( { length : 4 , charset : "alphabetic" , readable : true , capitalization : "lowercase" , } ) ; return `${ prefix }-${ rand }` ; } export class MockDeploy { prevDom : FinalDomElement | null = null ; currDom : FinalDomElement | null = null ; prevMountedElements : AdaptMountedElement [ ] = [ ] ; currMountedElements : AdaptMountedElement [ ] = [ ] ; prevDependencies : EPPrimitiveDependencies = { } ; logger = createMockLogger ( ) ; dataDir : string ; deployID = "MockDeploy123" ; deployment_ ? : Deployment ; deployOpID_ ? : DeployOpID ; plugins = new Map < string , PluginModule > ( ) ; stateStore : StateStore = createStateStore ( "{}" ) ; constructor ( options ) { if ( options . pluginCreates . length === 0 ) throw new Error ( `Must specify plugins` ) ; options . pluginCreates . forEach ( ( create ) => { const inst = create ( ) ; const name = inst . constructor . name ; this . plugins . set ( name , { name , module , create , packageName : name , version : "0.0.1" , } ) ; } ) ; if ( options . prevDom ) this . prevDom = options . prevDom ; if ( options . deployOpID != null ) this . deployOpID_ = options . deployOpID ; if ( options . uniqueDeployID ) this . deployID = makeDeployId ( "MockDeploy" ) ; this . dataDir = path . join ( options . tmpDir , "pluginData" ) ; } async init ( ) { this . deployment_ = await createMockDeployment ( { deployID : this . deployID } ) ; await fs . ensureDir ( this . dataDir ) ; if ( this . deployOpID_ == null ) this . deployOpID_ = await this . deployment . newOpID ( ) ; } get deployment ( ) { if ( ! this . deployment_ ) throw new Error ( `deployment == null. init not called?` ) ; return this . deployment_ ; } get deployOpID ( ) { if ( this . deployOpID_ == null ) throw new Error ( `deployOpID == null. init not called?` ) ; return this . deployOpID_ ; } set deployOpID ( id ) { this . deployOpID_ = id ; } async deploy ( orig , options = { } ) { return this . _deploy ( orig , { returnPlan : false , ... options } ) ; } async getExecutionPlan ( orig , options = { } ) { return this . _deploy ( orig , { returnPlan : true , ... options } ) ; } private async _deploy ( orig , options ) ; private async _deploy ( orig , options ) ; private async _deploy ( orig , options ) { const { debug , style , ... opts } = { ... defaultDeployOptions , ... options } ; let dom ; let mountedOrig ; let processStateUpdates ; let mountedElements ; this . deployOpID = await this . deployment . newOpID ( ) ; const startTime = new Date ( ) . getTime ( ) ; while ( true ) { const now = new Date ( ) . getTime ( ) ; const elapsed = now - startTime ; if ( elapsed > opts . timeoutInMs ) { throw new Error ( `MockDeploy.deploy timed out in ${ opts . timeoutInMs / 1000 }s` ) ; } const taskObserver = createTaskObserver ( "parent" , { logger : this . logger } ) ; taskObserver . started ( ) ; processStateUpdates = noStateUpdates ; mountedElements = [ ] ; if ( orig === null ) { dom = null ; mountedOrig = null ; } else { const res = await doBuild ( orig , { debug , deployID : this . deployID , deployOpID : this . deployOpID , stateStore : this . stateStore , style , } ) ; dom = res . dom ; mountedOrig = res . mountedOrig ; processStateUpdates = res . processStateUpdates ; mountedElements = res . mountedElements ; } this . prevDom = this . currDom ; this . currDom = dom ; this . prevMountedElements = this . currMountedElements ; this . currMountedElements = mountedElements ; const actOpts = { dryRun : opts . dryRun , pollDelayMs : opts . pollDelayMs , processStateUpdates , taskObserver , timeoutMs : opts . timeoutInMs - elapsed } ; const mgr = createPluginManager ( this . plugins ) ; try { await mgr . start ( { dataDir : this . dataDir , deployOpID : this . deployOpID , deployment : this . deployment , logger : this . logger , newDom : dom , newMountedElements : this . currMountedElements , prevDependencies : this . prevDependencies , prevDom : this . prevDom , prevMountedElements : this . prevMountedElements , } ) ; await mgr . observe ( ) ; const analysis = mgr . analyze ( ) ; this . prevDependencies = analysis . dependencies ; if ( opts . returnPlan ) { if ( ! isPluginManagerImpl ( mgr ) ) throw new InternalError ( `Not a PluginManagerImpl` ) ; const plan = mgr . plan ; if ( ! isExecutionPlanImpl ( plan ) ) throw new InternalError ( `Not an ExecutionPlanImpl` ) ; const depOpts = { key : "id" as const , removeEmpty : true } ; return { dependencies : { FinishStart : dependencies ( plan , { type : DependencyType . FinishStart , ... depOpts } ) , FinishStartHard : dependencies ( plan , { type : DependencyType . FinishStartHard , ... depOpts } ) , StartStart : dependencies ( plan , { type : DependencyType . StartStart , ... depOpts } ) , } , dom , mountedOrig , plan , } ; } const actResults = await mgr . act ( actOpts ) ; await mgr . finish ( ) ; if ( opts . once || ( actResults . deployComplete && ! actResults . stateChanged ) ) { const stepID = await this . deployment . currentStepID ( this . deployOpID ) ; return { ... actResults , dom , mountedOrig , stepID } ; } } catch ( err ) { err . mockDeployMessages = messagesToString ( this . logger . messages ) ; if ( opts . logError ) { console . log ( `Deploy error:` , err . message , `\nDumping log messages:\n` , err . mockDeployMessages ) ; } throw err ; } } } } /**
 * A plugin that claims ALL primitive DOM elements. For any element
 * that has an `action` prop that's a function, that function will
 * get executed during the element's Action. Otherwise, the Action will
 * be a no-op.
 */ export class BasicTestPlugin implements Plugin < { } > { async start ( ) { } async observe ( ) { return { } ; } analyze ( oldDom , newDom , _obs ) { const diff = domDiff ( oldDom , newDom ) ; const actions = ( elems , ct ) => { return elems . map ( ( el ) => { const act = async ( ) => isFunction ( el . instance . action ) && el . instance . action ( ) ; const type = el . instance . action ? ct : ChangeType . none ; const detail = `Action ${ type } - ${ el . props . key }` ; return { act , type , detail , changes : [ { type , element : el , detail , } ] } ; } ) ; } ; return actions ( [ ... diff . added ] , ChangeType . create ) . concat ( actions ( [ ... diff . commonNew ] , ChangeType . modify ) ) . concat ( actions ( [ ... diff . deleted ] , ChangeType . delete ) ) ; } async finish ( ) { } } export function createBasicTestPlugin ( ) { return new BasicTestPlugin ( ) ; }