├── .babelrc ├── .gitignore ├── .npmignore ├── README.md ├── bin └── hi.js ├── index.js ├── package.json └── src ├── command ├── init.js └── install.js ├── config ├── constant.js └── index.js ├── index.js └── utils ├── OraLoading.js ├── copyFile.js ├── gitCtrl.js ├── initProjectQuestion.js ├── metalsmithACtion.js └── render.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": { 5 | "node": "current" 6 | } 7 | }] 8 | ], 9 | "plugins": [ 10 | "transform-es2015-modules-commonjs" 11 | ] 12 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | # webstorm 60 | .idea 61 | 62 | # osx 63 | .DS_Store 64 | 65 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | yarn.lock 2 | list.txt 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webpack-template-cli 2 | 3 | 用来初始化自己本地项目的cli,目前支持初始化[webpack多页面模板](https://github.com/CavinHuang/webpack-multi-skeleton) 4 | 5 | ```bash 6 | npm install webpack-template-cli -g 7 | ``` 8 | 执行命令 9 | ```bash 10 | webpack-template install # 下载远程项目 11 | 12 | webpack-template init # 初始化项目 13 | 14 | ``` 15 | -------------------------------------------------------------------------------- /bin/hi.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require( '../' ) -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require( "babel-register" ) 2 | require( "babel-core" ) 3 | .transform( "code", { 4 | presets: [ [ require( 'babel-preset-latest-node' ), { 5 | target: 'current' 6 | } ] ] 7 | } ); 8 | require( 'babel-polyfill' ) 9 | require( './src' ) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-template-cli", 3 | "version": "1.0.0", 4 | "description": "webpack template gentate cli", 5 | "main": "index.js", 6 | "bin": { 7 | "webpack-template-cli": "hi.js" 8 | }, 9 | "engines": { 10 | "node": ">= 0.12" 11 | }, 12 | "dependencies": { 13 | "babel-preset-env": "^1.6.1", 14 | "commander": "^2.13.0", 15 | "consolidate": "^0.15.0", 16 | "download-git-repo": "^1.0.2", 17 | "inquirer": "^5.0.1", 18 | "metalsmith": "^2.3.0", 19 | "mz": "^2.7.0", 20 | "ncp": "^2.0.0", 21 | "ora": "^1.4.0", 22 | "request": "^2.83.0", 23 | "rmfr": "^2.0.0-3", 24 | "swig": "^1.4.2" 25 | }, 26 | "devDependencies": { 27 | "babel-cli": "^6.26.0", 28 | "babel-eslint": "^8.2.1", 29 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0", 30 | "babel-preset-latest-node": "^0.4.0", 31 | "babel-preset-stage-3": "^6.24.1" 32 | }, 33 | "scripts": { 34 | "test": "echo \"Error: no test specified\" && exit 1" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "git+https://github.com/CavinHuang/node-cli-demo.git" 39 | }, 40 | "keywords": [ 41 | "webpack", 42 | "node-cli", 43 | "webpack-template" 44 | ], 45 | "author": "sujinw", 46 | "license": "ISC", 47 | "bugs": { 48 | "url": "https://github.com/CavinHuang/node-cli-demo/issues" 49 | }, 50 | "homepage": "https://github.com/CavinHuang/node-cli-demo#readme" 51 | } 52 | -------------------------------------------------------------------------------- /src/command/init.js: -------------------------------------------------------------------------------- 1 | import program from 'commander' 2 | import inquirer from 'inquirer' 3 | import { 4 | resolve 5 | } from 'path' 6 | import { 7 | readdir, 8 | exists 9 | } from 'mz/fs' 10 | import config from '../config' 11 | import { 12 | dirs 13 | } from '../config/constant' 14 | import rmfr from 'rmfr'; 15 | import copyFile from '../utils/copyFile' 16 | import metalsmithACtion from '../utils/metalsmithACtion' 17 | import OraLoading from '../utils/OraLoading' 18 | 19 | program 20 | .command( 'init' ) 21 | .description( 'init project for local' ) 22 | .action( async ( options ) => { //list命令的实现体 23 | // to do 24 | console.log( 'init command' ); 25 | let loader 26 | loader = OraLoading( 'check download dir' ); 27 | if ( !await exists( dirs.download ) ) { 28 | throw new Error( `There is no ${dirs.download}, Please install a template` ); 29 | } 30 | loader.succeed( 'check download dir success' ) 31 | loader = OraLoading( 'read download dir' ); 32 | const list = await readdir( dirs.download ); 33 | loader.succeed( 'read download dir success' ); 34 | if ( list.length === 0 ) { 35 | throw new Error( `There is no any scaffolds in your local folder ${dirs.download}, install it` ); 36 | } 37 | 38 | let questions = [ 39 | { 40 | type: 'list', 41 | name: 'template', 42 | message: 'which template do you want to init?', 43 | choices: list 44 | }, { 45 | type: 'input', 46 | name: 'dir', 47 | message: 'project name', 48 | async validate( input ) { 49 | const done = this.async(); 50 | if ( input.length === 0 ) { 51 | done( 'You must input project name' ); 52 | return; 53 | } 54 | const dir = resolve( process.cwd(), input ); 55 | if ( await exists( dir ) ) { 56 | done( 'The project name is already existed. Please change another name' ); 57 | } 58 | done( null, true ); 59 | } 60 | } 61 | ]; 62 | const answers = await inquirer.prompt( questions ) 63 | const metalsmith = config.metalsmith; 64 | if ( metalsmith ) { 65 | const tmp = `${dirs.tmp}/${answers.template}`; 66 | // 复制一份到临时目录,在临时目录编译生成 67 | loader = OraLoading( 'copy file to tmp dir' ) 68 | await copyFile( `${dirs.download}/${answers.template}`, tmp ); 69 | loader.succeed( 'copy file to tmp dir success' ) 70 | await metalsmithACtion( answers.template ); 71 | loader = OraLoading( 'compiling', answers.dir ); 72 | await copyFile( `${tmp}/${dirs.metalsmith}`, answers.dir ); 73 | await rmfr( tmp ); // 清除临时文件夹 74 | } else { 75 | loader = OraLoading( 'generating', answers.dir ); 76 | await copyFile( `${dirs.download}/${answers.template}`, answers.dir ); 77 | } 78 | loader.succeed( `generated ${answers.dir}` ); 79 | } ); 80 | program.parse( process.argv ); //开始解析用户输入的命令 -------------------------------------------------------------------------------- /src/command/install.js: -------------------------------------------------------------------------------- 1 | import program from 'commander' 2 | import inquirer from 'inquirer' 3 | import gitCtrl from '../utils/gitCtrl' 4 | import config from '../config' 5 | import OraLoading from '../utils/OraLoading' 6 | 7 | // 初始化git操作类 8 | let git = new gitCtrl.gitCtrl( config.repoType, config.registry ) 9 | 10 | program 11 | .command( 'install' ) 12 | .description( 'install github project to local' ) 13 | .action( async ( options ) => { //list命令的实现体 14 | // to do 15 | console.log( 'install command' ); 16 | let version, choices, repos, loader 17 | loader = OraLoading( 'fetch repo list' ) 18 | repos = await git.fetchRepoList(); 19 | loader.succeed( 'fetch repo list success' ); 20 | if ( repos.length === 0 ) { 21 | throw new Error( `There is no any scaffolds in https://github.com/${config.registry}. Please create and try` ); 22 | } 23 | 24 | choices = repos.map( ( { 25 | name 26 | } ) => name ); 27 | 28 | let questions = [ { 29 | type: 'list', 30 | name: 'repo', 31 | message: 'which repo do you want to install?', 32 | choices 33 | } ]; 34 | // 调用问题 35 | let answers = await inquirer.prompt( questions ) 36 | 37 | // 取出选择的git仓库 38 | const repo = answers.repo; 39 | // 获取选择仓库所有的版本 40 | loader = OraLoading( 'fetch repo tag list' ) 41 | const tags = await git.fetchRepoTagList( repo ); 42 | loader.succeed( 'fetch repo tag list success' ); 43 | if ( tags.length === 0 ) { 44 | version = ''; 45 | } else { 46 | choices = tags.map( ( { 47 | name 48 | } ) => name ); 49 | 50 | answers = await inquirer.prompt( [ 51 | { 52 | type: 'list', 53 | name: 'version', 54 | message: 'which version do you want to install?', 55 | choices 56 | } 57 | ] ); 58 | version = answers.version; 59 | } 60 | console.log( answers ); // 输出最终的答案 61 | loader = OraLoading( 'begin download repo' ) 62 | let result = await git.downloadGitRepo( [ repo, version ].join( '@' ) ); 63 | //console.log( result ? 'SUCCESS' : result ) 64 | loader.succeed( 'download repe success' ) 65 | } ); 66 | program.parse( process.argv ); //开始解析用户输入的命令 -------------------------------------------------------------------------------- /src/config/constant.js: -------------------------------------------------------------------------------- 1 | const os = require( 'os' ); 2 | import { 3 | name, 4 | version, 5 | engines 6 | } from '../../package.json'; 7 | 8 | // 系统user文件夹 9 | const home = process.env[ ( process.platform === 'win32' ) ? 'USERPROFILE' : 'HOME' ]; 10 | 11 | // user agent 12 | export const ua = `${name}-${version}`; 13 | 14 | /** 15 | * 文件夹定义 16 | * @type {Object} 17 | */ 18 | export const dirs = { 19 | home, 20 | download: `${home}/.webpack-project`, 21 | rc: `${home}/.webpack-project`, 22 | tmp: os.tmpdir(), 23 | metalsmith: 'metalsmith' 24 | }; 25 | 26 | /** 27 | * 版本 28 | * @type {Object} 29 | */ 30 | export const versions = { 31 | node: process.version.substr( 1 ), 32 | nodeEngines: engines.node, 33 | [ name ]: version 34 | }; -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 配置文件 3 | */ 4 | 5 | export default { 6 | registry: 'cavinHuangORG', // 仓库地址 7 | repoType: 'org', // ['org', 'user'] 8 | metalsmith: true 9 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | var program = require( 'commander' ); 2 | program.parse( process.argv ); //开始解析用户输入的命令 3 | require( './command/' + program.args + '.js' ) // 根据不同的命令转到不同的命令处理文件 -------------------------------------------------------------------------------- /src/utils/OraLoading.js: -------------------------------------------------------------------------------- 1 | import ora from 'ora'; 2 | 3 | export default function OraLoading( action = 'getting', repo = '' ) { 4 | const l = ora( `${action} ${repo}` ); 5 | return l.start(); 6 | } -------------------------------------------------------------------------------- /src/utils/copyFile.js: -------------------------------------------------------------------------------- 1 | import { 2 | ncp 3 | } from 'ncp'; 4 | import mkdirp from 'mkdirp' 5 | import { 6 | exists 7 | } from 'mz/fs' 8 | export default function copyFile( src, dest ) { 9 | return new Promise( async ( resolve, reject ) => { 10 | if ( !( await exists( src ) ) ) { 11 | mkdirp.sync( src ); 12 | } 13 | ncp( src, dest, ( err ) => { 14 | if ( err ) { 15 | reject( err ); 16 | return; 17 | } 18 | resolve(); 19 | } ); 20 | } ); 21 | } -------------------------------------------------------------------------------- /src/utils/gitCtrl.js: -------------------------------------------------------------------------------- 1 | import { 2 | basename 3 | } from 'path'; 4 | import request from 'request'; 5 | import DownloadGitRepo from 'download-git-repo'; 6 | import { 7 | dirs 8 | } from '../config/constant' 9 | /** 10 | * git操作类 11 | */ 12 | class gitCtrl { 13 | constructor( type, registry ) { 14 | this.type = type 15 | this.registry = registry 16 | } 17 | 18 | /** 19 | * request Promise封装 方便调用 20 | * @param {[string]} api [地址] 21 | * @param {[string]} ua [User-Agent] 22 | * @return {[type]} [description] 23 | */ 24 | fetch( api, ua ) { 25 | return new Promise( ( resolve, reject ) => { 26 | request( { 27 | url: api, 28 | method: 'GET', 29 | headers: { 30 | 'User-Agent': `${ua}` 31 | } 32 | }, ( err, res, body ) => { 33 | if ( err ) { 34 | reject( err ); 35 | return; 36 | } 37 | const data = JSON.parse( body ); 38 | if ( data.message === 'Not Found' ) { 39 | reject( new Error( `${api} is not found` ) ); 40 | } else { 41 | resolve( data ); 42 | } 43 | } ); 44 | } ); 45 | } 46 | 47 | /** 48 | * 获取git仓库列表 49 | */ 50 | async fetchRepoList() { 51 | const api = `https://api.github.com/${this.type}s/${this.registry}/repos`; 52 | return await this.fetch( api ); 53 | } 54 | 55 | /** 56 | * 获取仓库所有的版本 57 | * @param {[string]} repo [仓库名称] 58 | * @return {[type]} [description] 59 | */ 60 | async fetchRepoTagList( repo ) { 61 | const { 62 | url, 63 | scaffold 64 | } = await this.fetchGitInfo( repo ); 65 | const api = `https://api.github.com/repos/${url}/tags`; 66 | 67 | return this.fetch( api, scaffold, url ); 68 | } 69 | 70 | /** 71 | * 获取仓库详细信息 72 | * @param {[string]} repo [仓库名称] 73 | * @return {[type]} [description] 74 | */ 75 | async fetchGitInfo( repo ) { 76 | let template = repo; 77 | let [ scaffold ] = template.split( '@' ); 78 | 79 | scaffold = basename( scaffold ); 80 | 81 | template = template.split( '@' ) 82 | .filter( Boolean ) 83 | .join( '#' ); 84 | const url = `${this.registry}/${template}`; 85 | return { 86 | url, 87 | scaffold 88 | }; 89 | } 90 | 91 | /** 92 | * 下载git仓库代码到指定文件夹 93 | * @param {[type]} repo [description] 94 | * @return {[type]} [description] 95 | */ 96 | async downloadGitRepo( repo ) { 97 | const { 98 | url, 99 | scaffold 100 | } = await this.fetchGitInfo( repo ); 101 | 102 | return new Promise( ( resolve, reject ) => { 103 | DownloadGitRepo( url, `${dirs.download}/${scaffold}`, ( err ) => { 104 | if ( err ) { 105 | reject( err ); 106 | return; 107 | } 108 | resolve( true ); 109 | } ); 110 | } ); 111 | } 112 | } 113 | 114 | export default { 115 | gitCtrl 116 | } -------------------------------------------------------------------------------- /src/utils/initProjectQuestion.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 项目初始化问题整理 3 | * @param {[type]} template [description] 4 | * @param {[type]} user [description] 5 | * @param {[type]} email [description] 6 | * @return {[type]} [description] 7 | */ 8 | export default function askCreator( { 9 | template, 10 | user, 11 | email 12 | } ) { 13 | return [ 14 | { 15 | type: 'confirm', 16 | name: 'private', 17 | message: 'Is the project private ?' 18 | }, 19 | { 20 | type: 'input', 21 | name: 'name', 22 | message: 'package name', 23 | default: template, 24 | validate( input ) { 25 | const done = this.async(); 26 | if ( input.trim() 27 | .length === 0 ) { 28 | done( 'project name is empty' ); 29 | return; 30 | } 31 | done( null, true ); 32 | } 33 | }, 34 | 35 | { 36 | type: 'input', 37 | name: 'description', 38 | message: 'description' 39 | }, 40 | 41 | { 42 | type: 'list', 43 | name: 'license', 44 | message: 'license', 45 | choices: [ 'MIT', "BSD 2-clause 'Simplified'", 'Apache 2.0', 'GNU General Public v3.0', 'BSD 3-clause', 'Eclipse Public 1.0', 'GNU Affero General Public v3.0', 'GNU General Public v2.0', 'GNU Lesser General Public v2.1', 'GNU Lesser General Public v3.0', 'Mozilla Public 2.0', 'The Unlicense' ] 46 | }, 47 | { 48 | type: 'input', 49 | name: 'author', 50 | message: 'author', 51 | default: email 52 | }, 53 | { 54 | type: 'input', 55 | name: 'git', 56 | message: 'user/repo', 57 | default: `${user}/${template}`, 58 | validate( input ) { 59 | const done = this.async(); 60 | if ( !/\w+\/\w+/.test( input ) ) { 61 | done( 'Please input like user/repo' ); 62 | return; 63 | } 64 | done( null, true ); 65 | } 66 | } 67 | ]; 68 | } -------------------------------------------------------------------------------- /src/utils/metalsmithACtion.js: -------------------------------------------------------------------------------- 1 | import Metalsmith from 'metalsmith'; 2 | import inquirer from 'inquirer'; 3 | import mkdirp from 'mkdirp'; 4 | import { 5 | exists 6 | } from 'mz/fs'; 7 | import { 8 | execSync 9 | } from 'child_process'; 10 | import { 11 | dirs 12 | } from '../config/constant'; 13 | import question from './initProjectQuestion'; 14 | import render from './render'; 15 | 16 | export default async function apply( template ) { 17 | const base = `${dirs.tmp}/${template}`; 18 | const metalsmith = Metalsmith( base ); 19 | const tmpBuildDir = `${base}/${dirs.metalsmith}`; 20 | 21 | if ( !( await exists( tmpBuildDir ) ) ) { 22 | mkdirp.sync( tmpBuildDir ); 23 | } 24 | 25 | let user = execSync( 'git config --global user.name', { 26 | encoding: 'utf-8' 27 | } ); 28 | let email = execSync( 'git config --global user.email', { 29 | encoding: 'utf-8' 30 | } ); 31 | 32 | user = user.trim(); 33 | email = email.trim(); 34 | 35 | const answers = await inquirer.prompt( question( { 36 | template, 37 | user, 38 | email 39 | } ) ); 40 | 41 | return new Promise( ( resolve, reject ) => { 42 | metalsmith 43 | .metadata( answers ) 44 | .source( './' ) 45 | .destination( tmpBuildDir ) 46 | .clean( false ) 47 | .use( render() ) 48 | .build( ( err ) => { 49 | if ( err ) { 50 | reject( err ); 51 | return; 52 | } 53 | resolve( true ); 54 | } ); 55 | } ); 56 | } -------------------------------------------------------------------------------- /src/utils/render.js: -------------------------------------------------------------------------------- 1 | import consolidate from 'consolidate'; 2 | 3 | const renderContent = consolidate.swig.render; 4 | /** 5 | * metalsmith 渲染插件 6 | * @return {[type]} [description] 7 | */ 8 | export default function render() { 9 | return function _render( files, metalsmith, next ) { 10 | const meta = metalsmith.metadata(); 11 | Object.keys( files ) 12 | .forEach( function ( file ) { 13 | const str = files[ file ].contents.toString(); 14 | renderContent( str, meta, ( err, res ) => { 15 | if ( err ) { 16 | return next( err ); 17 | } 18 | 19 | files[ file ].contents = new Buffer( res ); 20 | next(); 21 | } ); 22 | } ) 23 | } 24 | } --------------------------------------------------------------------------------