├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .prettierrc.js ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin └── easy.js ├── commitlint.config.js ├── jest.config.js ├── lib ├── Creator.js ├── add-template.js ├── config │ └── templateGItRepo.json ├── delete-template.js ├── easy-create.js ├── list-template.js └── util │ ├── clearConsole.js │ ├── enhanceErrorMessages.js │ ├── env.js │ ├── getPackageVersion.js │ ├── getVersions.js │ ├── loadRemotePreset.js │ ├── logger.js │ ├── readTemplateData.js │ ├── request.js │ └── spinner.js ├── package.json ├── scripts └── release.js └── test └── utils.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | /node_modules/ 3 | /test/ 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | commonjs: true, 5 | es6: true 6 | }, 7 | extends: ['standard', 'plugin:prettier/recommended'], 8 | globals: {}, 9 | parserOptions: { 10 | ecmaVersion: 2018 11 | }, 12 | plugins: ['prettier'], 13 | rules: { 14 | 'no-useless-return': 0 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | dist/ 7 | .idea/ 8 | 9 | # Editor directories and files 10 | .idea 11 | .vscode 12 | *.suo 13 | *.ntvs* 14 | *.njsproj 15 | *.sln 16 | test/e2e/mock-template-build 17 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | docs 3 | example 4 | coverage 5 | .editorconfig 6 | .eslintignore 7 | .eslintrc.js 8 | .gitignore 9 | .prettierrc.js 10 | .travis.yml 11 | .commitlint.config.js 12 | jest.config.js 13 | package-lock.lock 14 | .vscode 15 | coverage.lcov 16 | .github 17 | CHANGELOG.md 18 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | singleQuote: true, 4 | trailingComma: 'none' 5 | }; 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '8' 4 | cache: 5 | directories: 6 | - node_modules 7 | install: 8 | - npm install 9 | script: 10 | - npm run lint 11 | deploy: 12 | provider: npm 13 | email: "$NPM_EMAIL" 14 | api_key: "$AUTH_TOKEN" 15 | skip_cleanup: true 16 | on: 17 | branch: master 18 | # after_success: 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [1.0.27](https://github.com/NuoHui/easy-cli/compare/v1.0.26...v1.0.27) (2019-06-18) 6 | 7 | 8 | 9 | ### [1.0.26](https://github.com/NuoHui/easy-cli/compare/v1.0.25...v1.0.26) (2019-06-18) 10 | 11 | 12 | ### Features 13 | 14 | * add .npmignore file ([53af792](https://github.com/NuoHui/easy-cli/commit/53af792)) 15 | * 添加test文件夹 ([945855d](https://github.com/NuoHui/easy-cli/commit/945855d)) 16 | * 自动发布时候生成changelog.md ([adfd96b](https://github.com/NuoHui/easy-cli/commit/adfd96b)) 17 | * 配置jest环境 ([b754d8a](https://github.com/NuoHui/easy-cli/commit/b754d8a)) 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 XYZ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 | 5 | 6 | 7 | 8 | 9 |

10 |
11 | 12 | # easyCli 13 | 14 | 一个快速上手的前端脚手架, 轻松创建项目模板, 实现0配置, 快速开发。 15 | 16 | ## Features 17 | 18 | - 支持多类型项目模板(目前[Node](https://github.com/NuoHui/node_code_constructor), [Vue CSR](https://github.com/NuoHui/vue_code_constructor)), 模板都会集成代码扫描, 工作流等, 具体查看模板github地址。 19 | - 支持添加项目模板, 删除项目模板(flok 作为自己的工具推荐使用) 20 | - 支持自动检测脚手架更新 21 | 22 | ## Installation & Quick start 23 | 24 | ### 安装 25 | 26 | Windows系统安装 27 | ``` 28 | $ npm i easy-tool-cli -g 29 | ``` 30 | 31 | Mac下安装 32 | ``` 33 | $ sudo npm i easy-tool-cli -g 34 | ``` 35 | 36 | ### 查看帮助信息 37 | 38 | ``` 39 | $ easy 40 | ``` 41 | 42 | 43 | ### 创建项目 44 | 45 | ``` 46 | # 指定项目名字创建项目 47 | $ easy create 模板名 项目名字 48 | 49 | # 在当前目录创建项目 50 | $ easy create 模板名 . 51 | ``` 52 | 53 | ### 查看所有支持的项目模板 54 | 55 | ``` 56 | $ easy list 57 | ``` 58 | 59 | ### 添加项目模板 60 | 61 | ``` 62 | $ easy add 模板名 模板github仓库地址,支持ssh/https格式 63 | ``` 64 | 65 | ### 删除项目模板 66 | 67 | ``` 68 | $ easy delete 模板名 69 | ``` 70 | 71 | ### 发布到npm 72 | 73 | 执行pkg下的脚本, 自动发版并且生成changelog, travis就会执行检测后续自动发到npm. 74 | ``` 75 | npm run release 76 | ``` 77 | 78 | 79 | ## Changelog 80 | 81 | [Changelog](https://github.com/NuoHui/easy-cli/blob/master/CHANGELOG.md) 82 | 83 | ## TODOLIST 84 | 85 | - 优化Node应用模板 86 | - 优化Vue应用模板 87 | - 添加单测 88 | -------------------------------------------------------------------------------- /bin/easy.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const program = require('commander'); // 命令行工具 4 | const chalk = require('chalk'); // 命令行输出美化 5 | const didYouMean = require('didyoumean'); // 简易的智能匹配引擎 6 | const semver = require('semver'); // npm的语义版本包 7 | const enhanceErrorMessages = require('../lib/util/enhanceErrorMessages.js'); 8 | const requiredNodeVersion = require('../package.json').engines.node; 9 | 10 | didYouMean.threshold = 0.6; 11 | 12 | function checkNodeVersion(wanted, cliName) { 13 | // 检测node版本是否符合要求范围 14 | if (!semver.satisfies(process.version, wanted)) { 15 | console.log( 16 | chalk.red( 17 | 'You are using Node ' + 18 | process.version + 19 | ', but this version of ' + 20 | cliName + 21 | ' requires Node ' + 22 | wanted + 23 | '.\nPlease upgrade your Node version.' 24 | ) 25 | ); 26 | // 退出进程 27 | process.exit(1); 28 | } 29 | } 30 | 31 | // 检测node版本 32 | checkNodeVersion(requiredNodeVersion, '@easy/cli'); 33 | 34 | program 35 | .version(require('../package').version, '-v, --version') // 版本 36 | .usage(' [options]'); // 使用信息 37 | 38 | // 初始化项目模板 39 | program 40 | .command('create ') 41 | .description('create a new project from a template') 42 | .action((templateName, projectName, cmd) => { 43 | // 输入参数校验 44 | validateArgsLen(process.argv.length, 5); 45 | require('../lib/easy-create')(lowercase(templateName), projectName); 46 | }); 47 | 48 | // 添加一个项目模板 49 | program 50 | .command('add ') 51 | .description('add a project template') 52 | .action((templateName, gitRepoAddress, cmd) => { 53 | validateArgsLen(process.argv.length, 5); 54 | require('../lib/add-template')(lowercase(templateName), gitRepoAddress); 55 | }); 56 | 57 | // 列出支持的项目模板 58 | program 59 | .command('list') 60 | .description('list all available project template') 61 | .action(cmd => { 62 | validateArgsLen(process.argv.length, 3); 63 | require('../lib/list-template')(); 64 | }); 65 | 66 | // 删除一个项目模板 67 | program 68 | .command('delete ') 69 | .description('delete a project template') 70 | .action((templateName, cmd) => { 71 | validateArgsLen(process.argv.length, 4); 72 | require('../lib/delete-template')(templateName); 73 | }); 74 | 75 | // 处理非法命令 76 | program.arguments('').action(cmd => { 77 | // 不退出输出帮助信息 78 | program.outputHelp(); 79 | console.log(` ` + chalk.red(`Unknown command ${chalk.yellow(cmd)}.`)); 80 | console.log(); 81 | suggestCommands(cmd); 82 | }); 83 | 84 | // 重写commander某些事件 85 | enhanceErrorMessages('missingArgument', argsName => { 86 | return `Missing required argument ${chalk.yellow(`<${argsName}>`)}`; 87 | }); 88 | 89 | program.parse(process.argv); // 把命令行参数传给commander解析 90 | 91 | // 输入easy显示帮助信息 92 | if (!process.argv.slice(2).length) { 93 | program.outputHelp(); 94 | } 95 | 96 | // easy支持的命令 97 | function suggestCommands(cmd) { 98 | const avaliableCommands = program.commands.map(cmd => { 99 | return cmd._name; 100 | }); 101 | // 简易智能匹配用户命令 102 | const suggestion = didYouMean(cmd, avaliableCommands); 103 | if (suggestion) { 104 | console.log(` ` + chalk.red(`Did you mean ${chalk.yellow(suggestion)}?`)); 105 | } 106 | } 107 | 108 | function lowercase(str) { 109 | return str.toLocaleLowerCase(); 110 | } 111 | 112 | function validateArgsLen(argvLen, maxArgvLens) { 113 | if (argvLen > maxArgvLens) { 114 | console.log( 115 | chalk.yellow( 116 | '\n Info: You provided more than argument. the rest are ignored.' 117 | ) 118 | ); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 配置commit规则 3 | build:主要目的是修改项目构建系统(例如 glup,webpack,rollup 的配置等)的提交 4 | ci:主要目的是修改项目继续集成流程(例如 Travis,Jenkins,GitLab CI,Circle等)的提交 5 | docs:文档更新 6 | feat:新增功能 7 | merge:分支合并 Merge branch ? of ? 8 | fix:bug 修复 9 | perf:性能, 体验优化 10 | refactor:重构代码(既没有新增功能,也没有修复 bug) 11 | style:不影响程序逻辑的代码修改(修改空白字符,格式缩进,补全缺失的分号等,没有改变代码逻辑) 12 | test:新增测试用例或是更新现有测试 13 | revert:回滚某个更早之前的提交 14 | chore:不属于以上类型的其他类型 15 | */ 16 | module.exports = { 17 | extends: ['@commitlint/config-conventional'] 18 | }; 19 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/private/var/folders/6g/kjvjmf8j59j2tf2mm360dqjm0000gn/T/jest_dx", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | clearMocks: true, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: null, 25 | 26 | // The directory where Jest should output its coverage files 27 | coverageDirectory: 'coverage', 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: null, 44 | 45 | // A path to a custom dependency extractor 46 | // dependencyExtractor: null, 47 | 48 | // Make calling deprecated APIs throw helpful error messages 49 | // errorOnDeprecated: false, 50 | 51 | // Force coverage collection from ignored files using an array of glob patterns 52 | // forceCoverageMatch: [], 53 | 54 | // A path to a module which exports an async function that is triggered once before all test suites 55 | // globalSetup: null, 56 | 57 | // A path to a module which exports an async function that is triggered once after all test suites 58 | // globalTeardown: null, 59 | 60 | // A set of global variables that need to be available in all test environments 61 | // globals: {}, 62 | 63 | // An array of directory names to be searched recursively up from the requiring module's location 64 | // moduleDirectories: [ 65 | // "node_modules" 66 | // ], 67 | 68 | // An array of file extensions your modules use 69 | // moduleFileExtensions: [ 70 | // "js", 71 | // "json", 72 | // "jsx", 73 | // "ts", 74 | // "tsx", 75 | // "node" 76 | // ], 77 | 78 | // A map from regular expressions to module names that allow to stub out resources with a single module 79 | // moduleNameMapper: {}, 80 | 81 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 82 | // modulePathIgnorePatterns: [], 83 | 84 | // Activates notifications for test results 85 | // notify: false, 86 | 87 | // An enum that specifies notification mode. Requires { notify: true } 88 | // notifyMode: "failure-change", 89 | 90 | // A preset that is used as a base for Jest's configuration 91 | // preset: null, 92 | 93 | // Run tests from one or more projects 94 | // projects: null, 95 | 96 | // Use this configuration option to add custom reporters to Jest 97 | // reporters: undefined, 98 | 99 | // Automatically reset mock state between every test 100 | // resetMocks: false, 101 | 102 | // Reset the module registry before running each individual test 103 | // resetModules: false, 104 | 105 | // A path to a custom resolver 106 | // resolver: null, 107 | 108 | // Automatically restore mock state between every test 109 | // restoreMocks: false, 110 | 111 | // The root directory that Jest should scan for tests and modules within 112 | // rootDir: null, 113 | 114 | // A list of paths to directories that Jest should use to search for files in 115 | // roots: [ 116 | // "" 117 | // ], 118 | 119 | // Allows you to use a custom runner instead of Jest's default test runner 120 | // runner: "jest-runner", 121 | 122 | // The paths to modules that run some code to configure or set up the testing environment before each test 123 | // setupFiles: [], 124 | 125 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 126 | // setupFilesAfterEnv: [], 127 | 128 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 129 | // snapshotSerializers: [], 130 | 131 | // The test environment that will be used for testing 132 | testEnvironment: 'node' 133 | 134 | // Options that will be passed to the testEnvironment 135 | // testEnvironmentOptions: {}, 136 | 137 | // Adds a location field to test results 138 | // testLocationInResults: false, 139 | 140 | // The glob patterns Jest uses to detect test files 141 | // testMatch: [ 142 | // "**/__tests__/**/*.[jt]s?(x)", 143 | // "**/?(*.)+(spec|test).[tj]s?(x)" 144 | // ], 145 | 146 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 147 | // testPathIgnorePatterns: [ 148 | // "/node_modules/" 149 | // ], 150 | 151 | // The regexp pattern or array of patterns that Jest uses to detect test files 152 | // testRegex: [], 153 | 154 | // This option allows the use of a custom results processor 155 | // testResultsProcessor: null, 156 | 157 | // This option allows use of a custom test runner 158 | // testRunner: "jasmine2", 159 | 160 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 161 | // testURL: "http://localhost", 162 | 163 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 164 | // timers: "real", 165 | 166 | // A map from regular expressions to paths to transformers 167 | // transform: null, 168 | 169 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 170 | // transformIgnorePatterns: [ 171 | // "/node_modules/" 172 | // ], 173 | 174 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 175 | // unmockedModulePathPatterns: undefined, 176 | 177 | // Indicates whether each individual test should be reported during the run 178 | // verbose: null, 179 | 180 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 181 | // watchPathIgnorePatterns: [], 182 | 183 | // Whether to use watchman for file crawling 184 | // watchman: true, 185 | }; 186 | -------------------------------------------------------------------------------- /lib/Creator.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const execa = require('execa'); // 一个child_process封装库 3 | const EventEmitter = require('events'); 4 | const fs = require('fs-extra'); 5 | const { clearConsole } = require('./util/clearConsole'); 6 | const { logWithSpinner, stopSpinner } = require('./util/spinner'); 7 | const { log, warn, error } = require('./util/logger'); 8 | const { hasGit, hasProjectGit } = require('./util/env'); 9 | const fetchRemotePreset = require('./util/loadRemotePreset'); 10 | const { readTemplateJson } = require('./util/readTemplateData'); 11 | 12 | module.exports = class Creator extends EventEmitter { 13 | constructor(name, context) { 14 | super(); 15 | this.name = name; 16 | this.context = process.env.EASY_CLI_CONTEXT = context; // 项目绝对路径 17 | } 18 | 19 | async create(cliOptions = {}) { 20 | const { name, context } = this; 21 | const templateGitRepoJson = readTemplateJson(); 22 | const gitRepoUrl = templateGitRepoJson[process.env.EASY_CLI_TEMPLATE_NAME]; 23 | let tmpdir; 24 | await clearConsole(true); 25 | log(`✨ Creating project in ${chalk.yellow(context)}.`); 26 | log(); 27 | try { 28 | stopSpinner(); 29 | logWithSpinner( 30 | `⠋`, 31 | `Download project template. This might take a while...` 32 | ); 33 | tmpdir = await fetchRemotePreset(gitRepoUrl['download']); 34 | } catch (e) { 35 | stopSpinner(); 36 | error(`Failed fetching remote git repo ${chalk.cyan(gitRepoUrl)}:`); 37 | throw e; 38 | } 39 | // 拷贝到项目文件下 40 | try { 41 | fs.copySync(tmpdir, context); 42 | } catch (error) { 43 | return console.error(chalk.red.dim(`Error: ${error}`)); 44 | } 45 | const shouldInitGit = this.shouldInitGit(); 46 | if (shouldInitGit) { 47 | // 已经安装git 48 | stopSpinner(); 49 | log(); 50 | logWithSpinner(`🗃`, `Initializing git repository...`); 51 | this.emit('creation', { event: 'git-init' }); 52 | // 执行git init 53 | await this.run('git init'); 54 | } 55 | 56 | // commit init state 57 | let gitCommitFailed = false; 58 | if (shouldInitGit) { 59 | await this.run('git add -A'); 60 | try { 61 | await this.run('git', ['commit', '-m', 'init']); 62 | } catch (error) { 63 | gitCommitFailed = true; 64 | } 65 | } 66 | 67 | stopSpinner(); 68 | log(); 69 | log(`🎉 Successfully created project ${chalk.yellow(name)}.`); 70 | log(); 71 | this.emit('creation', { event: 'done' }); 72 | if (gitCommitFailed) { 73 | // commit fail 74 | warn( 75 | `Skipped git commit due to missing username and email in git config.\n` + 76 | `You will need to perform the initial commit yourself.\n` 77 | ); 78 | } 79 | } 80 | 81 | run(command, args) { 82 | if (!args) { 83 | [command, ...args] = command.split(/\s+/); 84 | } 85 | return execa(command, args, { cwd: this.context }); 86 | } 87 | 88 | shouldInitGit() { 89 | if (!hasGit()) { 90 | return false; 91 | } 92 | return !hasProjectGit(this.context); 93 | } 94 | }; 95 | -------------------------------------------------------------------------------- /lib/add-template.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const isGitUrl = require('is-git-url'); 3 | const { stopSpinner } = require('./util/spinner'); 4 | const { log } = require('./util/logger'); 5 | const { 6 | readTemplateJson, 7 | writeTemplateJson 8 | } = require('./util/readTemplateData'); 9 | 10 | async function addProjectTemplate(templateName, gitRepoAddress) { 11 | const templateGitRepoJson = readTemplateJson(); 12 | if (templateGitRepoJson[templateName]) { 13 | console.log(` ` + chalk.red(`template name ${templateName} has exists.`)); 14 | return; 15 | } 16 | if (!isGitUrl(gitRepoAddress)) { 17 | console.log( 18 | ` ` + 19 | chalk.red( 20 | `git repo address ${gitRepoAddress} is not a correct git repo.` 21 | ) 22 | ); 23 | return; 24 | } 25 | // 转化为需要的正确格式 26 | const correctGitRepo = getRealGitRepo(gitRepoAddress); 27 | templateGitRepoJson[templateName] = { 28 | github: gitRepoAddress, 29 | download: correctGitRepo 30 | }; 31 | writeTemplateJson(templateGitRepoJson); 32 | stopSpinner(); 33 | log(); 34 | log(`🎉 Successfully add project template ${chalk.yellow(templateName)}.`); 35 | log(); 36 | } 37 | /** 38 | * format 39 | * https => https://github.com/NuoHui/node_code_constructor.git 40 | * ssh => git@github.com:NuoHui/node_code_constructor.git 41 | * want => github:NuoHui/node_code_constructor 42 | */ 43 | function getRealGitRepo(gitRepo) { 44 | const sshRegExp = /^git@github.com:(.+)\/(.+).git$/; 45 | const httpsRegExp = /^https:\/\/github.com\/(.+)\/(.+).git$/; 46 | if (sshRegExp.test(gitRepo)) { 47 | // ssh 48 | const match = gitRepo.match(sshRegExp); 49 | return `github:${match[1]}/${match[2]}`; 50 | } 51 | if (httpsRegExp.test(gitRepo)) { 52 | // https 53 | const match = gitRepo.match(httpsRegExp); 54 | return `github:${match[1]}/${match[2]}`; 55 | } 56 | } 57 | 58 | module.exports = (...args) => { 59 | return addProjectTemplate(...args).catch(err => { 60 | console.error(err); 61 | process.exit(1); 62 | }); 63 | }; 64 | -------------------------------------------------------------------------------- /lib/config/templateGItRepo.json: -------------------------------------------------------------------------------- 1 | {"node":{"github":"https://github.com/NuoHui/node_code_constructor.git","download":"github:NuoHui/node_code_constructor"},"vue":{"github":"https://github.com/NuoHui/vue_code_constructor.git","download":"github:NuoHui/vue_code_constructor"}} -------------------------------------------------------------------------------- /lib/delete-template.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const inquirer = require('inquirer'); 3 | const { stopSpinner } = require('./util/spinner'); 4 | const { log } = require('./util/logger'); 5 | const { 6 | readTemplateJson, 7 | writeTemplateJson 8 | } = require('./util/readTemplateData'); 9 | async function deleteTemplate(templateName) { 10 | const templateGitRepoJson = readTemplateJson(); 11 | if (!templateGitRepoJson[templateName]) { 12 | console.log( 13 | ` ` + chalk.red(`template name ${templateName} has not exists.`) 14 | ); 15 | return; 16 | } 17 | const { ok } = await inquirer.prompt([ 18 | { 19 | name: 'ok', 20 | type: 'confirm', 21 | message: `Make sure you want to delete template name ${templateName}?` 22 | } 23 | ]); 24 | if (!ok) { 25 | return; 26 | } 27 | delete templateGitRepoJson[templateName]; 28 | writeTemplateJson(templateGitRepoJson); 29 | stopSpinner(); 30 | log(); 31 | log( 32 | `🎉 Successfully delete project template ${chalk.yellow(templateName)}.` 33 | ); 34 | log(); 35 | } 36 | 37 | module.exports = (...args) => { 38 | return deleteTemplate(...args).catch(err => { 39 | console.error(err); 40 | process.exit(1); 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /lib/easy-create.js: -------------------------------------------------------------------------------- 1 | const program = require('commander'); 2 | const chalk = require('chalk'); 3 | const inquirer = require('inquirer'); 4 | const validateProjectName = require('validate-npm-package-name'); 5 | 6 | const path = require('path'); 7 | const fs = require('fs'); 8 | const exec = require('child_process').exec; 9 | 10 | const { pauseSpinner } = require('../lib/util/spinner'); 11 | const Creator = require('./Creator'); 12 | const { clearConsole } = require('./util/clearConsole'); 13 | const { readTemplateJson } = require('./util/readTemplateData'); 14 | 15 | async function create(templateName, projectName, options) { 16 | // TODO options方便后续扩展 17 | // 项目模板白名单 18 | const templateGitRepoJson = readTemplateJson(); 19 | // 校验模板是否存在 20 | if (!templateGitRepoJson[templateName]) { 21 | console.log(` ` + chalk.red(`Unknown template name ${templateName}.`)); 22 | program.outputHelp(); 23 | return; 24 | } 25 | // 获取node进程的工作目录 26 | const cwd = process.cwd(); 27 | // 判断是否是当前目录 28 | const inCurrentDir = projectName === '.'; 29 | // 获取项目名(当前目录 or 指定的项目名) 30 | const name = inCurrentDir ? path.relative('../', cwd) : projectName; 31 | // 真正的目录地址 32 | const targetDir = path.resolve(cwd, projectName); 33 | // 校验项目名(包名)是否合法 34 | const validateResult = validateProjectName(name); 35 | if (!validateResult.validForNewPackages) { 36 | // 打印出错误以及警告 37 | console.error(chalk.red(`Invalid project name: "${name}"`)); 38 | validateResult.errors && 39 | validateResult.errors.forEach(error => { 40 | console.error(chalk.red.dim(`Error: ${error}`)); 41 | }); 42 | validateResult.warnings && 43 | validateResult.warnings.forEach(warn => { 44 | console.error(chalk.red.dim(`Warning: ${warn}`)); 45 | }); 46 | process.exit(1); 47 | } 48 | if (fs.existsSync(targetDir)) { 49 | // TODO 可通过配置强制删除 50 | // 目录存在有两种情况: 1. 当前目录创建 2. 确实存在同名目录 51 | await clearConsole(); 52 | if (inCurrentDir) { 53 | // 当前目录下创建给用户提示 54 | const { ok } = await inquirer.prompt([ 55 | { 56 | name: 'ok', 57 | type: 'confirm', 58 | message: `Generate project in current directory?` 59 | } 60 | ]); 61 | if (!ok) { 62 | return; 63 | } 64 | } else { 65 | // 待创建目录已经存在 66 | const { action } = await inquirer.prompt([ 67 | { 68 | name: 'action', 69 | type: 'list', 70 | message: `Target directory ${chalk.cyan(targetDir)} 71 | already exists. Pick an action:`, 72 | choices: [ 73 | { name: 'Overwrite', value: 'overwrite' }, 74 | { 75 | name: 'Merge', 76 | value: 'merge' 77 | }, 78 | { 79 | name: 'Cancel', 80 | value: false 81 | } 82 | ] 83 | } 84 | ]); 85 | if (!action) { 86 | return; 87 | } else if (action === 'overwrite') { 88 | console.log(`\nRemoving ${chalk.cyan(targetDir)}...`); 89 | await exec(`rm -rf ${targetDir}`); 90 | } 91 | } 92 | } 93 | 94 | // 目录不存在 95 | process.env.EASY_CLI_TEMPLATE_NAME = templateName; 96 | const creator = new Creator(name, targetDir); 97 | await creator.create(options); 98 | } 99 | 100 | module.exports = (templateName, projectName, ...args) => { 101 | return create(templateName, projectName, ...args).catch(err => { 102 | pauseSpinner(); 103 | console.error(err); 104 | process.exit(1); 105 | }); 106 | }; 107 | -------------------------------------------------------------------------------- /lib/list-template.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const { readTemplateJson } = require('./util/readTemplateData'); 3 | const { stopSpinner } = require('./util/spinner'); 4 | const { log } = require('./util/logger'); 5 | async function listAllTemplate() { 6 | const templateGitRepoJson = readTemplateJson(); 7 | for (let key in templateGitRepoJson) { 8 | stopSpinner(); 9 | log(); 10 | log( 11 | `➡️ Template name ${chalk.yellow(key)}, Github address ${chalk.yellow( 12 | templateGitRepoJson[key]['github'] 13 | )}` 14 | ); 15 | log(); 16 | } 17 | if (!Object.keys(templateGitRepoJson).length) { 18 | stopSpinner(); 19 | log(); 20 | log(`💔 No any template.`); 21 | log(); 22 | } 23 | } 24 | 25 | module.exports = () => { 26 | return listAllTemplate().catch(err => { 27 | console.error(err); 28 | process.exit(1); 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /lib/util/clearConsole.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const semver = require('semver'); 3 | 4 | const { clearConsole } = require('./logger.js'); 5 | const getVersions = require('./getVersions.js'); 6 | 7 | exports.generateTitle = async function(checkUpdate) { 8 | const { current, latest } = await getVersions(); 9 | let title = chalk.bold.blue(`EASY CLI v${current}`); 10 | if (checkUpdate && semver.gt(latest, current)) { 11 | // 提示升级 12 | title += chalk.green(` 13 | ┌────────────────────${`─`.repeat(latest.length)}──┐ 14 | │ Update available: ${latest} │ 15 | └────────────────────${`─`.repeat(latest.length)}──┘`); 16 | } 17 | return title; 18 | }; 19 | 20 | exports.clearConsole = async function clearConsoleWithTitle(checkUpdate) { 21 | const title = await exports.generateTitle(checkUpdate); 22 | clearConsole(title); 23 | }; 24 | -------------------------------------------------------------------------------- /lib/util/enhanceErrorMessages.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('commander'); 2 | const chalk = require('chalk'); 3 | 4 | module.exports = (methodName, log) => { 5 | Command.prototype[methodName] = function(...args) { 6 | if (methodName === 'unknownOption' && this._allowUnknowOption) { 7 | return false; 8 | } 9 | this.outputHelp(); 10 | console.log(` ` + chalk.red(log(...args))); 11 | console.log(); 12 | process.exit(1); 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /lib/util/env.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require('child_process'); 2 | const LRU = require('lru-cache'); // 在内存中管理缓存数据 3 | let _hasGit; 4 | const _projectGit = new LRU({ 5 | max: 10, // 缓存大小 6 | maxAge: 1000 // 缓存过期时间 7 | }); 8 | 9 | // 测试是否安装了git 10 | exports.hasGit = () => { 11 | if (_hasGit) { 12 | return _hasGit; 13 | } 14 | try { 15 | // 执行shell 16 | execSync('git --version', { stdio: 'ignore' }); 17 | return (_hasGit = true); 18 | } catch (error) { 19 | return (_hasGit = false); 20 | } 21 | }; 22 | 23 | // 测试该项目是否已经是一个git repo 24 | exports.hasProjectGit = cwd => { 25 | if (_projectGit.has(cwd)) { 26 | return _projectGit.get(cwd); 27 | } 28 | let result; 29 | try { 30 | execSync('git status', { stdio: 'ignore' }, cwd); 31 | result = true; 32 | } catch (error) { 33 | result = false; 34 | } 35 | _projectGit.set(cwd, result); 36 | return result; 37 | }; 38 | -------------------------------------------------------------------------------- /lib/util/getPackageVersion.js: -------------------------------------------------------------------------------- 1 | const request = require('./request.js'); 2 | module.exports = async function getPackageVersion(id, range = '') { 3 | // const registry = (await require('./shouldUseTaobao')()) 4 | // ? `https://registry.npm.taobao.org` 5 | // : `https://registry.npmjs.org`; 6 | const registry = `https://registry.npm.taobao.org`; // TODO区分淘宝源与npm源 7 | let result; 8 | try { 9 | result = await request.get( 10 | // 关于npm对package的定义 https://docs.npmjs.com/about-packages-and-modules 11 | // https://registry.npmjs.org/easy-tool-cli/latest 12 | `${registry}/${encodeURIComponent(id).replace(/^%40/, '@')}/${range}` 13 | ); 14 | } catch (err) { 15 | return err; 16 | } 17 | return result; 18 | }; 19 | -------------------------------------------------------------------------------- /lib/util/getVersions.js: -------------------------------------------------------------------------------- 1 | const semver = require('semver'); 2 | 3 | // 缓存上一次的最新版本号以及本地版本号 4 | let sessionCached; 5 | // 缓存上次版本号以及获取时间 6 | let saveOptions = {}; 7 | 8 | module.exports = async function getVersions() { 9 | if (sessionCached) { 10 | return sessionCached; 11 | } 12 | let latest; 13 | let local = require('../../package.json').version; 14 | // 提供默认值作为第一次计算 15 | const { latestVersion = local, lastChecked = 0 } = saveOptions; 16 | // 本地最新的版本 17 | const cached = latestVersion; 18 | // 一天检查一次 19 | const daysPassed = (Date.now() - lastChecked) / (3600 * 1000 * 24); 20 | if (daysPassed > 1) { 21 | latest = await getAndCacheLatestVersion(cached); 22 | } else { 23 | getAndCacheLatestVersion(cached); 24 | latest = cached; 25 | } 26 | return (sessionCached = { 27 | latest, 28 | current: local 29 | }); 30 | }; 31 | 32 | // 获取最新版本并且缓存在磁盘本地以便下次使用 33 | async function getAndCacheLatestVersion(cached) { 34 | const getPackageVersion = require('./getPackageVersion'); 35 | const res = await getPackageVersion('easy-tool-cli', 'latest'); 36 | if (res.statusCode === 200) { 37 | const { version } = res.body; 38 | // 如果获得版本号是合法的并且与之前缓存的版本不一致说明是最新的 39 | if (semver.valid(version) && version !== cached) { 40 | saveOptions = { 41 | latestVersion: version, 42 | lastChecked: Date.now() 43 | }; 44 | return version; 45 | } 46 | } 47 | return cached; 48 | } 49 | -------------------------------------------------------------------------------- /lib/util/loadRemotePreset.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | 3 | module.exports = async function fetchRemotePreset(name, clone = false) { 4 | const os = require('os'); 5 | const path = require('path'); 6 | const download = require('download-git-repo'); 7 | // 生成临时目录, 方便后续中间件对其抓取下来的模板进行处理 8 | const tmpdir = path.resolve(os.tmpdir(), 'easy-cli'); 9 | if (clone) { 10 | await fs.remove(tmpdir); 11 | } 12 | return new Promise((resolve, reject) => { 13 | download(name, tmpdir, { clone }, err => { 14 | if (err) { 15 | return reject(err); 16 | } 17 | return resolve(tmpdir); 18 | }); 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /lib/util/logger.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const readline = require('readline'); 3 | const EventEmitter = require('events'); 4 | const padStart = String.prototype.padStart; 5 | 6 | const chalkTag = msg => chalk.bgBlackBright.white.dim(` ${msg} `); 7 | 8 | function _log(type, tag, message) { 9 | if (message) { 10 | exports.events.emit('log', { 11 | message, 12 | type, 13 | tag 14 | }); 15 | } 16 | } 17 | 18 | const format = (label, msg) => { 19 | return msg.split('\n').map((line, i) => { 20 | return i === 0 21 | ? `${label} ${line}` 22 | : padStart(line, chalk.reset(label).length); // 对齐 23 | }); 24 | }; 25 | 26 | exports.events = new EventEmitter(); 27 | 28 | exports.clearConsole = title => { 29 | if (process.stdout.isTTY) { 30 | // 判断是否在终端环境 31 | const blank = '\n'.repeat(process.stdout.rows); 32 | console.log(blank); 33 | // 在终端移动光标到标准输出流的起始坐标位置, 然后清除给定的TTY流 34 | readline.cursorTo(process.stdout, 0, 0); 35 | readline.clearScreenDown(process.stdout); 36 | if (title) { 37 | console.log(title); 38 | } 39 | } 40 | }; 41 | 42 | exports.warn = (msg, tag = null) => { 43 | console.warn( 44 | format( 45 | chalk.bgYellow.black(' WARN ') + (tag ? chalkTag(tag) : ''), 46 | chalk.yellow(msg) 47 | ) 48 | ); 49 | _log('warn', tag, msg); 50 | }; 51 | 52 | exports.error = (msg, tag = null) => { 53 | console.error( 54 | format(chalk.bgRed(' ERROR ') + (tag ? chalkTag(tag) : ''), chalk.red(msg)) 55 | ); 56 | _log('error', tag, msg); 57 | if (msg instanceof Error) { 58 | console.error(msg.stack); 59 | _log('error', tag, msg.stack); 60 | } 61 | }; 62 | 63 | exports.log = (msg = '', tag = null) => { 64 | tag ? console.log(format(chalkTag(tag), msg)) : console.log(msg); 65 | _log('log', tag, msg); 66 | }; 67 | 68 | exports.info = (msg, tag = null) => { 69 | console.log( 70 | format(chalk.bgBlue.black(' INFO ') + (tag ? chalkTag(tag) : ''), msg) 71 | ); 72 | _log('info', tag, msg); 73 | }; 74 | -------------------------------------------------------------------------------- /lib/util/readTemplateData.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | exports.readTemplateJson = () => { 5 | return JSON.parse( 6 | fs.readFileSync( 7 | path.join(__dirname, '../config/templateGitRepo.json'), 8 | 'utf8' 9 | ) 10 | ); 11 | }; 12 | 13 | exports.writeTemplateJson = json => { 14 | return fs.writeFileSync( 15 | path.join(__dirname, '../config/templateGItRepo.json'), 16 | JSON.stringify(json), 17 | 'utf8' 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /lib/util/request.js: -------------------------------------------------------------------------------- 1 | const request = { 2 | get(url) { 3 | const request = require('request-promise-native'); 4 | const reqOptions = { 5 | method: 'GET', 6 | timeout: 30000, 7 | resolveWithFullResponse: true, 8 | json: true, 9 | url 10 | }; 11 | return request(reqOptions); 12 | } 13 | }; 14 | 15 | module.exports = request; 16 | -------------------------------------------------------------------------------- /lib/util/spinner.js: -------------------------------------------------------------------------------- 1 | const ora = require('ora'); // 美化终端交互 2 | const chalk = require('chalk'); 3 | const spinner = ora(); 4 | 5 | let lastMsg = null; 6 | 7 | exports.logWithSpinner = (symbol, msg) => { 8 | if (!msg) { 9 | msg = symbol; 10 | symbol = chalk.green('✔'); 11 | } 12 | if (lastMsg) { 13 | // 清除上次的spinner 14 | spinner.stopAndPersist({ 15 | symbol: lastMsg.symbol, 16 | text: lastMsg.text 17 | }); 18 | } 19 | spinner.text = ' ' + msg; 20 | lastMsg = { 21 | symbol: symbol + ' ', 22 | text: msg 23 | }; 24 | spinner.start(); 25 | }; 26 | 27 | exports.stopSpinner = persist => { 28 | if (lastMsg && persist !== false) { 29 | spinner.stopAndPersist({ 30 | symbol: lastMsg.symbol, 31 | text: lastMsg.text 32 | }); 33 | } else { 34 | spinner.stop(); 35 | } 36 | lastMsg = null; 37 | }; 38 | 39 | exports.pauseSpinner = function() { 40 | spinner.stop(); 41 | }; 42 | 43 | exports.resumeSpinner = function() { 44 | spinner.start(); 45 | }; 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "easy-tool-cli", 3 | "version": "1.0.27", 4 | "description": "a simple CLI for scaffolding of FE", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint --ext .js ./", 8 | "release": "node scripts/release.js", 9 | "test": "jest --color" 10 | }, 11 | "bin": { 12 | "easy": "bin/easy.js" 13 | }, 14 | "publishConfig": { 15 | "access": "public" 16 | }, 17 | "directories": { 18 | "lib": "lib" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/NuoHui/easy-cli.git" 23 | }, 24 | "keywords": [ 25 | "cli", 26 | "tool" 27 | ], 28 | "husky": { 29 | "hooks": { 30 | "pre-commit": "lint-staged", 31 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 32 | } 33 | }, 34 | "lint-staged": { 35 | "**/*.js": [ 36 | "eslint --fix", 37 | "prettier --write", 38 | "git add" 39 | ] 40 | }, 41 | "author": "xyz ", 42 | "license": "MIT", 43 | "devDependencies": { 44 | "@commitlint/cli": "^7.6.1", 45 | "@commitlint/config-conventional": "^7.6.0", 46 | "babel-eslint": "^10.0.1", 47 | "cross-env": "^5.2.0", 48 | "eslint": "^5.16.0", 49 | "eslint-config-prettier": "^4.3.0", 50 | "eslint-config-standard": "^12.0.0", 51 | "eslint-plugin-import": "^2.17.2", 52 | "eslint-plugin-node": "^9.0.1", 53 | "eslint-plugin-prettier": "^3.1.0", 54 | "eslint-plugin-promise": "^4.1.1", 55 | "eslint-plugin-standard": "^4.0.0", 56 | "husky": "^2.3.0", 57 | "jest": "^24.8.0", 58 | "lint-staged": "^8.1.7", 59 | "nodemon": "^1.19.0", 60 | "prettier": "^1.17.1", 61 | "standard-version": "^8.0.1" 62 | }, 63 | "engines": { 64 | "node": ">=8.0.0" 65 | }, 66 | "dependencies": { 67 | "chalk": "^2.4.2", 68 | "commander": "^2.20.0", 69 | "didyoumean": "^1.2.1", 70 | "download-git-repo": "^2.0.0", 71 | "execa": "^1.0.0", 72 | "fs-extra": "^8.0.1", 73 | "inquirer": "^6.3.1", 74 | "is-git-url": "^1.0.0", 75 | "lru-cache": "^5.1.1", 76 | "ora": "^3.4.0", 77 | "request": "^2.88.0", 78 | "request-promise-native": "^1.0.7", 79 | "semver": "^6.1.1", 80 | "validate-npm-package-name": "^3.0.0" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /scripts/release.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require('child_process'); 2 | const semver = require('semver'); 3 | const inquirer = require('inquirer'); 4 | 5 | const currentVerison = require('../package.json').version; 6 | 7 | const release = async () => { 8 | console.log(`Current easy cli version is ${currentVerison}`); 9 | const releaseActions = ['patch', 'minor', 'major']; 10 | const versions = {}; 11 | // 生成预发布版本标示 12 | releaseActions.map(r => (versions[r] = semver.inc(currentVerison, r))); 13 | const releaseChoices = releaseActions.map(r => ({ 14 | name: `${r} (${versions[r]})`, 15 | value: r 16 | })); 17 | // 选择发布方式 18 | const { release } = await inquirer.prompt([ 19 | { 20 | name: 'release', 21 | message: 'Select a release type', 22 | type: 'list', 23 | choices: [...releaseChoices] 24 | } 25 | ]); 26 | // 优先自定义版本 27 | const version = versions[release]; 28 | // 二次确认发布 29 | const { yes } = await inquirer.prompt([ 30 | { 31 | name: 'yes', 32 | message: `Confirm releasing ${version}`, 33 | type: 'confirm' 34 | } 35 | ]); 36 | if (yes) { 37 | execSync(`standard-version -r ${release}`, { 38 | stdio: 'inherit' 39 | }); 40 | } 41 | }; 42 | 43 | release().catch(err => { 44 | console.error(err); 45 | process.exit(1); 46 | }); 47 | -------------------------------------------------------------------------------- /test/utils.test.js: -------------------------------------------------------------------------------- 1 | const env = require('../lib/util/env'); 2 | 3 | test('Has git.', () => { 4 | expect(env.hasGit()).toBeTruthy(); 5 | }); 6 | 7 | test('hasProjectGit', () => { 8 | expect(env.hasProjectGit()).toBeTruthy(); 9 | }); 10 | --------------------------------------------------------------------------------