UNPKG

8.36 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 {
35 log,
36} from './config'
37import {
38 PUPPET_DEPENDENCIES,
39 PuppetModuleName,
40} from './puppet-config'
41
42export interface ResolveOptions {
43 puppet : Puppet | PuppetModuleName,
44 puppetOptions? : PuppetOptions,
45}
46
47export class PuppetManager {
48
49 public static async resolve (
50 options: ResolveOptions
51 ): Promise<Puppet> {
52 log.verbose('PuppetManager', 'resolve({puppet: %s, puppetOptions: %s})',
53 options.puppet,
54 JSON.stringify(options.puppetOptions),
55 )
56
57 let puppetInstance: Puppet
58
59 /**
60 * Huan(202001): (DEPRECATED) When we are developing, we might experiencing we have two version of wechaty-puppet installed,
61 * if `optoins.puppet` is Puppet v1, but the `Puppet` in Wechaty is v2,
62 * then options.puppet will not instanceof Puppet.
63 * So I changed here to match not a string as a workaround.
64 *
65 * Huan(202020): The wechaty-puppet-xxx must NOT dependencies `wechaty-puppet` so that it can be `instanceof`-ed
66 * wechaty-puppet-xxx should put `wechaty-puppet` in `devDependencies` and `peerDependencies`.
67 */
68 if (options.puppet instanceof Puppet) {
69 puppetInstance = await this.resolveInstance(options.puppet)
70 } else if (typeof options.puppet !== 'string') {
71 log.error('PuppetManager', 'resolve() %s',
72 `
73 Wechaty Framework must keep only one Puppet instance #1930
74 See: https://github.com/wechaty/wechaty/issues/1930
75 `,
76 )
77 throw new Error('Wechaty Framework must keep only one Puppet instance #1930')
78 } else {
79 const MyPuppet = await this.resolveName(options.puppet)
80 /**
81 * We will meet the following error:
82 *
83 * [ts] Cannot use 'new' with an expression whose type lacks a call or construct signature.
84 *
85 * When we have different puppet with different `constructor()` args.
86 * For example: PuppetA allow `constructor()` but PuppetB requires `constructor(options)`
87 *
88 * SOLUTION: we enforce all the PuppetImplenmentation to have `options` and should not allow default parameter.
89 * Issue: https://github.com/wechaty/wechaty-puppet/issues/2
90 */
91 puppetInstance = new MyPuppet(options.puppetOptions)
92 }
93
94 return puppetInstance
95 }
96
97 protected static async resolveName (
98 puppetName: PuppetModuleName,
99 ): Promise<PuppetImplementation> {
100 log.verbose('PuppetManager', 'resolveName(%s)', puppetName)
101
102 if (!puppetName) {
103 throw new Error('must provide a puppet name')
104 }
105
106 if (!(puppetName in PUPPET_DEPENDENCIES)) {
107 throw new Error(
108 [
109 '',
110 'puppet npm module not supported: "' + puppetName + '"',
111 'learn more about supported Wechaty Puppet from our directory at',
112 '<https://github.com/wechaty/wechaty-puppet/wiki/Directory>',
113 '',
114 ].join('\n')
115 )
116 }
117
118 await this.checkModule(puppetName)
119
120 const puppetModule = await import(puppetName)
121
122 if (!puppetModule.default) {
123 throw new Error(`Puppet(${puppetName}) has not provided the default export`)
124 }
125
126 const MyPuppet = puppetModule.default as PuppetImplementation
127
128 return MyPuppet
129 }
130
131 protected static async checkModule (puppetName: PuppetModuleName): Promise<void> {
132 log.verbose('PuppetManager', 'checkModule(%s)', puppetName)
133
134 const versionRange = PUPPET_DEPENDENCIES[puppetName]
135
136 /**
137 * 1. Not Installed
138 */
139 if (!this.installed(puppetName)) {
140 log.silly('PuppetManager', 'checkModule(%s) not installed.', puppetName)
141 await this.install(puppetName, versionRange)
142 return
143 }
144
145 const moduleVersion = this.getModuleVersion(puppetName)
146
147 const satisfy = semver.satisfies(
148 moduleVersion,
149 versionRange,
150 )
151
152 /**
153 * 2. Installed But Version Not Satisfy
154 */
155 if (!satisfy) {
156 log.silly('PuppetManager', 'checkModule() %s installed version %s NOT satisfied range %s',
157 puppetName,
158 moduleVersion,
159 versionRange,
160 )
161 await this.install(puppetName, versionRange)
162 return
163 }
164
165 /**
166 * 3. Installed and Version Satisfy
167 */
168 log.silly('PuppetManager', 'checkModule() %s installed version %s satisfied range %s',
169 puppetName,
170 moduleVersion,
171 versionRange,
172 )
173 }
174
175 protected static getModuleVersion (moduleName: string): string {
176 const modulePath = path.dirname(
177 require.resolve(
178 moduleName,
179 ),
180 )
181 const pkg = readPkgUp.sync({ cwd: modulePath })!.packageJson
182 const version = pkg.version
183
184 return version
185 }
186
187 protected static async resolveInstance (instance: Puppet): Promise<Puppet> {
188 log.verbose('PuppetManager', 'resolveInstance(%s)', instance)
189 // const version = instance.version()
190 // const name = instance.name()
191
192 // const satisfy = semver.satisfies(
193 // version,
194 // puppetConfig.npm.version,
195 // )
196
197 // TODO: check the instance version to satisfy semver
198 return instance
199 }
200
201 protected static installed (moduleName: string): boolean {
202 try {
203 require.resolve(moduleName)
204 return true
205 } catch (e) {
206 return false
207 }
208 }
209
210 private static async preInstallPuppeteer (): Promise<void> {
211 let gfw = false
212 try {
213 gfw = await inGfw()
214 if (gfw) {
215 log.verbose('PuppetManager', 'preInstallPuppeteer() inGfw = true')
216 }
217 } catch (e) {
218 log.verbose('PuppetManager', 'preInstallPuppeteer() exception: %s', e)
219 }
220
221 // https://github.com/GoogleChrome/puppeteer/issues/1597#issuecomment-351945645
222 if (gfw && !process.env.PUPPETEER_DOWNLOAD_HOST) {
223 log.info('PuppetManager', 'preInstallPuppeteer() set PUPPETEER_DOWNLOAD_HOST=https://npm.taobao.org/mirrors/')
224 process.env.PUPPETEER_DOWNLOAD_HOST = 'https://npm.taobao.org/mirrors/'
225 }
226 }
227
228 public static async install (
229 puppetModule: string,
230 puppetVersion = 'latest',
231 ): Promise<void> {
232 log.info('PuppetManager', 'install(%s@%s) please wait ...', puppetModule, puppetVersion)
233
234 if (puppetModule === 'wechaty-puppet-puppeteer') {
235 await this.preInstallPuppeteer()
236 }
237
238 await npm.install(
239 `${puppetModule}@${puppetVersion}`,
240 {
241 cwd : await pkgDir(__dirname),
242 output : true,
243 save : false,
244 },
245 )
246 log.info('PuppetManager', 'install(%s@%s) done', puppetModule, puppetVersion)
247 }
248
249 /**
250 * Install all `wechaty-puppet-*` modules from `puppet-config.ts`
251 */
252 public static async installAll (): Promise<void> {
253 log.info('PuppetManager', 'installAll() please wait ...')
254
255 const skipList = [
256 '@juzibot/wechaty-puppet-donut', // windows puppet
257 ]
258
259 const moduleList: string[] = []
260
261 for (const puppetModuleName of Object.keys(PUPPET_DEPENDENCIES)) {
262 const version = PUPPET_DEPENDENCIES[puppetModuleName as PuppetModuleName]
263
264 if (version === '0.0.0' || skipList.includes(puppetModuleName)) {
265 continue
266 }
267
268 moduleList.push(`${puppetModuleName}@${version}`)
269 }
270
271 await npm.install(
272 moduleList,
273 {
274 cwd : await pkgDir(__dirname),
275 output : true,
276 save : false,
277 },
278 )
279
280 }
281
282}