提交 ddb10041 编写于 作者: Huan (李卓桓)'s avatar Huan (李卓桓)

refactor puppet-manager

上级 0c23c7ff
......@@ -95,7 +95,7 @@
"rxjs": "^6.1.0",
"state-switch": "^0.6.2",
"watchdog": "^0.8.1",
"wechaty-puppet": "^0.9.3",
"wechaty-puppet": "^0.9.15",
"ws": "^6.0.0"
},
"devDependencies": {
......
......@@ -119,7 +119,7 @@ export class Config {
public apihost = process.env.WECHATY_APIHOST || DEFAULT_SETTING.DEFAULT_APIHOST
public head = ('WECHATY_HEAD' in process.env) ? (!!process.env.WECHATY_HEAD) : (!!(DEFAULT_SETTING.DEFAULT_HEAD))
public systemPuppetName () {
public systemPuppetName (): PuppetName {
return (
process.env.WECHATY_PUPPET || 'default'
).toLowerCase() as PuppetName
......
import clearModule from 'clear-module'
import {
Constructor,
} from 'clone-class'
import npm from 'npm-programmatic'
import pkgDir from 'pkg-dir'
import semver from 'semver'
import {
Puppet,
} from 'wechaty-puppet'
import {
log,
} from './config'
export interface PuppetConfig {
npm: {
name : string,
version : string,
},
}
/**
* Wechaty Official Puppet Plugins List
* Wechaty Official Puppet Implementations List
*/
const mock: PuppetConfig = {
npm: {
name: 'wechaty-puppet-mock',
version: '^0.8.2',
},
}
const wechat4u: PuppetConfig = {
npm: {
name : 'wechaty-puppet-wechat4u',
version : '^0.8.3',
},
}
const padchat: PuppetConfig = {
npm: {
name : 'wechaty-puppet-padchat',
version : '^0.8.1',
},
}
const puppeteer: PuppetConfig = {
npm: {
name: 'wechaty-puppet-puppeteer',
version: '^0.8.2',
},
}
export const PUPPET_DICT = {
default: puppeteer,
//////////////////////////
mock,
padchat,
puppeteer,
wechat4u,
}
export type PuppetName = keyof typeof PUPPET_DICT
export async function puppetResolver (puppet: PuppetName): Promise<typeof Puppet & Constructor<Puppet>> {
log.verbose('PuppetConfig', 'puppetResolver(%s)', puppet)
validatePuppetConfig()
const puppetConfig = PUPPET_DICT[puppet]
if (!puppetConfig) {
throw new Error('no such puppet: ' + puppet)
}
// tslint:disable-next-line:variable-name
let puppetModule: {
VERSION: string,
default: typeof Puppet,
}
try {
puppetModule = await import(puppetConfig.npm.name)
const version = puppetModule.VERSION
if (semver.satisfies(
version,
puppetConfig.npm.version,
)) {
log.silly('PuppetConfig', 'puppetResolver() installed version %s satisfies required version %s for %s',
version,
puppetConfig.npm.version,
puppetConfig.npm.name,
)
} else {
clearModule(puppetConfig.npm.name)
throw new Error(`installed puppet version ${puppetModule.VERSION} is not satisfies config version ${puppetConfig.npm.version}`)
}
} catch (e) {
log.silly('PuppetConfig', 'puppetResolver(%s) exception: %s', puppet, e.message)
try {
await installPuppet(
puppetConfig.npm.name,
puppetConfig.npm.version,
)
puppetModule = await import(puppetConfig.npm.name)
} catch (e) {
log.error('PupptConfig', 'puppetResolver(%s) install fail: %s', puppet, e.message)
throw e
}
}
log.silly('PuppetConfig', 'puppetResolver(%s) import success.', puppet)
return puppetModule.default as typeof Puppet & Constructor<typeof Puppet>
export const PUPPET_DEPENDENCIES = {
'default' : '0.0.0', // will be replaced with PUPPET_DEFAULT
'wechaty-puppet-mock' : '^0.8.2',
'wechaty-puppet-padchat' : '^0.8.1',
'wechaty-puppet-puppeteer' : '^0.8.2',
'wechaty-puppet-wechat4u' : '^0.8.3',
}
async function installPuppet (
puppetNpm: string,
puppetVersion = 'latest',
): Promise<void> {
log.info('PuppetConfig', 'installPuppet(%s@%s) please wait ...', puppetNpm, puppetVersion)
await npm.install(
`${puppetNpm}@${puppetVersion}`,
{
cwd : await pkgDir(__dirname),
output : true,
save : false,
},
)
log.info('PuppetConfig', 'installPuppet(%s) done', puppetNpm)
}
function validatePuppetConfig () {
let puppetName: PuppetName
for (puppetName of Object.keys(PUPPET_DICT) as PuppetName[]) {
const puppetConfig = PUPPET_DICT[puppetName]
const version = puppetConfig.npm.version || '*'
export const PUPPET_DEFAULT = 'wechaty-puppet-puppeteer'
if (!version || !semver.validRange(version)) {
throw new Error(`puppet config version ${version} not valid for ${puppetName}`)
}
}
}
export type PuppetName = keyof typeof PUPPET_DEPENDENCIES
import path from 'path'
import readPkgUp from 'read-pkg-up'
import npm from 'npm-programmatic'
import pkgDir from 'pkg-dir'
import semver from 'semver'
import {
Puppet,
PuppetImplementation,
PuppetOptions,
} from 'wechaty-puppet'
import {
log,
} from './config'
import {
PUPPET_DEFAULT,
PUPPET_DEPENDENCIES,
PuppetName,
} from './puppet-config'
import {
Wechaty,
} from './wechaty'
export interface ResolveOptions {
wechaty : Wechaty,
puppet : Puppet | PuppetName,
puppetOptions? : PuppetOptions,
}
export class PuppetManager {
public static async resolve (
options: ResolveOptions
): Promise<Puppet> {
log.verbose('PuppetManager', 'resolve({wechaty: %s, puppet: %s, puppetOptions: %s})',
options.wechaty,
options.puppet,
JSON.stringify(options.puppetOptions),
)
if (!options.puppet || options.puppet === 'default') {
options.puppet = PUPPET_DEFAULT
}
let puppetInstance: Puppet
if (options.puppet instanceof Puppet) {
puppetInstance = await this.resolveInstance(options.puppet)
} else {
const MyPuppet = await this.resolveName(options.puppet)
/**
* We will meet the following error:
*
* [ts] Cannot use 'new' with an expression whose type lacks a call or construct signature.
*
* When we have different puppet with different `constructor()` args.
* For example: PuppetA allow `constructor()` but PuppetB requires `constructor(options)`
*
* SOLUTION: we enforce all the PuppetImplenmentation to have `options` and should not allow default parameter.
* Issue: https://github.com/Chatie/wechaty-puppet/issues/2
*/
puppetInstance = new MyPuppet(options.puppetOptions)
}
return puppetInstance
}
protected static async resolveName (puppetName: PuppetName): Promise<PuppetImplementation> {
log.verbose('PuppetManager', 'resolveName(%s)', puppetName)
await this.checkModule(puppetName)
const puppetModule = await import(puppetName)
const MyPuppet = puppetModule.default as PuppetImplementation
return MyPuppet
}
protected static async checkModule (puppetName: PuppetName): Promise<void> {
log.verbose('PuppetManager', 'checkModule(%s)', puppetName)
const versionRange = PUPPET_DEPENDENCIES[puppetName]
/**
* 1. Not Installed
*/
if (!this.installed(puppetName)) {
await this.install(puppetName, versionRange)
return
}
const moduleVersion = this.getModuleVersion(puppetName)
const satisfy = semver.satisfies(
moduleVersion,
versionRange,
)
/**
* 2. Installed But Version Not Satisfy
*/
if (!satisfy) {
log.silly('PuppetManager', 'checkModule() %s installed version %s NOT satisfied range %s',
puppetName,
moduleVersion,
versionRange,
)
await this.install(puppetName, versionRange)
return
}
/**
* 3. Installed and Version Satisfy
*/
log.silly('PuppetManager', 'checkModule() %s installed version %s satisfied range %s',
puppetName,
moduleVersion,
versionRange,
)
return
}
protected static getModuleVersion (moduleName: string): string {
const modulePath = path.dirname(
require.resolve(
moduleName,
),
)
const pkg = readPkgUp.sync({ cwd: modulePath }).pkg
const version = pkg.version
return version
}
protected static async resolveInstance (instance: Puppet): Promise<Puppet> {
log.verbose('PuppetManager', 'resolveInstance(%s)', instance)
// const version = instance.version()
// const name = instance.name()
// const satisfy = semver.satisfies(
// version,
// puppetConfig.npm.version,
// )
// TODO: check the instance version to satisfy semver
return instance
}
protected static installed (moduleName: string): boolean {
try {
require.resolve(moduleName)
return true
} catch (e) {
return false
}
}
public static async install (
puppetModule : string,
puppetVersion = 'latest',
): Promise<void> {
log.info('PuppetManager', 'install(%s@%s) please wait ...', puppetModule, puppetVersion)
await npm.install(
`${puppetModule}@${puppetVersion}`,
{
cwd : await pkgDir(__dirname),
output : true,
save : false,
},
)
log.info('PuppetManager', 'install(%s@%s) done', puppetModule, puppetVersion)
}
}
......@@ -325,7 +325,7 @@ export class Room extends Accessory implements Sayable {
}
/**
* @private
* @hidden
*/
public async ready (
dirty = false,
......@@ -356,7 +356,7 @@ export class Room extends Accessory implements Sayable {
}
/**
* @private
* @hidden
*/
public isReady (): boolean {
return !!(this.payload)
......
......@@ -76,7 +76,7 @@ test('Config setting', async t => {
})
test('event:start/stop', async t => {
const wechaty = new Wechaty({ puppet: 'mock' })
const wechaty = new Wechaty({ puppet: 'wechaty-puppet-mock' })
const startSpy = sinon.spy()
const stopSpy = sinon.spy()
......
......@@ -19,7 +19,6 @@
*/
import cuid from 'cuid'
import os from 'os'
import semver from 'semver'
import {
// Constructor,
......@@ -70,8 +69,10 @@ import {
} from './io'
import {
PuppetName,
puppetResolver,
} from './puppet-config'
import {
PuppetManager,
} from './puppet-manager'
import {
Contact,
......@@ -229,9 +230,9 @@ export class Wechaty extends Accessory implements Sayable {
? null
: (options.profile || config.default.DEFAULT_PROFILE)
this.id = cuid()
this.memory = new MemoryCard(options.profile || undefined)
this.state = new StateSwitch('Wechaty', log)
this.id = cuid()
/**
* @ignore
......@@ -242,11 +243,11 @@ export class Wechaty extends Accessory implements Sayable {
* https://github.com/Microsoft/TypeScript/issues/19197
*/
// TODO: make Message & Room constructor private???
this.Contact = cloneClass(Contact)
this.ContactSelf = cloneClass(ContactSelf)
this.Friendship = cloneClass(Friendship)
this.Message = cloneClass(Message)
this.Room = cloneClass(Room)
this.Contact = cloneClass(Contact)
this.ContactSelf = cloneClass(ContactSelf)
this.Friendship = cloneClass(Friendship)
this.Message = cloneClass(Message)
this.Room = cloneClass(Room)
this.RoomInvitation = cloneClass(RoomInvitation)
}
......@@ -525,95 +526,22 @@ export class Wechaty extends Accessory implements Sayable {
return
}
const puppet = await this.initPuppetResolver(this.options.puppet)
this.initPuppetVersionSatisfy(puppet)
this.initPuppetEventBridge(puppet)
this.initPuppetAccessory(puppet)
}
/**
* @private
*/
private initPuppetVersionSatisfy (puppet: Puppet): void {
log.verbose('Wechaty', 'initPuppetVersionSatisfy(%s)', puppet)
if (this.initPuppetSemverSatisfy(
puppet.wechatyVersionRange(),
)) {
return
}
throw new Error(`The Puppet Plugin(${puppet.constructor.name}) `
+ `requires a version range(${puppet.wechatyVersionRange()}) `
+ `that is not satisfying the Wechaty version: ${this.version()}.`,
)
}
/**
* @private
*
* Init the Puppet
*/
private async initPuppetResolver (puppet?: PuppetName | Puppet): Promise<Puppet> {
log.verbose('Wechaty', 'initPuppetResolver(%s)', puppet)
if (!puppet) {
puppet = config.systemPuppetName()
log.info('Wechaty', 'initPuppet() using puppet: %s', puppet)
}
const puppet = this.options.puppet || config.systemPuppetName()
const puppetMemory = this.memory.sub(puppet.toString())
let puppetInstance: Puppet
if (typeof puppet === 'string') {
// tslint:disable-next-line:variable-name
const MyPuppet = await puppetResolver(puppet)
if (!MyPuppet) {
throw new Error('no such puppet: ' + puppet)
}
/**
* We will meet the following error:
*
* [ts] Cannot use 'new' with an expression whose type lacks a call or construct signature.
*
* When we have different puppet with different `constructor()` args.
* For example: PuppetA allow `constructor()` but PuppetB requires `constructor(options)`
*
* SOLUTION: we enforce all the PuppetImplenmentation to have `options` and should not allow default parameter.
* Issue: https://github.com/Chatie/wechaty-puppet/issues/2
*/
puppetInstance = new MyPuppet(this.options.puppetOptions)
} else if (puppet instanceof Puppet) {
puppetInstance = puppet
} else {
throw new Error('unsupported options.puppet: ' + puppet)
}
const puppetInstance = await PuppetManager.resolve({
puppet,
puppetOptions : this.options.puppetOptions,
wechaty : this,
})
// give puppet the memory
/**
* Plug the Memory Card to Puppet
*/
puppetInstance.setMemory(puppetMemory)
log.info('Wechaty', 'initPuppet() inited puppet: %s', puppetInstance.toString())
return puppetInstance
}
/**
* @private
*
* Plugin Version Range Check
*/
private initPuppetSemverSatisfy (versionRange: string) {
log.verbose('Wechaty', 'initPuppetSemverSatisfy(%s)', versionRange)
return semver.satisfies(
this.version(true),
versionRange,
)
this.initPuppetEventBridge(puppetInstance)
this.initPuppetAccessory(puppetInstance)
}
protected initPuppetEventBridge (puppet: Puppet) {
......@@ -767,24 +695,24 @@ export class Wechaty extends Accessory implements Sayable {
/**
* 1. Set Wechaty
*/
this.Contact.wechaty = this
this.ContactSelf.wechaty = this
this.Friendship.wechaty = this
this.Message.wechaty = this
this.Room.wechaty = this
this.Contact.wechaty = this
this.ContactSelf.wechaty = this
this.Friendship.wechaty = this
this.Message.wechaty = this
this.Room.wechaty = this
this.RoomInvitation.wechaty = this
/**
* 2. Set Puppet
*/
this.Contact.puppet = puppet
this.ContactSelf.puppet = puppet
this.Friendship.puppet = puppet
this.Message.puppet = puppet
this.Room.puppet = puppet
this.Contact.puppet = puppet
this.ContactSelf.puppet = puppet
this.Friendship.puppet = puppet
this.Message.puppet = puppet
this.Room.puppet = puppet
this.RoomInvitation.puppet = puppet
this.puppet = puppet
this.puppet = puppet
}
/**
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册