UNPKG

9.04 kBPlain TextView Raw
1/**
2 * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty
3 *
4 * @copyright 2016 Huan LI (李卓桓) <https://github.com/huan>, and
5 * Wechaty Contributors <https://github.com/wechaty>.
6 *
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
10 *
11 * http://www.apache.org/licenses/LICENSE-2.0
12 *
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 *
19 */
20import path from 'path'
21
22import readPkgUp from 'read-pkg-up'
23import npm from 'npm-programmatic'
24import pkgDir from 'pkg-dir'
25import semver from 'semver'
26import inGfw from 'in-gfw'
27
28import {
29 Puppet,
30 PuppetImplementation,
31 PuppetOptions,
32} from 'wechaty-puppet'
33
34import { looseInstanceOfClass } from './helper-functions/pure/loose-instance-of-class'
35
36import {
37 log,
38} from './config'
39import {
40 PUPPET_DEPENDENCIES,
41 PuppetModuleName,
42} from './puppet-config'
43
44export interface ResolveOptions {
45 puppet : Puppet | PuppetModuleName,
46 puppetOptions? : PuppetOptions,
47}
48
49/**
50 * Huan(202011):
51 * Create a `looseInstanceOfClass` to check `FileBox` and `Puppet` instances #2090
52 * https://github.com/wechaty/wechaty/issues/2090
53 */
54const looseInstanceOfPuppet = looseInstanceOfClass(Puppet as any as Puppet & { new (...args: any): Puppet })
55
56export class PuppetManager {
57
58 public static async resolve (
59 options: ResolveOptions
60 ): Promise<Puppet> {
61 log.verbose('PuppetManager', 'resolve({puppet: %s, puppetOptions: %s})',
62 options.puppet,
63 JSON.stringify(options.puppetOptions),
64 )
65
66 let puppetInstance: Puppet
67
68 /**
69 * Huan(202001): (DEPRECATED) When we are developing, we might experiencing we have two version of wechaty-puppet installed,
70 * if `options.puppet` is Puppet v1, but the `Puppet` in Wechaty is v2,
71 * then options.puppet will not instanceof Puppet.
72 * So I changed here to match not a string as a workaround.
73 *
74 * Huan(202020): The wechaty-puppet-xxx must NOT dependencies `wechaty-puppet` so that it can be `instanceof`-ed
75 * wechaty-puppet-xxx should put `wechaty-puppet` in `devDependencies` and `peerDependencies`.
76 */
77 if (looseInstanceOfPuppet(options.puppet)) {
78 puppetInstance = await this.resolveInstance(options.puppet)
79 } else if (typeof options.puppet !== 'string') {
80 log.error('PuppetManager', 'resolve() %s',
81 `
82 Wechaty Framework must keep only one Puppet instance #1930
83 See: https://github.com/wechaty/wechaty/issues/1930
84 `,
85 )
86 throw new Error('Wechaty Framework must keep only one Puppet instance #1930')
87 } else {
88 const MyPuppet = await this.resolveName(options.puppet)
89 /**
90 * We will meet the following error:
91 *
92 * [ts] Cannot use 'new' with an expression whose type lacks a call or construct signature.
93 *
94 * When we have different puppet with different `constructor()` args.
95 * For example: PuppetA allow `constructor()` but PuppetB requires `constructor(options)`
96 *
97 * SOLUTION: we enforce all the PuppetImplementation to have `options` and should not allow default parameter.
98 * Issue: https://github.com/wechaty/wechaty-puppet/issues/2
99 */
100
101 /**
102 * Huan(20210313) Issue #2151 - https://github.com/wechaty/wechaty/issues/2151
103 * error TS2511: Cannot create an instance of an abstract class.
104 *
105 * Huan(20210530): workaround by "as any"
106 */
107 puppetInstance = new (MyPuppet as any)(options.puppetOptions)
108 }
109
110 return puppetInstance
111 }
112
113 protected static async resolveName (
114 puppetName: PuppetModuleName,
115 ): Promise<PuppetImplementation> {
116 log.verbose('PuppetManager', 'resolveName(%s)', puppetName)
117
118 if (!puppetName) {
119 throw new Error('must provide a puppet name')
120 }
121
122 if (!(puppetName in PUPPET_DEPENDENCIES)) {
123 throw new Error(
124 [
125 '',
126 'puppet npm module not supported: "' + puppetName + '"',
127 'learn more about supported Wechaty Puppet from our directory at',
128 '<https://github.com/wechaty/wechaty-puppet/wiki/Directory>',
129 '',
130 ].join('\n')
131 )
132 }
133
134 await this.checkModule(puppetName)
135
136 const puppetModule = await import(puppetName)
137
138 if (!puppetModule.default) {
139 throw new Error(`Puppet(${puppetName}) has not provided the default export`)
140 }
141
142 const MyPuppet = puppetModule.default as PuppetImplementation
143
144 return MyPuppet
145 }
146
147 protected static async checkModule (puppetName: PuppetModuleName): Promise<void> {
148 log.verbose('PuppetManager', 'checkModule(%s)', puppetName)
149
150 const versionRange = PUPPET_DEPENDENCIES[puppetName]
151
152 /**
153 * 1. Not Installed
154 */
155 if (!this.installed(puppetName)) {
156 log.silly('PuppetManager', 'checkModule(%s) not installed.', puppetName)
157 await this.install(puppetName, versionRange)
158 return
159 }
160
161 const moduleVersion = this.getModuleVersion(puppetName)
162
163 const satisfy = semver.satisfies(
164 moduleVersion,
165 versionRange,
166 )
167
168 /**
169 * 2. Installed But Version Not Satisfy
170 */
171 if (!satisfy) {
172 log.silly('PuppetManager', 'checkModule() %s installed version %s NOT satisfied range %s',
173 puppetName,
174 moduleVersion,
175 versionRange,
176 )
177 await this.install(puppetName, versionRange)
178 return
179 }
180
181 /**
182 * 3. Installed and Version Satisfy
183 */
184 log.silly('PuppetManager', 'checkModule() %s installed version %s satisfied range %s',
185 puppetName,
186 moduleVersion,
187 versionRange,
188 )
189 }
190
191 protected static getModuleVersion (moduleName: string): string {
192 const modulePath = path.dirname(
193 require.resolve(
194 moduleName,
195 ),
196 )
197 const pkg = readPkgUp.sync({ cwd: modulePath })!.packageJson
198 const version = pkg.version
199
200 return version
201 }
202
203 protected static async resolveInstance (instance: Puppet): Promise<Puppet> {
204 log.verbose('PuppetManager', 'resolveInstance(%s)', instance)
205 // const version = instance.version()
206 // const name = instance.name()
207
208 // const satisfy = semver.satisfies(
209 // version,
210 // puppetConfig.npm.version,
211 // )
212
213 // TODO: check the instance version to satisfy semver
214 return instance
215 }
216
217 protected static installed (moduleName: string): boolean {
218 try {
219 require.resolve(moduleName)
220 return true
221 } catch (e) {
222 return false
223 }
224 }
225
226 private static async preInstallPuppeteer (): Promise<void> {
227 let gfw = false
228 try {
229 gfw = await inGfw()
230 if (gfw) {
231 log.verbose('PuppetManager', 'preInstallPuppeteer() inGfw = true')
232 }
233 } catch (e) {
234 log.verbose('PuppetManager', 'preInstallPuppeteer() exception: %s', e)
235 }
236
237 // https://github.com/GoogleChrome/puppeteer/issues/1597#issuecomment-351945645
238 if (gfw && !process.env['PUPPETEER_DOWNLOAD_HOST']) {
239 log.info('PuppetManager', 'preInstallPuppeteer() set PUPPETEER_DOWNLOAD_HOST=https://npm.taobao.org/mirrors/')
240 process.env['PUPPETEER_DOWNLOAD_HOST'] = 'https://npm.taobao.org/mirrors/'
241 }
242 }
243
244 public static async install (
245 puppetModule: string,
246 puppetVersion = 'latest',
247 ): Promise<void> {
248 log.info('PuppetManager', 'install(%s@%s) please wait ...', puppetModule, puppetVersion)
249
250 if (puppetModule === 'wechaty-puppet-puppeteer') {
251 await this.preInstallPuppeteer()
252 }
253
254 await npm.install(
255 `${puppetModule}@${puppetVersion}`,
256 {
257 cwd : await pkgDir(__dirname),
258 output : true,
259 save : false,
260 },
261 )
262 log.info('PuppetManager', 'install(%s@%s) done', puppetModule, puppetVersion)
263 }
264
265 /**
266 * Install all `wechaty-puppet-*` modules from `puppet-config.ts`
267 */
268 public static async installAll (): Promise<void> {
269 log.info('PuppetManager', 'installAll() please wait ...')
270
271 const skipList = [
272 '@juzibot/wechaty-puppet-donut', // windows puppet
273 '@juzibot/wechaty-puppet-wxwork', // wxwork puppet
274 ]
275
276 const moduleList: string[] = []
277
278 for (const puppetModuleName of Object.keys(PUPPET_DEPENDENCIES)) {
279 const version = PUPPET_DEPENDENCIES[puppetModuleName as PuppetModuleName]
280
281 if (version === '0.0.0' || skipList.includes(puppetModuleName)) {
282 continue
283 }
284
285 moduleList.push(`${puppetModuleName}@${version}`)
286 }
287
288 await npm.install(
289 moduleList,
290 {
291 cwd : await pkgDir(__dirname),
292 output : true,
293 save : false,
294 },
295 )
296
297 }
298
299}