/** * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty * * @copyright 2016 Huan LI (李卓桓) , and * Wechaty Contributors . * * 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. * */ 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 inGfw from 'in-gfw' import { Puppet, PuppetImplementation, PuppetOptions, } from 'wechaty-puppet' import { looseInstanceOfClass } from './helper-functions/pure/loose-instance-of-class' import { log, } from './config' import { PUPPET_DEPENDENCIES, PuppetModuleName, } from './puppet-config' export interface ResolveOptions { puppet : Puppet | PuppetModuleName, puppetOptions? : PuppetOptions, } /** * Huan(202011): * Create a `looseInstanceOfClass` to check `FileBox` and `Puppet` instances #2090 * https://github.com/wechaty/wechaty/issues/2090 */ const looseInstanceOfPuppet = looseInstanceOfClass(Puppet as any as Puppet & { new (...args: any): Puppet }) export class PuppetManager { public static async resolve ( options: ResolveOptions ): Promise { log.verbose('PuppetManager', 'resolve({puppet: %s, puppetOptions: %s})', options.puppet, JSON.stringify(options.puppetOptions), ) let puppetInstance: Puppet /** * Huan(202001): (DEPRECATED) When we are developing, we might experiencing we have two version of wechaty-puppet installed, * if `options.puppet` is Puppet v1, but the `Puppet` in Wechaty is v2, * then options.puppet will not instanceof Puppet. * So I changed here to match not a string as a workaround. * * Huan(202020): The wechaty-puppet-xxx must NOT dependencies `wechaty-puppet` so that it can be `instanceof`-ed * wechaty-puppet-xxx should put `wechaty-puppet` in `devDependencies` and `peerDependencies`. */ if (looseInstanceOfPuppet(options.puppet)) { puppetInstance = await this.resolveInstance(options.puppet) } else if (typeof options.puppet !== 'string') { log.error('PuppetManager', 'resolve() %s', ` Wechaty Framework must keep only one Puppet instance #1930 See: https://github.com/wechaty/wechaty/issues/1930 `, ) throw new Error('Wechaty Framework must keep only one Puppet instance #1930') } 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 PuppetImplementation to have `options` and should not allow default parameter. * Issue: https://github.com/wechaty/wechaty-puppet/issues/2 */ /** * Huan(20210313) Issue #2151 - https://github.com/wechaty/wechaty/issues/2151 * error TS2511: Cannot create an instance of an abstract class. * * Huan(20210530): workaround by "as any" */ puppetInstance = new (MyPuppet as any)(options.puppetOptions) } return puppetInstance } protected static async resolveName ( puppetName: PuppetModuleName, ): Promise { log.verbose('PuppetManager', 'resolveName(%s)', puppetName) if (!puppetName) { throw new Error('must provide a puppet name') } if (!(puppetName in PUPPET_DEPENDENCIES)) { throw new Error( [ '', 'puppet npm module not supported: "' + puppetName + '"', 'learn more about supported Wechaty Puppet from our directory at', '', '', ].join('\n') ) } await this.checkModule(puppetName) const puppetModule = await import(puppetName) if (!puppetModule.default) { throw new Error(`Puppet(${puppetName}) has not provided the default export`) } const MyPuppet = puppetModule.default as PuppetImplementation return MyPuppet } protected static async checkModule (puppetName: PuppetModuleName): Promise { log.verbose('PuppetManager', 'checkModule(%s)', puppetName) const versionRange = PUPPET_DEPENDENCIES[puppetName] /** * 1. Not Installed */ if (!this.installed(puppetName)) { log.silly('PuppetManager', 'checkModule(%s) not 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, ) } protected static getModuleVersion (moduleName: string): string { const modulePath = path.dirname( require.resolve( moduleName, ), ) const pkg = readPkgUp.sync({ cwd: modulePath })!.packageJson const version = pkg.version return version } protected static async resolveInstance (instance: Puppet): Promise { 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 } } private static async preInstallPuppeteer (): Promise { let gfw = false try { gfw = await inGfw() if (gfw) { log.verbose('PuppetManager', 'preInstallPuppeteer() inGfw = true') } } catch (e) { log.verbose('PuppetManager', 'preInstallPuppeteer() exception: %s', e) } // https://github.com/GoogleChrome/puppeteer/issues/1597#issuecomment-351945645 if (gfw && !process.env['PUPPETEER_DOWNLOAD_HOST']) { log.info('PuppetManager', 'preInstallPuppeteer() set PUPPETEER_DOWNLOAD_HOST=https://npm.taobao.org/mirrors/') process.env['PUPPETEER_DOWNLOAD_HOST'] = 'https://npm.taobao.org/mirrors/' } } public static async install ( puppetModule: string, puppetVersion = 'latest', ): Promise { log.info('PuppetManager', 'install(%s@%s) please wait ...', puppetModule, puppetVersion) if (puppetModule === 'wechaty-puppet-puppeteer') { await this.preInstallPuppeteer() } await npm.install( `${puppetModule}@${puppetVersion}`, { cwd : await pkgDir(__dirname), output : true, save : false, }, ) log.info('PuppetManager', 'install(%s@%s) done', puppetModule, puppetVersion) } /** * Install all `wechaty-puppet-*` modules from `puppet-config.ts` */ public static async installAll (): Promise { log.info('PuppetManager', 'installAll() please wait ...') const skipList = [ '@juzibot/wechaty-puppet-donut', // windows puppet '@juzibot/wechaty-puppet-wxwork', // wxwork puppet ] const moduleList: string[] = [] for (const puppetModuleName of Object.keys(PUPPET_DEPENDENCIES)) { const version = PUPPET_DEPENDENCIES[puppetModuleName as PuppetModuleName] if (version === '0.0.0' || skipList.includes(puppetModuleName)) { continue } moduleList.push(`${puppetModuleName}@${version}`) } await npm.install( moduleList, { cwd : await pkgDir(__dirname), output : true, save : false, }, ) } }