├── .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 |
--------------------------------------------------------------------------------