blob: d4258c35a5be5f080696c00ba3a2e8d0d282e249 [file] [log] [blame]
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 {}
}
}