puppet-manager.ts 6.8 KB
Newer Older
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1 2
import path       from 'path'

3 4 5 6 7
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'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
8 9 10 11 12 13 14 15 16 17 18 19

import {
  Puppet,
  PuppetImplementation,
  PuppetOptions,
}                         from 'wechaty-puppet'

import {
  log,
}                       from './config'
import {
  PUPPET_DEPENDENCIES,
20
  PuppetModuleName,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
21 22 23
}                       from './puppet-config'

export interface ResolveOptions {
24
  puppet         : Puppet | PuppetModuleName,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
25 26 27 28 29 30 31 32
  puppetOptions? : PuppetOptions,
}

export class PuppetManager {

  public static async resolve (
    options: ResolveOptions
  ): Promise<Puppet> {
Huan (李卓桓)'s avatar
fix log  
Huan (李卓桓) 已提交
33
    log.verbose('PuppetManager', 'resolve({puppet: %s, puppetOptions: %s})',
34 35
      options.puppet,
      JSON.stringify(options.puppetOptions),
36
    )
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52

    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.
53
       * Issue: https://github.com/wechaty/wechaty-puppet/issues/2
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
54 55 56 57 58 59 60
       */
      puppetInstance = new MyPuppet(options.puppetOptions)
    }

    return puppetInstance
  }

61
  protected static async resolveName (puppetName: PuppetModuleName): Promise<PuppetImplementation> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
62 63
    log.verbose('PuppetManager', 'resolveName(%s)', puppetName)

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
64 65 66 67
    if (!puppetName) {
      throw new Error('must provide a puppet name')
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
68
    // TODO(huan): remove the unnecessary switch
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
69
    switch (puppetName) {
70
      // case 'padchat':
71
      //   // issue #1496 https://github.com/wechaty/wechaty/issues/1496
72 73 74
      //   // compatible old settings for padchat
      //   puppetName = 'wechaty-puppet-padchat'
      //   break
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
75

76 77 78
      // case 'mock':
      //   puppetName = 'wechaty-puppet-mock'
      //   break
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
79

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
80 81 82
      // case 'default':
      //   puppetName = PUPPET_NAME_DEFAULT
      //   break
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
83 84 85 86 87 88 89 90

      default:
        if (!(puppetName in PUPPET_DEPENDENCIES)) {
          throw new Error(
            [
              '',
              'puppet npm module not supported: "' + puppetName + '"',
              'learn more about supported Wechaty Puppet from our directory at',
91
              '<https://github.com/wechaty/wechaty-puppet/wiki/Directory>',
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
92 93 94 95
              '',
            ].join('\n')
          )
        }
96
        // PuppetName is valid
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
97 98 99
        break
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
100 101 102
    await this.checkModule(puppetName)

    const puppetModule = await import(puppetName)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
103
    const MyPuppet     = puppetModule.default as PuppetImplementation
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
104 105 106 107

    return MyPuppet
  }

108
  protected static async checkModule (puppetName: PuppetModuleName): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
109 110 111 112 113 114 115 116
    log.verbose('PuppetManager', 'checkModule(%s)', puppetName)

    const versionRange = PUPPET_DEPENDENCIES[puppetName]

    /**
     * 1. Not Installed
     */
    if (!this.installed(puppetName)) {
Huan (李卓桓)'s avatar
Add log  
Huan (李卓桓) 已提交
117
      log.silly('PuppetManager', 'checkModule(%s) not installed.', puppetName)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
      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',
134 135 136
        puppetName,
        moduleVersion,
        versionRange,
137
      )
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
138 139 140 141 142 143 144 145
      await this.install(puppetName, versionRange)
      return
    }

    /**
     * 3. Installed and Version Satisfy
     */
    log.silly('PuppetManager', 'checkModule() %s installed version %s satisfied range %s',
146 147 148
      puppetName,
      moduleVersion,
      versionRange,
149
    )
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
150 151 152 153 154 155 156 157
  }

  protected static getModuleVersion (moduleName: string): string {
    const modulePath = path.dirname(
      require.resolve(
        moduleName,
      ),
    )
158
    const pkg     = readPkgUp.sync({ cwd: modulePath })!.package
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
    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
    }
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
187
  private static async preInstallPuppeteer (): Promise<void> {
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
    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/'
    }
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
205
  public static async install (
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
206
    puppetModule: string,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
207 208 209
    puppetVersion = 'latest',
  ): Promise<void> {
    log.info('PuppetManager', 'install(%s@%s) please wait ...', puppetModule, puppetVersion)
210

211 212
    if (puppetModule === 'wechaty-puppet-puppeteer') {
      await this.preInstallPuppeteer()
213 214
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
215 216 217 218 219 220 221 222 223 224 225
    await npm.install(
      `${puppetModule}@${puppetVersion}`,
      {
        cwd    : await pkgDir(__dirname),
        output : true,
        save   : false,
      },
    )
    log.info('PuppetManager', 'install(%s@%s) done', puppetModule, puppetVersion)
  }

226 227 228
  /**
   * Install all `wechaty-puppet-*` modules from `puppet-config.ts`
   */
229 230 231
  public static async installAll (): Promise<void> {
    log.info('PuppetManager', 'installAll() please wait ...')

232 233
    const moduleList: string[] = []

234
    for (const puppetModuleName of Object.keys(PUPPET_DEPENDENCIES)) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
235
      const version = PUPPET_DEPENDENCIES[puppetModuleName as PuppetModuleName]
236 237
      if (version !== '0.0.0') {
        moduleList.push(`${puppetModuleName}@${version}`)
238 239
      }
    }
240 241 242 243 244 245 246 247 248 249

    await npm.install(
      moduleList,
      {
        cwd    : await pkgDir(__dirname),
        output : true,
        save   : false,
      },
    )

250
  }
251

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
252
}