| import { build, fs, IParameters, http, install, logger, strings, semver, inquirer } from '../index' |
| import { parseParams } from '../toolbox/parameter-tools' |
| import { machineIdSync } from 'node-machine-id' |
| import * as path from 'path' |
| |
| const debug = require('debug')('weex:core') |
| |
| export enum ModType { |
| PLUGIN, |
| EXTENSION, |
| } |
| |
| export enum ErrorType { |
| PACKAGE_VERSION_NOT_FOUND = 101, |
| PACKAGE_NOT_FOUND, |
| } |
| export interface ModData { |
| mods: { |
| [key: string]: ModItem |
| } |
| last_update_time: number |
| } |
| |
| export interface Command { |
| name?: string |
| alias?: string |
| dashed?: boolean |
| description?: string |
| } |
| |
| export interface ModItem { |
| type: ModType |
| version: string |
| description: string |
| dependencies?: { |
| [key: string]: string |
| } |
| next_version?: string |
| is_next?: boolean |
| changelog?: string |
| local: string |
| commands?: string[] |
| } |
| export interface PluginItem { |
| value?: string |
| options?: any |
| commands?: { |
| name: string |
| alias: string |
| description: string |
| showed: boolean |
| }[] |
| name?: string |
| } |
| |
| export interface CliConfiguration { |
| cliVersion: string |
| corePath: string |
| coreName: string |
| coreRoot: string |
| moduleRoot: string |
| moduleConfigFileName: string |
| home: string |
| registry: string |
| argv: string |
| trash: string |
| modules: ModData |
| globalConfigFileName: string |
| force?: boolean |
| configs?: { |
| [key: string]: any |
| } |
| ENVS?: any |
| } |
| |
| export interface CoreOptions { |
| brand?: string |
| help?: any |
| defaultCommand?: any |
| version?: any |
| plugin?: { |
| value?: string |
| options?: any |
| } |
| plugins?: { |
| value?: string |
| options?: any |
| } |
| exclude?: string[] |
| } |
| // use for analyzer function |
| let cliConfiguration: CliConfiguration |
| |
| export default class Cli { |
| private cli: any |
| private updateTime: number |
| private rawArgv: string[] | string |
| private argv: IParameters |
| private plugins: PluginItem[] = [] |
| private cliConfiguration: CliConfiguration |
| constructor(data: CliConfiguration, options: CoreOptions = {}) { |
| debug('Init Cli instance on <%s>', __filename) |
| this.rawArgv = data.argv |
| |
| this.updateTime = data.configs.update_time || 7 |
| |
| this.argv = parseParams(data.argv) |
| |
| this.cliConfiguration = cliConfiguration = { |
| ...data, |
| force: this.argv.options.f || this.argv.options.force, |
| } |
| // create a CLI runtime |
| this.cli = build(options.brand || 'weex') |
| .src(__dirname) |
| .help(options.help) |
| .version(options.version) |
| .exclude(options.exclude) |
| .defaultCommand(options.defaultCommand) |
| |
| if (options.plugins && options.plugins.value) { |
| this.cli = this.cli.plugins(options.plugins.value, options.plugins.options) |
| } |
| |
| if (!this.cliConfiguration.modules.mods) { |
| this.cliConfiguration.modules['mods'] = {} |
| this.cliConfiguration.modules['last_update_time'] = new Date().getTime() |
| } |
| |
| if (options.plugin) { |
| if (Array.isArray(options.plugin)) { |
| options.plugin.forEach(p => { |
| this.cli = this.cli.plugin(p.value, p.options) |
| }) |
| } else { |
| this.cli = this.cli.plugin(options.plugin.value, options.plugin.options) |
| } |
| } |
| } |
| |
| async start() { |
| const command = this.argv.array[0] |
| const moduleConfigFilePath = path.join(this.cliConfiguration.moduleRoot, this.cliConfiguration.moduleConfigFileName) |
| const pkg = require('../../package.json') |
| |
| const traceData = { |
| cmd: command || '', |
| argv: this.argv.array.join('+'), |
| mid: machineIdSync(), |
| node: process.version, |
| core: pkg.version |
| } |
| usertrack('usage', traceData, this.cliConfiguration.configs.telemetry) |
| |
| if (this.cliConfiguration.modules) { |
| this.plugins = pickPlugins(this.cliConfiguration.modules) |
| } else { |
| fs.write(moduleConfigFilePath, { |
| mods: {}, |
| last_update_time: new Date().getTime(), |
| }) |
| } |
| if (command === 'repair') { |
| debug(`Do repair`) |
| } else if (command === 'config') { |
| debug('Do config') |
| } else if (command === 'install') { |
| debug('Do install') |
| } else if (command === 'uninstall') { |
| debug('Do uninstall') |
| } else if (command) { |
| const plugin = searchPlugin(command, this.plugins) |
| let commands = [] |
| let type = ModType.EXTENSION |
| // If the module has been instll, skip |
| if (!plugin || !plugin.name) { |
| let packageSubName = command |
| if (command === 'create' || command === 'platform') { |
| packageSubName = 'generator' |
| } |
| const res: { error?: string; [key: string]: any } = await suggestPackage( |
| packageSubName, |
| this.cliConfiguration.configs.registry, |
| ) |
| if (res && !res.error) { |
| const packages: any = await installPackage(this.cliConfiguration, `@weex-cli/${packageSubName}`, 'latest', { |
| root: this.cliConfiguration.moduleRoot, |
| registry: this.cliConfiguration.configs.registry, |
| force: this.cliConfiguration.force, |
| ENVS: this.cliConfiguration.configs.ENVS, |
| }) |
| for (let i = 0; i < packages.length; i++) { |
| const commandBasePath = path.join(packages[i].root, 'commands') |
| const extensionBasePath = path.join(packages[i].root, 'extensions') |
| const commandFiles: string[] = fs.list(commandBasePath) || [] |
| commands = [] |
| // continue next package while the package has not commands and extensions folder |
| if (!fs.existsAsync(commandBasePath) && !fs.existsAsync(extensionBasePath)) { |
| continue |
| } |
| commandFiles.forEach(file => { |
| let content: Command = {} |
| try { |
| content = require(path.join(commandBasePath, file)) |
| } catch (e) { |
| debug(`Check module error with: ${e.stack}`) |
| // try prev version |
| } |
| if (content.name) { |
| commands.push({ |
| name: content.name || '', |
| alias: content.alias || '', |
| showed: typeof content.dashed === 'boolean' ? !content.dashed : true, |
| description: content.description || '', |
| }) |
| type = ModType.PLUGIN |
| } else { |
| type = ModType.EXTENSION |
| } |
| }) |
| |
| this.cliConfiguration.modules.mods[packages[i].package.name] = { |
| type: type, |
| version: packages[i].package.version, |
| description: packages[i].package.description || '', |
| dependencies: packages[i].package.pluginDependencies, |
| next_version: '', |
| is_next: true, |
| changelog: packages[i].package.changelog || '', |
| local: packages[i].root, |
| commands: commands, |
| } |
| this.plugins.push({ |
| value: packages[i].root, |
| options: {}, |
| commands: commands, |
| name: packages[i].package.name, |
| }) |
| } |
| // update module file |
| fs.write(moduleConfigFilePath, { |
| mods: this.cliConfiguration.modules.mods, |
| last_update_time: new Date().getTime(), |
| }) |
| } |
| } else { |
| // check if there has some module need to be upgraded |
| // check last_update_time |
| const info: any = await updateNpmPackageInfo( |
| this.cliConfiguration.modules, |
| this.cliConfiguration.configs.registry, |
| this.updateTime, |
| ) |
| if (info) { |
| let upgradeList = {} |
| for (let mod in info.mods) { |
| if (!info.mods[mod].is_next) { |
| upgradeList[mod] = info.mods[mod] |
| } |
| } |
| const generateChangelog = (changelog: any[] | string): string => { |
| if (!changelog) { |
| return '' |
| } |
| if (typeof changelog === 'string') { |
| changelog = [changelog] |
| } |
| changelog = changelog.map(log => `\n • ${log}`) |
| return `\n changelog:${changelog.join('')}` |
| } |
| |
| let lists = Object.keys(upgradeList).map(key => { |
| return { |
| name: `${key} ${logger.colors.grey( |
| `${upgradeList[key].version} -> ${upgradeList[key].next_version}${generateChangelog( |
| upgradeList[key].changelog, |
| )}`, |
| )}`, |
| value: key, |
| } |
| }) |
| let yes = 'Yes, update all' |
| let no = 'No, next time' |
| let choices = [yes, no, new inquirer.Separator('Or choose update package')].concat(lists) |
| if (lists.length > 0) { |
| let res = await inquirer.prompt({ |
| name: 'choose', |
| type: 'list', |
| choices: choices, |
| default: yes, |
| message: 'New update detected, update now?', |
| }) |
| if (yes === res.choose) { |
| for (let item in upgradeList) { |
| await repairPackage(this.cliConfiguration, item, upgradeList[item].next_version) |
| logger.success( |
| `[${logger.checkmark}] Upgrade ${item} ${upgradeList[item].version} -> ${ |
| upgradeList[item].next_version |
| } success`, |
| ) |
| } |
| logger.success(`All task completed.`) |
| } else if (no !== res.choose) { |
| await repairPackage(this.cliConfiguration, res.choose, upgradeList[res.choose].next_version) |
| logger.success( |
| `[${logger.checkmark}] Upgrade ${res.choose} ${upgradeList[res.choose].version} -> ${ |
| upgradeList[res.choose].next_version |
| } success`, |
| ) |
| } |
| } |
| fs.write(moduleConfigFilePath, info) |
| } |
| } |
| if (this.plugins.length > 0) { |
| this.plugins.forEach(p => { |
| this.cli = this.cli.plugin(p.value, p.options) |
| }) |
| } |
| } |
| // run the cli |
| const toolbox = await this.cli.create().run(this.rawArgv, { __config: this.cliConfiguration, __analyzer: analyzer }) |
| // send it back (for testing, mostly) |
| return toolbox |
| } |
| } |
| |
| /** |
| * Repair npm package |
| * |
| * @param name npm package name |
| * @param version npm package version |
| */ |
| export async function repairPackage(config: CliConfiguration, name: string, version: string) { |
| let commands = [] |
| let type = ModType.EXTENSION |
| const moduleConfigFilePath = path.join(config.moduleRoot, config.moduleConfigFileName) |
| const packages: any = await installPackage(config, name, version, { |
| root: config.moduleRoot, |
| registry: config && config.configs && config.configs.registry, |
| force: config.force, |
| ENVS: config.ENVS, |
| }) |
| for (let i = 0; i < packages.length; i++) { |
| let commandBasePath = path.join(packages[i].root, 'commands') |
| let commandFiles: string[] = fs.list(commandBasePath) || [] |
| commandFiles.forEach(file => { |
| let content |
| try { |
| content = require(path.join(commandBasePath, file)) |
| } catch (e) { |
| debug(`Check module error with: ${e.stack}`) |
| // try prev version |
| } |
| commands.push({ |
| name: (content && content.name) || '', |
| alias: (content && content.alias) || '', |
| showed: content && typeof content.dashed === 'boolean' ? !content.dashed : true, |
| description: (content && content.description) || '', |
| }) |
| type = ModType.PLUGIN |
| }) |
| if (commands.length > 0) { |
| config.modules.mods[packages[i].package.name] = { |
| type: type, |
| version: packages[i].package.version, |
| description: packages[i].package.description || '', |
| dependencies: packages[i].package.pluginDependencies, |
| next_version: '', |
| is_next: true, |
| changelog: packages[i].package.changelog || '', |
| local: packages[i].root, |
| commands: commands, |
| } |
| commands = [] |
| } else { |
| config.modules.mods[packages[i].package.name] = { |
| type: type, |
| version: packages[i].package.version, |
| description: packages[i].package.description || '', |
| dependencies: packages[i].package.pluginDependencies, |
| next_version: '', |
| is_next: true, |
| changelog: packages[i].package.changelog || '', |
| local: packages[i].root, |
| } |
| } |
| } |
| debug(`save modjson: ${JSON.stringify(config.modules.mods)}`) |
| // update module file |
| fs.write(moduleConfigFilePath, { |
| mods: config.modules.mods, |
| last_update_time: new Date().getTime(), |
| }) |
| } |
| |
| /** |
| * Install packages |
| * |
| * @param name package name |
| * @param version package version |
| * @param options install options |
| * @param result data from reduce |
| */ |
| export async function installPackage( |
| config: CliConfiguration, |
| name: string, |
| version: string, |
| options: any, |
| result: any = [], |
| ) { |
| const info: any = await install(name, version || 'latest', options) |
| let res = result.concat(info) |
| if (info.package.pluginDependencies) { |
| for (let name in info.package.pluginDependencies) { |
| res = await installPackage(config, name, info.package.pluginDependencies[name], options, res) |
| } |
| } |
| return res |
| } |
| |
| /** |
| * Suggest if there has a official module for the command. |
| * |
| * @param command command name |
| * @param registry npm registry |
| */ |
| export async function suggestPackage(command: string, registry: string = 'https://registry.npm.taobao.org/') { |
| const npmApi = http.create({ |
| baseURL: `${registry}${registry.slice(-1) === '/' ? '' : '/'}-/package/@weex-cli/`, |
| timeout: 30000, |
| headers: { |
| Accept: '*/*', |
| }, |
| }) |
| const res: any = await npmApi.get(`${command}/dist-tags`) |
| if (res.problem) { |
| debug('suggest package error:', res.problem) |
| await analyzer('request', res.problem, { registry }) |
| } |
| return res.data |
| } |
| |
| /** |
| * Checking if there has the package on the specified registry with specified version |
| * |
| * @param name package name |
| * @param version package version |
| * @param registry npm registry |
| */ |
| export async function checkNpmPackageExist(name: string, version: string, registry: string) { |
| const npmApi = http.create({ |
| baseURL: `${registry}`, |
| }) |
| const res: any = await npmApi.get(`${name}`) |
| if (res && res.data.error) { |
| if (/not_found/.test(res.data.error)) { |
| res.data.error = ErrorType.PACKAGE_NOT_FOUND |
| } |
| } else if (version && version !== 'latest') { |
| if (res && !res.data.versions[version]) { |
| res.data.error = ErrorType.PACKAGE_VERSION_NOT_FOUND |
| res.data.versions = Object.keys(res.data.versions) |
| } |
| } |
| return res.data |
| } |
| |
| /** |
| * Check if there has some modules need to be upgraded. |
| * |
| * @param modules module item |
| * @param registry npm registry |
| * @param time update checking times, day |
| */ |
| export async function updateNpmPackageInfo(modules: ModData, registry: string, time: number = 1) { |
| const modData = Object.assign({}, modules) |
| const date = new Date() |
| if (date.getTime() - modData.last_update_time <= 24 * 3600 * 1000 * time) { |
| return false |
| } |
| logger.info(`Time: ${formateTime(date)}, verify if there is an update`) |
| for (let mod in modData.mods) { |
| let spinner = logger.spin(`Checking ${mod} ... ${logger.colors.grey('this may take a few seconds')}`) |
| spinner.text = `` |
| let res = await getLatestNpmPackageInfo(mod, registry) |
| if (res && !res.error) { |
| spinner.stopAndPersist({ |
| symbol: `${logger.colors.green(`[${logger.checkmark}]`)}`, |
| text: `sync module [${mod}]`, |
| }) |
| if (semver.gt(res.latest, modData.mods[mod].version)) { |
| modData.mods[mod].is_next = false |
| modData.mods[mod].next_version = res.latest |
| modData.mods[mod].changelog = res.package.changelog || '' |
| } else { |
| modData.mods[mod].is_next = true |
| modData.mods[mod].next_version = res.latest |
| modData.mods[mod].changelog = res.package.changelog || '' |
| } |
| } else { |
| if (res.error === ErrorType.PACKAGE_NOT_FOUND) { |
| spinner.stopAndPersist({ |
| symbol: `${logger.colors.red(`[${logger.xmark}]`)}`, |
| text: `Package [${mod}] not found on registry ${registry}`, |
| }) |
| } else { |
| spinner.stopAndPersist({ |
| symbol: `${logger.colors.red(`[${logger.xmark}]`)}`, |
| text: `Unkonw error with checking [${mod}], ${res.error}`, |
| }) |
| } |
| } |
| } |
| modData.last_update_time = date.getTime() |
| return modData |
| } |
| |
| /** |
| * Get the latest version of a package. |
| * |
| * @param name package name |
| * @param registry npm registry |
| * @returns {error?:string, latest?:string} |
| */ |
| export async function getLatestNpmPackageInfo(name: string, registry: string = 'https://registry.npm.taobao.org/') { |
| const npmApi = http.create({ |
| baseURL: `${registry}`, |
| timeout: 30000, |
| headers: { |
| Accept: '*/*', |
| }, |
| }) |
| const res: any = await npmApi.get(`${registry}${registry.slice(-1) === '/' ? '' : '/'}${name}/latest`) |
| let error |
| if (res && res.data) { |
| debug('Get latest npm package info: ', JSON.stringify(res.data)) |
| if (res.data.error) { |
| if (/not_found/.test(res.data.error)) { |
| error = ErrorType.PACKAGE_NOT_FOUND |
| } |
| } else if (res.data['version']) { |
| const latest = res.data['version'] |
| return { |
| latest: latest, |
| package: res.data, |
| } |
| } |
| } else { |
| error = `can't found ${name} latest version` |
| } |
| return { |
| error: error, |
| } |
| } |
| |
| /** |
| * Pick suitable search from stack |
| * @param stack error stack |
| */ |
| export function pickSearchKey(stack: string) { |
| let res = /error:/gi.exec(stack) |
| let errorIndex |
| if (res) { |
| errorIndex = res.index |
| } else { |
| errorIndex = -6 |
| } |
| if (errorIndex) { |
| return stack |
| .slice(errorIndex + 6) |
| .split('\n')[0] |
| .split(' ') |
| .join('+') |
| } else { |
| return stack |
| .split('\n')[0] |
| .split(' ') |
| .join('+') |
| } |
| } |
| |
| /** |
| * Show help message while catch an unknow issue |
| * @param stack |
| */ |
| export function showUnknowErrorsHelp(stack: string) { |
| logger.error(stack) |
| logger.log(logger.colors.grey(`Search for existing GitHub issues similar to yours:`)) |
| let searchKey = pickSearchKey(stack) |
| logger.log(`https://github.com/weexteam/weex-toolkit/issues?q=${searchKey}&type=issue`) |
| |
| logger.log(logger.colors.grey(`\nIf none exists, create a ticket, with the template displayed above, on:`)) |
| logger.log(`https://github.com/weexteam/weex-toolkit/issues/new`) |
| |
| logger.log( |
| logger.colors.grey( |
| `\nBe sure to first read the contributing guide for details on how to properly submit a ticket:`, |
| ), |
| ) |
| logger.log(`https://github.com/weexteam/weex-toolkit/master/CONTRIBUTING.md`) |
| |
| logger.log(logger.colors.grey(`\nDon't forget to anonymize any private data!`)) |
| |
| logger.log(logger.colors.grey(`\nLooking for related issues on:`)) |
| logger.log('https://github.com/weexteam/weex-toolkit/issues?q=is%3Aclosed') |
| } |
| |
| /** |
| * Analyzer error stack and give some solution. |
| * |
| * @param type command type |
| * @param stack error stack |
| * @param options data from error stack |
| */ |
| export async function analyzer(type: string, stack: any, options?: any) { |
| if (type === 'repair') { |
| if (ErrorType.PACKAGE_NOT_FOUND === stack) { |
| const innerMods = [ |
| '@weex-cli/debug', |
| '@weex-cli/generator', |
| '@weex-cli/build', |
| '@weex-cli/preview', |
| '@weex-cli/run', |
| '@weex-cli/doctor', |
| '@weex-cli/lint', |
| '@weex-cli/device', |
| ] |
| let score |
| let tempScore |
| let suggestName |
| innerMods.forEach(mod => { |
| tempScore = strings.strSimilarity2Number(mod, options.name) |
| if (!score) { |
| score = tempScore |
| suggestName = mod |
| } else if (tempScore < score) { |
| score = tempScore |
| suggestName = mod |
| } |
| }) |
| logger.warn(`Module "${options.name}" not found, do you mean "${suggestName}"?`) |
| } else if (ErrorType.PACKAGE_VERSION_NOT_FOUND === stack) { |
| logger.warn(`Module "${options.name}@${options.version}" not found`) |
| if (Array.isArray(options.versions) && options.versions.length > 0) { |
| logger.info('Chose one of the versions:') |
| options.versions.forEach(version => { |
| logger.info(`- ${version}`) |
| }) |
| } |
| } else if (typeof stack === 'string') { |
| if (/module is locked/.test(stack)) { |
| logger.log(`The module is locked now, please be patient and wait for other installation processes to complete.`) |
| logger.warn(`You can also enforce it by adding \`-f\` or \`--force\` flag.`) |
| } |
| showUnknowErrorsHelp(stack) |
| } |
| } else if (type === 'request') { |
| if (stack === 'CONNECTION_ERROR') { |
| logger.log(`Please check if your network can access ${logger.colors.yellow(options.registry)} normally`) |
| logger.log(`Or you can use \`weex config set registry [registry-url]\` command to use other npm registry`) |
| } else if (stack === 'TIMEOUT_ERROR') { |
| logger.log(`Please check if your network can access ${logger.colors.yellow(options.registry)} normally`) |
| logger.info(`Or you can use the \`install\` command to install the plugin.`) |
| } |
| } else if (type === 'compile') { |
| let vueMismatchReg = /Vue packages version mismatch/gi |
| if (vueMismatchReg.test(stack)) { |
| logger.error(stack) |
| logger.warn('\n Try `weex doctor` command to fix this problem.') |
| } else { |
| showUnknowErrorsHelp(stack) |
| } |
| } else if (typeof stack === 'string') { |
| showUnknowErrorsHelp(stack) |
| usertrack('error_track', {type, stack}, cliConfiguration.configs.telemetry) |
| } |
| } |
| |
| /** |
| * usertrack |
| * @param stage usertrack usage |
| * @param track data |
| * @param telemetry can report data or not |
| */ |
| export function usertrack (stage: string, track: any, telemetry: boolean) { |
| if (!telemetry) { |
| return |
| } |
| // usertrack |
| const usertrackapi = http.create({ |
| baseURL: `http://gm.mmstat.com/`, |
| }) |
| let trackQuery = `weex-cli-2.0.tool_usage.${stage}?` |
| for (let key in track) { |
| trackQuery += `${key}=${track[key]}&` |
| } |
| trackQuery +=`t=${new Date().getTime()}` |
| try { |
| /* tslint-disabled */ |
| usertrackapi.get(trackQuery) |
| } catch (error) { |
| debug('Http request error', error) |
| } |
| } |
| |
| /** |
| * Formate time to hh:mm:ss |
| * @param date |
| */ |
| export function formateTime(date: Date) { |
| let hourse = date.getHours() > 9 ? date.getHours() : '0' + date.getHours() |
| let min = date.getHours() > 9 ? date.getMinutes() : '0' + date.getMinutes() |
| let seconds = date.getSeconds() > 9 ? date.getSeconds() : '0' + date.getSeconds() |
| return `${hourse}:${min}:${seconds}` |
| } |
| |
| /** |
| * Pick plugin items for cli toolbox. |
| * |
| * @param modules data from module.json |
| */ |
| export function pickPlugins(modules: ModData): PluginItem[] { |
| if (!modules) return [] |
| let plugins = [] |
| for (let item in modules.mods) { |
| let mod: ModItem = modules.mods[item] |
| plugins.push({ |
| value: mod.local, |
| options: {}, |
| commands: mod.commands, |
| name: item, |
| }) |
| } |
| return plugins |
| } |
| |
| /** |
| * Map command to a package module. |
| * |
| * @param command |
| * @param mods |
| */ |
| export function searchPlugin(command: string, mods: PluginItem[]): PluginItem { |
| if (mods.length > 0) { |
| let result |
| mods.forEach((mod: PluginItem) => { |
| if (mod.commands && Array.isArray(mod.commands)) { |
| mod.commands.forEach(cmd => { |
| if (cmd.name === command || cmd.alias === command) { |
| result = mod |
| } |
| }) |
| } |
| }) |
| return result |
| } else { |
| return {} |
| } |
| } |