1 | import Commander from './commander';
|
2 | import Hook from './hook';
|
3 | import Binp from './universal-pkg/binp';
|
4 | import fs from 'fs';
|
5 | import inquirer from 'inquirer';
|
6 | import logger from './logger';
|
7 | import osenv from 'osenv';
|
8 | import path from 'path';
|
9 | import Table from 'easy-table';
|
10 | import spawn from 'cross-spawn';
|
11 | import loadPlugins from './plugin/loadPlugins';
|
12 | import loadUniversalPlugin from './plugin/loadUniversalPlugin';
|
13 | import loadDevkits from './devkit/loadDevkits';
|
14 | import getCommandLine from './devkit/commandOptions';
|
15 | import {
|
16 | FEFLOW_ROOT,
|
17 | FEFLOW_BIN,
|
18 | FEFLOW_LIB,
|
19 | UNIVERSAL_PKG_JSON,
|
20 | UNIVERSAL_MODULES
|
21 | } from '../shared/constant';
|
22 | import { safeDump, parseYaml } from '../shared/yaml';
|
23 | import packageJson from '../shared/packageJson';
|
24 | import { getRegistryUrl, install } from '../shared/npm';
|
25 | import chalk from 'chalk';
|
26 | import semver from 'semver';
|
27 | import commandLineUsage from 'command-line-usage';
|
28 | import { UniversalPkg } from './universal-pkg/dep/pkg';
|
29 | const pkg = require('../../package.json');
|
30 |
|
31 | export default class Feflow {
|
32 | public args: any;
|
33 | public cmd: any;
|
34 | public projectConfig: any;
|
35 | public projectPath: any;
|
36 | public version: string;
|
37 | public logger: any;
|
38 | public commander: any;
|
39 | public hook: any;
|
40 | public root: any;
|
41 | public rootPkg: any;
|
42 | public universalPkgPath: string;
|
43 | public universalModules: string;
|
44 | public config: any;
|
45 | public configPath: any;
|
46 | public bin: string;
|
47 | public lib: string;
|
48 | public universalPkg: UniversalPkg;
|
49 |
|
50 | constructor(args: any) {
|
51 | args = args || {};
|
52 | const root = path.join(osenv.home(), FEFLOW_ROOT);
|
53 | const configPath = path.join(root, '.feflowrc.yml');
|
54 | this.root = root;
|
55 | const bin = path.join(root, FEFLOW_BIN);
|
56 | const lib = path.join(root, FEFLOW_LIB);
|
57 | this.bin = bin;
|
58 | this.lib = lib;
|
59 | this.rootPkg = path.join(root, 'package.json');
|
60 | this.universalPkgPath = path.join(root, UNIVERSAL_PKG_JSON);
|
61 | this.universalModules = path.join(root, UNIVERSAL_MODULES);
|
62 | this.args = args;
|
63 | this.version = pkg.version;
|
64 | this.config = parseYaml(configPath);
|
65 | this.configPath = configPath;
|
66 | this.commander = new Commander();
|
67 | this.hook = new Hook();
|
68 | this.logger = logger({
|
69 | debug: Boolean(args.debug),
|
70 | silent: Boolean(args.silent)
|
71 | });
|
72 | this.universalPkg = new UniversalPkg(this.universalPkgPath);
|
73 | }
|
74 |
|
75 | async init(cmd: string) {
|
76 | if (cmd === 'config') {
|
77 | await this.initClient();
|
78 | await this.loadNative();
|
79 | } else {
|
80 | await this.initClient();
|
81 | await this.initPackageManager();
|
82 | const disableCheck =
|
83 | !this.args['disable-check'] && !(this.config.disableCheck === 'true');
|
84 | if (disableCheck) {
|
85 | await this.checkCliUpdate();
|
86 | await this.checkUpdate();
|
87 |
|
88 | }
|
89 | await this.loadNative();
|
90 | await this.loadInternalPlugins();
|
91 | await loadPlugins(this);
|
92 | await loadUniversalPlugin(this);
|
93 | await loadDevkits(this);
|
94 | }
|
95 | }
|
96 |
|
97 | initClient() {
|
98 | const { root, rootPkg } = this;
|
99 |
|
100 | return new Promise<any>((resolve, reject) => {
|
101 | if (fs.existsSync(root) && fs.statSync(root).isFile()) {
|
102 | fs.unlinkSync(root);
|
103 | }
|
104 |
|
105 | if (!fs.existsSync(root)) {
|
106 | fs.mkdirSync(root);
|
107 | }
|
108 |
|
109 | if (!fs.existsSync(rootPkg)) {
|
110 | fs.writeFileSync(
|
111 | rootPkg,
|
112 | JSON.stringify(
|
113 | {
|
114 | name: 'feflow-home',
|
115 | version: '0.0.0',
|
116 | private: true
|
117 | },
|
118 | null,
|
119 | 2
|
120 | )
|
121 | );
|
122 | }
|
123 | this.initBinPath();
|
124 | resolve();
|
125 | });
|
126 | }
|
127 |
|
128 | private initBinPath() {
|
129 | const { bin } = this;
|
130 |
|
131 | if (fs.existsSync(bin) && fs.statSync(bin).isFile()) {
|
132 | fs.unlinkSync(bin);
|
133 | }
|
134 |
|
135 | if (!fs.existsSync(bin)) {
|
136 | fs.mkdirSync(bin);
|
137 | }
|
138 | new Binp().register(bin);
|
139 | }
|
140 |
|
141 | initPackageManager() {
|
142 | const { root, logger } = this;
|
143 |
|
144 | return new Promise<any>((resolve, reject) => {
|
145 | if (!this.config || !this.config.packageManager) {
|
146 | const isInstalled = (packageName: string) => {
|
147 | try {
|
148 | const ret = spawn.sync(packageName, ['-v'], { stdio: 'ignore' });
|
149 | if (ret.status !== 0) {
|
150 | return false;
|
151 | }
|
152 | return true;
|
153 | } catch (err) {
|
154 | return false;
|
155 | }
|
156 | };
|
157 |
|
158 | const packageManagers = [
|
159 | {
|
160 | name: 'npm',
|
161 | installed: isInstalled('npm')
|
162 | },
|
163 | {
|
164 | name: 'tnpm',
|
165 | installed: isInstalled('tnpm')
|
166 | },
|
167 | {
|
168 | name: 'cnpm',
|
169 | installed: isInstalled('cnpm')
|
170 | },
|
171 | {
|
172 | name: 'yarn',
|
173 | installed: isInstalled('yarn')
|
174 | }
|
175 | ];
|
176 |
|
177 | const installedPackageManagers = packageManagers.filter(
|
178 | (packageManager) => packageManager.installed
|
179 | );
|
180 |
|
181 | if (installedPackageManagers.length === 0) {
|
182 | const notify = 'You must installed a package manager';
|
183 | console.error(notify);
|
184 | } else {
|
185 | const options = installedPackageManagers.map(
|
186 | (installedPackageManager: any) => {
|
187 | return installedPackageManager.name;
|
188 | }
|
189 | );
|
190 | inquirer
|
191 | .prompt([
|
192 | {
|
193 | type: 'list',
|
194 | name: 'packageManager',
|
195 | message: 'Please select one package manager',
|
196 | choices: options
|
197 | }
|
198 | ])
|
199 | .then((answer: any) => {
|
200 | const configPath = path.join(root, '.feflowrc.yml');
|
201 | safeDump(answer, configPath);
|
202 | this.config = parseYaml(configPath);
|
203 | resolve();
|
204 | });
|
205 | }
|
206 | return;
|
207 | } else {
|
208 | logger.debug('Use packageManager is: ', this.config.packageManager);
|
209 | }
|
210 | resolve();
|
211 | });
|
212 | }
|
213 |
|
214 | checkUpdate() {
|
215 | const { root, rootPkg, config, logger } = this;
|
216 | if (!config) {
|
217 | return;
|
218 | }
|
219 |
|
220 | const table = new Table();
|
221 | const packageManager = config.packageManager;
|
222 | return Promise.all(
|
223 | this.getInstalledPlugins().map(async (name: any) => {
|
224 | const pluginPath = path.join(
|
225 | root,
|
226 | 'node_modules',
|
227 | name,
|
228 | 'package.json'
|
229 | );
|
230 | const content: any = fs.readFileSync(pluginPath);
|
231 | const pkg: any = JSON.parse(content);
|
232 | const localVersion = pkg.version;
|
233 | const registryUrl = await getRegistryUrl(packageManager);
|
234 | const latestVersion: any = await packageJson(name, registryUrl).catch(
|
235 | (err) => {
|
236 | logger.debug('Check plugin update error', err);
|
237 | }
|
238 | );
|
239 |
|
240 | if (latestVersion && semver.gt(latestVersion, localVersion)) {
|
241 | table.cell('Name', name);
|
242 | table.cell(
|
243 | 'Version',
|
244 | localVersion === latestVersion
|
245 | ? localVersion
|
246 | : localVersion + ' -> ' + latestVersion
|
247 | );
|
248 | table.cell('Tag', 'latest');
|
249 | table.cell('Update', localVersion === latestVersion ? 'N' : 'Y');
|
250 | table.newRow();
|
251 |
|
252 | return {
|
253 | name,
|
254 | latestVersion
|
255 | };
|
256 | } else {
|
257 | logger.debug('All plugins is in latest version');
|
258 | }
|
259 | })
|
260 | ).then((plugins: any) => {
|
261 | plugins = plugins.filter((plugin: any) => {
|
262 | return plugin && plugin.name;
|
263 | });
|
264 | if (plugins.length) {
|
265 | this.logger.info(
|
266 | 'It will update your local templates or plugins, this will take few minutes'
|
267 | );
|
268 | console.log(table.toString());
|
269 |
|
270 | this.updatePluginsVersion(rootPkg, plugins);
|
271 |
|
272 | const needUpdatePlugins: any = [];
|
273 | plugins.map((plugin: any) => {
|
274 | needUpdatePlugins.push(plugin.name);
|
275 | });
|
276 |
|
277 | return install(
|
278 | packageManager,
|
279 | root,
|
280 | packageManager === 'yarn' ? 'add' : 'install',
|
281 | needUpdatePlugins,
|
282 | false,
|
283 | true
|
284 | ).then(() => {
|
285 | this.logger.info('Plugin update success');
|
286 | });
|
287 | }
|
288 | });
|
289 | }
|
290 |
|
291 | updatePluginsVersion(packagePath: string, plugins: any) {
|
292 | const obj = require(packagePath);
|
293 |
|
294 | plugins.map((plugin: any) => {
|
295 | obj.dependencies[plugin.name] = plugin.latestVersion;
|
296 | });
|
297 |
|
298 | fs.writeFileSync(packagePath, JSON.stringify(obj, null, 4));
|
299 | }
|
300 |
|
301 | getInstalledPlugins() {
|
302 | const { root, rootPkg } = this;
|
303 |
|
304 | let plugins: any = [];
|
305 | const exist = fs.existsSync(rootPkg);
|
306 | const pluginDir = path.join(root, 'node_modules');
|
307 |
|
308 | if (!exist) {
|
309 | plugins = [];
|
310 | } else {
|
311 | const content: any = fs.readFileSync(rootPkg);
|
312 |
|
313 | let json: any;
|
314 |
|
315 | try {
|
316 | json = JSON.parse(content);
|
317 | const deps = json.dependencies || json.devDependencies || {};
|
318 |
|
319 | plugins = Object.keys(deps);
|
320 | } catch (ex) {
|
321 | plugins = [];
|
322 | }
|
323 | }
|
324 | return plugins.filter((name: any) => {
|
325 | if (
|
326 | !/^feflow-plugin-|^@[^/]+\/feflow-plugin-|generator-|^@[^/]+\/generator-/.test(
|
327 | name
|
328 | )
|
329 | ) {
|
330 | return false;
|
331 | }
|
332 | const pathFn = path.join(pluginDir, name);
|
333 | return fs.existsSync(pathFn);
|
334 | });
|
335 | }
|
336 |
|
337 | loadNative() {
|
338 | return new Promise<any>((resolve, reject) => {
|
339 | const nativePath = path.join(__dirname, './native');
|
340 | fs.readdirSync(nativePath)
|
341 | .filter((file) => {
|
342 | return file.endsWith('.js');
|
343 | })
|
344 | .map((file) => {
|
345 | require(path.join(__dirname, './native', file))(this);
|
346 | });
|
347 | resolve();
|
348 | });
|
349 | }
|
350 |
|
351 | loadInternalPlugins() {
|
352 | ['@feflow/feflow-plugin-devtool'].map((name: string) => {
|
353 | try {
|
354 | this.logger.debug('Plugin loaded: %s', chalk.magenta(name));
|
355 | return require(name)(this);
|
356 | } catch (err) {
|
357 | this.logger.error(
|
358 | { err: err },
|
359 | 'Plugin load failed: %s',
|
360 | chalk.magenta(name)
|
361 | );
|
362 | }
|
363 | });
|
364 | }
|
365 |
|
366 | call(name: any, ctx: any) {
|
367 | const args = ctx.args;
|
368 | if (args.h || args.help) {
|
369 | return this.showCommandOptionDescription(name, ctx);
|
370 | }
|
371 | return new Promise<any>((resolve, reject) => {
|
372 | const cmd = this.commander.get(name);
|
373 | if (cmd) {
|
374 | cmd.call(this, ctx);
|
375 | resolve();
|
376 | } else {
|
377 | reject(
|
378 | new Error('Command `' + name + '` has not been registered yet!')
|
379 | );
|
380 | }
|
381 | });
|
382 | }
|
383 |
|
384 | async updateCli(packageManager: string) {
|
385 | return new Promise((resolve, reject) => {
|
386 | const args =
|
387 | packageManager === 'yarn'
|
388 | ? ['global', 'add', '@feflow/cli@latest', '--extract']
|
389 | : [
|
390 | 'install',
|
391 | '@feflow/cli@latest',
|
392 | '--color=always',
|
393 | '--save',
|
394 | '--save-exact',
|
395 | '--loglevel',
|
396 | 'error',
|
397 | '-g'
|
398 | ];
|
399 |
|
400 | const child = spawn(packageManager, args, { stdio: 'inherit' });
|
401 | child.on('close', (code) => {
|
402 | if (code !== 0) {
|
403 | reject({
|
404 | command: `${packageManager} ${args.join(' ')}`
|
405 | });
|
406 | return;
|
407 | }
|
408 | resolve();
|
409 | });
|
410 | });
|
411 | }
|
412 |
|
413 | async checkCliUpdate() {
|
414 | const { args, version, config, configPath } = this;
|
415 | if (!config) {
|
416 | return;
|
417 | }
|
418 | const packageManager = config.packageManager;
|
419 | const autoUpdate = args['auto-update'] || config.autoUpdate === 'true';
|
420 | if (
|
421 | config.lastUpdateCheck &&
|
422 | +new Date() - parseInt(config.lastUpdateCheck, 10) <= 1000 * 3600 * 24
|
423 | ) {
|
424 | return;
|
425 | }
|
426 | const registryUrl = await getRegistryUrl(packageManager);
|
427 | const latestVersion: any = await packageJson(
|
428 | '@feflow/cli',
|
429 | registryUrl
|
430 | ).catch(() => {
|
431 | this.logger.warn(
|
432 | `Network error, can't reach ${registryUrl}, CLI give up verison check.`
|
433 | );
|
434 | });
|
435 |
|
436 | this.logger.debug(`Auto update: ${autoUpdate}`);
|
437 | if (latestVersion && semver.gt(latestVersion, version)) {
|
438 | this.logger.debug(
|
439 | `Find new version, current version: ${version}, latest version: ${autoUpdate}`
|
440 | );
|
441 | if (autoUpdate) {
|
442 | this.logger.debug(
|
443 | `Auto update version from ${version} to ${latestVersion}`
|
444 | );
|
445 | return await this.updateCli(packageManager);
|
446 | }
|
447 | const askIfUpdateCli = [
|
448 | {
|
449 | type: 'confirm',
|
450 | name: 'ifUpdate',
|
451 | message: `${chalk.yellow(
|
452 | `@feflow/cli's latest version is ${chalk.green(
|
453 | `${latestVersion}`
|
454 | )}, but your version is ${chalk.red(
|
455 | `${version}`
|
456 | )}, Do you want to update it?`
|
457 | )}`,
|
458 | default: true
|
459 | }
|
460 | ];
|
461 | const answer = await inquirer.prompt(askIfUpdateCli);
|
462 | if (answer.ifUpdate) {
|
463 | await this.updateCli(packageManager);
|
464 | } else {
|
465 | safeDump(
|
466 | {
|
467 | ...config,
|
468 | lastUpdateCheck: +new Date()
|
469 | },
|
470 | configPath
|
471 | );
|
472 | }
|
473 | } else {
|
474 | this.logger.debug(`Current version is already latest.`);
|
475 | }
|
476 | }
|
477 |
|
478 | async showCommandOptionDescription(cmd: any, ctx: any): Promise<any> {
|
479 | const registriedCommand = ctx.commander.get(cmd);
|
480 | let commandLine: object[] = [];
|
481 |
|
482 | if (registriedCommand && registriedCommand.options) {
|
483 | commandLine = getCommandLine(
|
484 | registriedCommand.options,
|
485 | registriedCommand.desc,
|
486 | cmd
|
487 | );
|
488 | }
|
489 |
|
490 | if (cmd === 'help') {
|
491 | return registriedCommand.call(this, ctx);
|
492 | }
|
493 | if (commandLine.length == 0) {
|
494 | ctx.logger.warn(`Current command dosen't have help message`);
|
495 | return;
|
496 | }
|
497 |
|
498 | let sections = [];
|
499 |
|
500 | sections.push(...commandLine);
|
501 | const usage = commandLineUsage(sections);
|
502 |
|
503 | console.log(usage);
|
504 | }
|
505 | }
|
506 |
|
\ | No newline at end of file |