diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..512664ac --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true + +[{lib,src,test}/**.js] +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..07764a78 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text eol=lf \ No newline at end of file diff --git a/.gitignore b/.gitignore index 41fc9bde..3e1b6174 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .idea/ .DS_Store node_modules -npm-debug.log \ No newline at end of file +npm-debug.log +debug.log \ No newline at end of file diff --git a/bin/feflow b/bin/feflow index e69de29b..1a5b6e0d 100644 --- a/bin/feflow +++ b/bin/feflow @@ -0,0 +1,6 @@ +#!/usr/bin/env node + +'use strict'; + +require('../lib/feflow')(); + diff --git a/lib/core/command.js b/lib/core/command.js new file mode 100644 index 00000000..a6ad7433 --- /dev/null +++ b/lib/core/command.js @@ -0,0 +1,65 @@ +'use strict'; + +const Promise = require('bluebird'); +const abbrev = require('abbrev'); + +class Command { + + constructor() { + this.store = {}; + this.alias = {}; + } + + get(name) { + name = name.toLowerCase(); + return this.store[this.alias[name]]; + } + + list() { + return this.store; + } + + register(name, desc, options, fn) { + if (!name) throw new TypeError('name is required'); + + if (!fn) { + if (options) { + if (typeof options === 'function') { + fn = options; + + if (typeof desc === 'object') { // name, options, fn + options = desc; + desc = ''; + } else { // name, desc, fn + options = {}; + } + } else { + throw new TypeError('fn must be a function'); + } + } else { + // name, fn + if (typeof desc === 'function') { + fn = desc; + options = {}; + desc = ''; + } else { + throw new TypeError('fn must be a function'); + } + } + } + + if (fn.length > 1) { + fn = Promise.promisify(fn); + } else { + fn = Promise.method(fn); + } + + const c = this.store[name.toLowerCase()] = fn; + c.options = options; + c.desc = desc; + + this.alias = abbrev(Object.keys(this.store)); + } +} + +module.exports = Command; diff --git a/lib/core/index.js b/lib/core/index.js new file mode 100644 index 00000000..bf4a28a9 --- /dev/null +++ b/lib/core/index.js @@ -0,0 +1,128 @@ +'use strict'; + +const vm = require('vm'); +const pathFn = require('path'); +const Module = require('module'); +const _ = require('lodash'); +const osenv = require('osenv'); +const Promise = require('bluebird'); +const chalk = require('chalk'); +const fs = require('hexo-fs'); +const logger = require('../utils/logger'); +const Command = require('./command'); +const pkg = require('../../package.json'); +const sep = pathFn.sep; + +class Feflow { + + /** + * Set root and plugin path, context variable include log, cli command object. + * @param args + */ + constructor(args) { + args = args || {}; + + const base = pathFn.join(osenv.home(), './.feflow'); + + this.version = pkg.version; + this.baseDir = base + sep; + this.pkgPath = pathFn.join(base, 'package.json'); + this.pluginDir = pathFn.join(base, 'node_modules') + sep; + + this.log = logger({ + debug: Boolean(args.debug) + }); + + this.cmd = new Command(); + } + + /** + * Read config and load internal and external plugins. + */ + init() { + const self = this; + + this.log.debug('Feflow version: %s', chalk.magenta(this.version)); + + // Load internal plugins + require('../plugins/console')(this); + require('../plugins/generator')(this); + require('../plugins/install')(this); + + // Load external plugins + return Promise.each([ + 'load_plugins' + ], function(name) { + return require('./' + name)(self); + }).then(function() { + // Init success + self.log.debug('init success!'); + }); + + } + + /** + * Call a command in console. + * @param name + * @param args + * @param callback + */ + call(name, args, callback) { + if (!callback && typeof args === 'function') { + callback = args; + args = {}; + } + + const self = this; + + return new Promise(function(resolve, reject) { + const c = self.cmd.get(name); + + if (c) { + c.call(self, args).then(resolve, reject); + } else { + reject(new Error('Command `' + name + '` has not been registered yet!')); + } + }).asCallback(callback); + } + + /** + * Load a plugin with vm module and inject feflow variable, + * feflow is an instance and has context environment. + * + * @param path {String} Plugin path + * @param callback {Function} Callback + */ + loadPlugin(path, callback) { + const self = this; + + return fs.readFile(path).then((script) => { + + const module = new Module(path); + module.filename = path; + module.paths = Module._nodeModulePaths(path); + + function require(path) { + return module.require(path); + } + + require.resolve = function(request) { + return Module._resolveFilename(request, module); + }; + + require.main = process.mainModule; + require.extensions = Module._extensions; + require.cache = Module._cache; + + // Inject feflow variable + script = '(function(exports, require, module, __filename, __dirname, feflow){' + + script + '});'; + + const fn = vm.runInThisContext(script, path); + + return fn(module.exports, require, module, path, pathFn.dirname(path), self); + }).asCallback(callback); + } +} + +module.exports = Feflow; \ No newline at end of file diff --git a/lib/core/load_plugins.js b/lib/core/load_plugins.js new file mode 100644 index 00000000..94f321b7 --- /dev/null +++ b/lib/core/load_plugins.js @@ -0,0 +1,53 @@ +'use strict'; + +const pathFn = require('path'); +const fs = require('hexo-fs'); +const Promise = require('bluebird'); +const chalk = require('chalk'); + +module.exports = function(ctx) { + return Promise.all([ + loadModules(ctx) + ]); +}; + +function loadModules(ctx) { + const self = this; + const pluginDir = ctx.pluginDir; + + return loadModuleList(ctx).map(function(name) { + let path = require.resolve(pathFn.join(pluginDir, name)); + + // Load plugins + return ctx.loadPlugin(path).then(function() { + ctx.log.debug('Plugin loaded: %s', chalk.magenta(name)); + }).catch(function(err) { + ctx.log.error({err: err}, 'Plugin load failed: %s', chalk.magenta(name)); + }); + }); +} + +function loadModuleList(ctx) { + const packagePath = pathFn.join(ctx.baseDir, 'package.json'); + const pluginDir = ctx.pluginDir; + + // Make sure package.json exists + return fs.exists(packagePath).then(function(exist) { + if (!exist) return []; + + // Read package.json and find dependencies + return fs.readFile(packagePath).then(function(content) { + const json = JSON.parse(content); + const deps = json.dependencies || json.devDependencies || {}; + + return Object.keys(deps); + }); + }).filter(function(name) { + // Ignore plugins whose name is not started with "feflow-plugin-" + if (!/^feflow-plugin-|^@[^/]+\/feflow-plugin-/.test(name)) return false; + + // Make sure the plugin exists + const path = pathFn.join(pluginDir, name); + return fs.exists(path); + }); +} \ No newline at end of file diff --git a/lib/feflow.js b/lib/feflow.js new file mode 100644 index 00000000..0dd089f9 --- /dev/null +++ b/lib/feflow.js @@ -0,0 +1,44 @@ +'use strict'; + +const Feflow = require('./core'); +const minimist = require('minimist'); + +function entry(args) { + args = minimist(process.argv.slice(2)); + + const feflow = new Feflow(args); + const log = feflow.log; + + function handleError(err) { + if (err) { + log.fatal(err); + } + + process.exit(2); + } + + return feflow.init().then(function() { + let cmd = ''; + + if (!args.h && !args.help) { + cmd = args._.shift(); + + if (cmd) { + let c = feflow.cmd.get(cmd); + if (!c) cmd = 'help'; + } else { + cmd = 'help'; + } + } else { + cmd = 'help'; + } + + return feflow.call(cmd, args).then(function() { + + }).catch(function(err) { + console.log(err); + }); + }).catch(handleError); +} + +module.exports = entry; \ No newline at end of file diff --git a/lib/index.js b/lib/index.js deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/plugins/console/help.js b/lib/plugins/console/help.js new file mode 100644 index 00000000..b7dd1c46 --- /dev/null +++ b/lib/plugins/console/help.js @@ -0,0 +1,26 @@ +'use strict'; +const meow = require('meow'); + +function helpConsole(args) { + const cli = meow(` + Usage: feflow [options] [command] + + Commands: + init Choose a scaffold to initialize project. + install Install a plugin or a yeoman generator. + publish Publish files to cdn or offline package. + jb Publish to jb when in development. + ars --env Publish to ars code platform, env is daily, pre or prod. + + + Options: + --version, -[vV] Print version and exit successfully. + --help, -[hH] Print this help and exit successfully. + + Report bugs to http://git.code.oa.com/feflow/discussion/issues. + `); + + return cli.showHelp(1); +} + +module.exports = helpConsole; diff --git a/lib/plugins/console/index.js b/lib/plugins/console/index.js new file mode 100644 index 00000000..52e6c727 --- /dev/null +++ b/lib/plugins/console/index.js @@ -0,0 +1,10 @@ +'use strict'; + +module.exports = function(ctx) { + + const cmd = ctx.cmd; + + cmd.register('help', 'Get help on a command.', {}, require('./help')); + + cmd.register('version', 'Display version information.', {}, require('./version')); +}; diff --git a/lib/plugins/console/version.js b/lib/plugins/console/version.js new file mode 100644 index 00000000..34dc78e3 --- /dev/null +++ b/lib/plugins/console/version.js @@ -0,0 +1,22 @@ +'use strict'; + +const os = require('os'); +const Promise = require('bluebird'); + +function versionConsole(args) { + const versions = process.versions; + const keys = Object.keys(versions); + let key = ''; + + console.log('feflow:', this.version); + console.log('os:', os.type(), os.release(), os.platform(), os.arch()); + + for (let i = 0, len = keys.length; i < len; i++) { + key = keys[i]; + console.log('%s: %s', key, versions[key]); + } + + return Promise.resolve(); +} + +module.exports = versionConsole; \ No newline at end of file diff --git a/lib/plugins/generator/generator.js b/lib/plugins/generator/generator.js new file mode 100644 index 00000000..bd13a8d4 --- /dev/null +++ b/lib/plugins/generator/generator.js @@ -0,0 +1,94 @@ +'use strict'; + +const osenv = require('osenv'); +const pathFn = require('path'); +const fs = require('hexo-fs'); +const inquirer = require('inquirer'); +const chalk = require('chalk'); +const yeoman = require('yeoman-environment'); +const yeomanEnv = yeoman.createEnv(); + + +function init(ctx) { + const log = ctx.log; + return loadGeneratorList(ctx).then((generators) => { + const options = generators.map((item) => {return item.desc}); + if (generators.length) { + inquirer.prompt([{ + type: 'list', + name: 'desc', + message: '您想要创建哪中类型的工程?', + choices: options + }]).then((answer) => { + let name; + + generators.map((item) => { + if (item.desc = answer.desc) { + name = item.name; + } + }); + + name && run(name, ctx); + }); + } else { + log.warn('检测到你还未安装任何脚手架,请先安装后再进行项目初始化,参考文档:http://feflow.oa.com/docs/index.html') + } + }); +} + +function run(name, ctx) { + const pluginDir = ctx.pluginDir; + + const path = pathFn.join(pluginDir, name, 'generators/app/index.js'); + + yeomanEnv.register(require.resolve(path), name); + + yeomanEnv.run(name, { 'skip-install': true }, err => {}); +} + +function loadGeneratorList(ctx) { + const baseDir = ctx.baseDir; + const pluginDir = ctx.pluginDir; + const packagePath = pathFn.join(baseDir, 'package.json'); + + // Make sure package.json exists + return fs.exists(packagePath).then(function(exist) { + if (!exist) return []; + + // Read package.json and find dependencies + return fs.readFile(packagePath).then(function(content) { + const json = JSON.parse(content); + const deps = json.dependencies || json.devDependencies || {}; + + return Object.keys(deps); + }); + }).filter(function(name) { + // Find yeoman generator. + if (!/^generator-|^@[^/]+\/generator-/.test(name)) return false; + + // Make sure the generator exists + const path = pathFn.join(pluginDir, name); + return fs.exists(path); + }).map(function(name) { + let path = pathFn.join(pluginDir, name); + let packagePath = pathFn.join(path, 'package.json'); + + // Read generator config. + return fs.readFile(packagePath).then(function(content) { + const json = JSON.parse(content); + const desc = json.description; + + return { + name, + desc + }; + }); + }); +} + + +module.exports = function (args) { + const ctx = this; + + return init(ctx); +}; diff --git a/lib/plugins/generator/index.js b/lib/plugins/generator/index.js new file mode 100644 index 00000000..a8824204 --- /dev/null +++ b/lib/plugins/generator/index.js @@ -0,0 +1,8 @@ +'use strict'; + +module.exports = function(ctx) { + + const cmd = ctx.cmd; + + cmd.register('init', 'Choose a scaffold to initialize project.', {}, require('./generator')); +}; \ No newline at end of file diff --git a/lib/plugins/install/index.js b/lib/plugins/install/index.js new file mode 100644 index 00000000..b9c6decf --- /dev/null +++ b/lib/plugins/install/index.js @@ -0,0 +1,8 @@ +'use strict'; + +module.exports = function(ctx) { + + const cmd = ctx.cmd; + + cmd.register('install', 'Install a plugin or a yeoman generator.', {}, require('./install_plugins')); +}; \ No newline at end of file diff --git a/lib/plugins/install/install_plugins.js b/lib/plugins/install/install_plugins.js new file mode 100644 index 00000000..7679b280 --- /dev/null +++ b/lib/plugins/install/install_plugins.js @@ -0,0 +1,83 @@ +'use strict'; + +const pathFn = require('path'); +const chalk = require('chalk'); +const Table = require('easy-table'); +const fs = require('hexo-fs'); +const Promise = require('bluebird'); + +/** + * Install one or many plugins. + * @param plugins {Array} plugin arrays + * @param ctx {Object} context environment + */ +function installModules(plugins, ctx) { + if (typeof plugins === 'string') { + plugins = [plugins]; + } + + const pkgPath = ctx.pkgPath; + const pluginDir = ctx.pluginDir; + + const table = new Table(); + + // Ignore plugins whose name is not started with "feflow-plugin-" or "generator-" + plugins = plugins.filter(function(name) { + if (!/^feflow-plugin-|^@[^/]+\/feflow-plugin-|^generator-|^@[^/]+\/generator-/.test(name)) return false; + + // Make sure the plugin exists + const path = pathFn.join(pluginDir, name); + return fs.exists(path); + }); + + Promise.all(data).then((ret) => { + console.log(ret) + }); + +} + +/** + * Get local packages version +* @param plugins {Array} plugin arrays + */ +function localVersions(plugins) { + return plugins.map(function (name) { + const path = pathFn.join(pluginDir, name, 'package.json'); + return readPkg(path); + }); +} + +/** + * Get remote packages latest version +* @param plugins {Array} plugin arrays + */ +function remoteVersions(plugins) { + return plugins.map(function (name) { + return fetchRegistry(name); + }); +} + +function readPkg(path) { + return fs.exists(path).then(function(exist) { + if (!exist) return; + + return fs.readFile(path).then(function(content) { + const pkg = JSON.parse(content); + return pkg.version; + }); + }); +} + + +function fetchRegistry(plugins) { + + return new Promise(function (resolve, reject) { + + }); +} + +module.exports = function (args) { + const ctx = this; + + return installModules(args['_'], ctx); +}; diff --git a/lib/utils/index.js b/lib/utils/index.js new file mode 100644 index 00000000..185f708e --- /dev/null +++ b/lib/utils/index.js @@ -0,0 +1,5 @@ +'use strict'; + +exports.spawnCommand = require('./spawn_command'); + +exports.install = require('./install'); \ No newline at end of file diff --git a/lib/utils/install.js b/lib/utils/install.js new file mode 100644 index 00000000..73e73d4b --- /dev/null +++ b/lib/utils/install.js @@ -0,0 +1,38 @@ +'use strict'; +const spawnCommand = require('./spawn_command'); + +module.exports = function (modules, where) { + return new Promise((resolve, reject) => { + + const args = ['install'].concat(modules).concat('--color=always'); + + const tnpm = spawnCommand('tnpm', args, {cwd: where}); + + // 尝试使用tnpm来安装二方包或者三方包 + let tnpmOutput = ''; + tnpm.stdout.on('data', (data) => { + tnpmOutput += data; + }).pipe(process.stdout); + + tnpm.stderr.on('data', (data) => { + tnpmOutput += data; + }).pipe(process.stderr); + + tnpm.on('close', (code) => { + if (!code) { + resolve({ + success: true, + msg: 'tnpm 安装成功!', + data: tnpmOutput + }); + return; + } else { + reject({ + success: false, + msg: 'tnpm异常退出,code: ' + code, + data: tnpmOutput + }); + } + }); + }); +}; \ No newline at end of file diff --git a/lib/utils/logger.js b/lib/utils/logger.js new file mode 100644 index 00000000..24cc2d90 --- /dev/null +++ b/lib/utils/logger.js @@ -0,0 +1,106 @@ +'use strict'; + +const bunyan = require('bunyan'); +const chalk = require('chalk'); +const Writable = require('stream').Writable; + +const levelNames = { + 10: 'TRACE', + 20: 'DEBUG', + 30: 'INFO ', + 40: 'WARN ', + 50: 'ERROR', + 60: 'FATAL' +}; + +const levelColors = { + 10: 'gray', + 20: 'gray', + 30: 'green', + 40: 'bgYellow', + 50: 'bgRed', + 60: 'bgRed' +}; + + +function ConsoleStream(env) { + Writable.call(this, {objectMode: true}); + + this.debug = env.debug; +} + +require('util').inherits(ConsoleStream, Writable); + +ConsoleStream.prototype._write = function(data, enc, callback) { + const level = data.level; + let msg = ''; + + // Time + if (this.debug) { + msg += chalk.gray(formatTime(data.time)) + ' '; + } + + // Level + msg += chalk[levelColors[level]]('Feflow' + ' ' + levelNames[level]) + ' '; + + // Message + msg += data.msg + '\n'; + + // Error + if (data.err) { + const err = data.err.stack || data.err.message; + if (err) msg += chalk.yellow(err) + '\n'; + } + + if (level >= 40) { + process.stderr.write(msg); + } else { + process.stdout.write(msg); + } + + callback(); +}; + +function formatTime(date) { + return date.toISOString().substring(11, 23); +} + +function createLogger(options) { + options = options || {}; + + const streams = []; + + if (!options.silent) { + streams.push({ + type: 'raw', + level: options.debug ? 'trace' : 'info', + stream: new ConsoleStream(options) + }); + } + + if (options.debug) { + streams.push({ + level: 'trace', + path: 'debug.log' + }); + } + + const logger = bunyan.createLogger({ + name: options.name || 'feflow', + streams: streams, + serializers: { + err: bunyan.stdSerializers.err + } + }); + + // Alias for logger levels + logger.d = logger.debug; + logger.i = logger.info; + logger.w = logger.warn; + logger.e = logger.error; + logger.log = logger.info; + + return logger; +} + +module.exports = createLogger; \ No newline at end of file diff --git a/lib/utils/spawn_command.js b/lib/utils/spawn_command.js new file mode 100644 index 00000000..8d77491f --- /dev/null +++ b/lib/utils/spawn_command.js @@ -0,0 +1,13 @@ +'use strict'; + +const spawn = require('child_process').spawn; + +module.exports = function (command, args, options) { + const win32 = process.platform === 'win32'; + + const winCommand = win32 ? 'cmd' : command; + + const winArgs = win32 ? ['/c'].concat(command, args) : args; + + return spawn(winCommand, winArgs, options); +}; \ No newline at end of file diff --git a/package.json b/package.json index 32bcdaac..ceb2a0d7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "feflow-cli", - "version": "0.0.1", + "name": "@tencent/feflow-cli", + "version": "0.10.0", "description": "A command line tool aims to improve front-end engineer workflow.", "main": "lib/index.js", "scripts": { @@ -10,21 +10,32 @@ }, "repository": { "type": "git", - "url": "https://github.com/iv-web/feflow-cli.git" + "url": "https://github.com/iv-web/feflow-cli" }, "keywords": [ "workflow", "commandline", "front-end" ], - "author": "lewischeng", + "author": "cpselvis (cpselvis@gmail.com)", "license": "MIT", "bin": { "feflow": "./bin/feflow" }, "dependencies": { + "abbrev": "^1.1.0", + "bluebird": "^3.5.0", + "bunyan": "^1.8.12", + "chalk": "^2.0.1", + "co": "^4.6.0", + "figlet": "^1.2.0", + "hexo-fs": "^0.2.1", "inquirer": "^3.0.6", - "shelljs": "^0.7.7", + "lodash": "^4.17.4", + "meow": "^3.7.0", + "minimist": "^1.2.0", + "os": "^0.1.1", + "osenv": "^0.1.4", "yeoman-environment": "^1.6.6" }, "devDependencies": { @@ -33,4 +44,4 @@ "conventional-changelog-cli": "^1.2.0", "husky": "^0.13.1" } -} \ No newline at end of file +}