Commit 94e83206 authored by lipengcheng 's avatar lipengcheng

init

parents
File added
.history
node_modules
\ No newline at end of file
[toc]
## 安装cli
1. 安装nrm
```bash
npm intall -g nrm
# yarn
yarn global add nrm
```
2. 添加并切换到私有源
`nrm add laihua http://40.65.185.26:8083/`
`nrm use laihua`
> 如果要切换回npm/yarn源或者其他源,使用`nrm ls`查看可用源列表,`nrm use <registry>`切换。
3. 安装
```bash
npm install -g @lhfe/cli
# yarn
yarn global add @lhfe/cli
```
4. 拉取项目模板
```bash
lhfe crate [projectName]
```
> 查看使用帮助:`lhfe -h`
## TODO
- [x] commander配置多条命令(命令直接对应文件)
- [ ] 显示环境配置信息 --nuxt
- [x] 未知的命令给出相关的建议命令
- [x] 通过pkg.engines及semver指定项目的工作环境
- [ ] this.run给仓库做git初始化操作 -- vuecli
- [ ] vuecli中对ora(spinner)使用的封装
- [ ] vuecli中对log的封装(logger)
- [ ] command <list> 显示模板列表
- [ ] 全局捕获creator错误 -- vuecli
- [ ] 编译复制的时候去掉不需要的文件
- [ ] 动态生成package.json、readme文件内容
- [ ] 拼接package.json内容的方式
- [ ] 初始化项目时端口防重
- [ ] 脚手架版本提示:"New version available 4.5.11 → 4.5.12";提示信息以方框的形式展示
- [ ] 删除生成的项目的git目录
#! /usr/bin/env node
const { program } = require('commander')
const chalk = require('chalk')
const leven = require('leven')
const { checkNodeVersion } = require('../lib/utils')
const PKG = require('../package.json')
checkNodeVersion(PKG.engines.node, PKG.name)
program
.command(`create <app-name>`)
.description(`create a new project`)
.option(`-f, --force`, `overwrite target direcrtory if it exists`)
.action((name, cmd) => {
require('../lib/create.js')(name, cmd)
})
program
.command(`config [value]`)
.description(`inspect and modify the config file`)
.option(`-g, --get <path>`, 'get value from option')
.option(`-s, --set <path> <value>`)
.option(`-d, --delete <path>`, `delete option from config`)
.action((value, cmd) => {
console.log(`value:`,value);
console.log(`cmd:`,cmd);
})
// 执行独立可执行文件
program
.command('start <service>', 'start named service')
.command('stop [service]', 'stop named service, or all if no name supplied');
program
.version(`lhfe ${PKG.version}`)
.usage(`<command> [option]`)
program
.on(`--help`, () => console.log(`Run ${chalk.cyan(`lhfe <commadn> -h`)} show details`))
program.on('option:verbose', function () {
console.log(`this.verbose:`,this.verbose);
process.env.VERBOSE = this.verbose;
});
/**
* 未知的命令给出帮助信息及相关命令建议信息
*/
program
.on('command:*', ([cmd]) => {
program.outputHelp()
console.log()
console.log(` ` + chalk.red(`Unknown command ${chalk.yellow(cmd)}`))
suggestCommands(cmd)
process.exitCode = 1
})
// 解析参数
program.parse(process.argv)
// console.log('Options: ', program.opts());
// console.log('Remaining arguments: ', program.args);
/**
* @param unknownCommand 未知的自定义命令
*/
function suggestCommands(unknownCommand) {
const availableCommands = program.commands.map(cmd => cmd._name)
let suggestion = ''
availableCommands.forEach(cmd => {
const isBestMatch = leven(cmd, unknownCommand) < leven(suggestion, unknownCommand)
if (leven(cmd, unknownCommand) < 3 && isBestMatch) {
suggestion = cmd
}
})
if (suggestion) {
console.log(` ` + chalk.red(`Did you mean ${chalk.yellow(suggestion)}?`))
}
}
\ No newline at end of file
#! /usr/bin/env node
console.log('start====');
\ No newline at end of file
#! /usr/bin/env node
console.log('stop====');
\ No newline at end of file
const inquirer = require('inquirer')
const { fetchRepoList, fetchTagList } = require('./request')
const { wrapLoading } = require('./utils')
const downlodRepo = require('download-git-repo') //不支持promise
const util = require('util')
const path = require('path')
const { generate } = require('./generate')
const fse = require('fs-extra');
const ORG = 'zhu-cli'
// const ORG = 'vuejs'
module.exports = class Creator {
constructor(targetDir, meta, repo) {
this.targetDir = targetDir
this.meta = meta
this.repo = repo
this.tmpTarget = path.resolve(targetDir, '.lhfe-cli')
this.downlodRepo = util.promisify(downlodRepo)
}
asyncDownloadRepo(...args){
return new Promise((resolve,reject) => {
download(...args,(err) => {
err ? reject(err) : resolve()
})
})
}
async fetchRepo() {
let repos = await wrapLoading(fetchRepoList, 'Waitting for fetch repo list...')
if (!repos || !Array.isArray(repos)) {
process.exit()
};
repos = repos.map(i => i.name)
const { repo } = await inquirer.prompt([
{
name: 'repo',
type: 'list',
choices: repos,
message: 'Choose a template'
}
])
return repo
}
async fetchTag(repo) {
let tags = await wrapLoading(fetchTagList, 'Waitting for fetch repo tags....', repo)
if (!tags) return;
tags = tags.map(i => i.name)
const { tag } = await inquirer.prompt([
{
name: 'tag',
type: 'list',
choices: tags,
message: 'Choose a tag',
}
])
return tag
}
async exeDownload(repo, tag) {
// let requestUrl = `${ORG}/${repo}${tag ? `#${tag}` : ''}`
let requestUrl = ORG + '/' + repo + tag ? `#${tag}` : ''
console.log(`requestUrl:`,requestUrl);
// 优化:先下载到系统目录中(用作缓存),再使用ejs handlerbar渲染模板,然后再写入到目标目录
// await this.downlodRepo(requestUrl, path.resolve(process.cwd(), `${repo}@${tag}`))
await wrapLoading(this.downlodRepo, 'Wating for downloading repo...', requestUrl, path.resolve(process.cwd(), `${repo}@${tag}`))
}
async create(type) {
switch (type) {
case 'github':
this.createFromGithub()
break;
default:
this.createFromGitlab()
break;
}
}
async createFromGithub() {
const repo = await this.fetchRepo()
const tag = await this.fetchTag(repo)
// 拉取仓库
await this.exeDownload(repo, tag)
// 进入目录安装依赖
}
async createFromGitlab() {
// const url = `direct:https://gitlab.ilaihua.com/laihua-web/laihua-webh5.git` // 会报错git checkout,是git仓库
// const url = `direct:https://gitlab.ilaihua.com/laihua-web/laihua-webh5.git#develop` // 没报错,不是git仓库,可以拉取到指定分支
// const url = `direct:https://gitlab.ilaihua.com/laihua-web/laihua-webh5` // 会报错 是git仓库
// const url = `direct:https://gitlab.ilaihua.com/laihua-web/laihua-webh5#uat` // 没报错,不是git仓库,可以拉取到指定分支
// const url = `https://gitlab.ilaihua.com:laihua-web/laihua-webh5` // 报错,是git仓库
// const url = `https://gitlab.ilaihua.com:laihua-web/laihua-webh5#uat` // 没报错 不是git仓库,可以拉取到指定分支
// const url = `direct:https://gitlab.ilaihua.com/lhfe/mould-vuecli3#feature/tpl`
const url = `direct:https://gitlab.ilaihua.com/lhfe/${this.repo}.git#feature/tpl`
// const url = `direct:https://gitlab.ilaihua.com/lhfe/mould-nuxt-h5.git#feature/tpl`
await wrapLoading(this.downlodRepo, 'Waitting for download...', url, this.tmpTarget, { clone: true })
// 编译模板
// const source = path.resolve(__dirname, '../../aa/bbbb/.lhfe-cli')
// const source = path.resolve(__dirname, '../tpl/testgit')
await generate(this.tmpTarget, this.targetDir, this.meta)
// 移除临时目录
await fse.remove(this.tmpTarget)
// 进入项目初始化git
// 。。
}
}
\ No newline at end of file
const path = require('path')
const fse = require('fs-extra');
const inquirer = require('inquirer');
const prompts = require('./prompts')
const Creator = require('./Creator')
const { validatePkgName } = require('../lib/utils')
const chalk = require('chalk')
// ======
module.exports = async function(projectname, options){
const cwd = process.cwd()
// path.relative('../', cwd) === path.basename(cwd)
const basename = path.basename(cwd) || path.relative('../', cwd)
let inCurrent = false
if (projectname === basename) {
const { ok } = await inquirer.prompt(prompts.sameName)
inCurrent = ok
} else if (projectname === '.') {
const { ok } = await inquirer.prompt(prompts.initCurrent)
inCurrent = ok
}
const pkgName = inCurrent ? basename : projectname
const targetDir = path.resolve(cwd, inCurrent ? '' : projectname)
validatePkgName(pkgName)
// 已存在同名目录
if (!inCurrent && fse.existsSync(targetDir)){
// 有force参数,直接强制删除
// todo: 强制删除前,如果现有目录非空,可以给出提示
// const files = fse.existsSync(outDir) ? fse.readdirSync(outDir) : []
// files.length
if(options.force){
await fse.remove(targetDir)
} else {
// 提示是否要覆盖
const answers = await inquirer.prompt(prompts.overwrite)
const { action } = answers
if (!action) {
return
} else if (action === 'overwrite') {
console.log(`\nRemoving ${chalk.cyan(targetDir)}...`)
fse.remove(targetDir)
}
}
}
const { sourceType } = await inquirer.prompt(prompts.source)
let repo = 'mould-nuxt-h5'
if (sourceType === 'gitlab'){
repo = (await inquirer.prompt(prompts.repos)).repo
}
const meta = await inquirer.prompt(prompts.initMetaPrompts(projectname))
const creator = new Creator(targetDir, meta, repo)
creator.create(sourceType)
}
\ No newline at end of file
const Metalsmith = require('metalsmith')
const Handlebars = require('handlebars')
const path = require('path');
const { promisify } = require('util');
const render = require('consolidate').ejs.render; // 统一所有的模板引擎
const renderPro = promisify(render);
const ncp = require('ncp');
const ncpPro = promisify(ncp);
const editPkg = require('../utils/editPkg')
async function generate(source, target, meta) {
console.log(`source==:`,source);
console.log(`target:`,target);
console.log(`meta:`, meta);
const extra = {
name: meta.pkgName,
}
try {
await editPkg(`${source}/package.json`, extra)
} catch (err) {
console.log(`err:`,err);
}
let result = await new Promise((resolve, reject) => {
Metalsmith(source)
.metadata(meta)
.source(source)
.destination(target)
.use(async (files, metal, done) => {
// const m = metal.metadata();
// Object.assign(m, {email: 'xxxxx', description:'this is an extra attr' });
const excludeFiles = ['CHANGELOG.md']
excludeFiles.forEach(file => delete files[file])
done()
})
.use(async (files, metal, done) => {
const meta = metal.metadata();
Object.keys(files).forEach(async (file) => {
let content = files[file].contents.toString();
// 判断是否是模板 可用正则匹配
if (content.includes('<%')) {
content = await renderPro(content, meta);
files[file].contents = Buffer.from(content);
}
})
done()
})
.build((err) => {
err ? reject(false) : resolve(true)
})
}).catch(err => console.log(`err:`, err))
return result
}
module.exports = {
generate
}
const { execSync } = require('child_process')
module.exports.source = [
{
name: 'sourceType',
type: 'list',
message: `Do u want to create project from Github or Gitlab?`,
choices: [
{ name: 'Gitlab', value: 'gitlab' },
{ name: 'Github', value: 'github' },
]
}
]
module.exports.sameName = [
{
name: 'ok',
type: 'confirm',
message: `项目名称与当前工作目录名称一样,是否直接在工作目录初始化项目?`
}
]
module.exports.initCurrent = [
{
name: 'ok',
type: 'confirm',
message: `Generate project in current directory?`
}
]
module.exports.overwrite = [
{
name: 'action',
type: 'list',
message: `Target dir has exists`,
choices: [
{ name: 'Overwrite', value: 'overwrite' },
{ name: 'Cncel', value: false },
]
}
]
module.exports.repos = [
{
name: 'repo',
type: 'list',
message: `选择要拉取的仓库`,
choices: [
{ name: 'mould-nuxt', value: 'mould-nuxt' },
{ name: 'mould-nuxt-h5', value: 'mould-nuxt-h5' },
{ name: 'mould-vuecli3', value: 'mould-vuecli3' },
]
}
]
module.exports.initMetaPrompts = function (pkgName='') {
let user = execSync('git config --global user.name', { encoding: 'utf-8' });
let email = execSync('git config --global user.email', { encoding: 'utf-8' });
user = user.trim();
email = email.trim();
return [
{
type: 'confirm',
name: 'private',
message: 'Is the project private ?'
},
{
type: 'input',
name: 'pkgName',
message: 'package name',
default: pkgName,
// validate(input) {
// if (input.trim().length === 0) {
// return;
// }
// }
},
{
type: 'input',
name: 'port',
message: 'project port',
// validate(input) {
// if (input.trim().length === 0) {
// return;
// }
// }
},
{
type: 'input',
name: 'description',
message: 'description'
},
// {
// type: 'list',
// name: 'license',
// message: 'license',
// 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']
// },
{
type: 'input',
name: 'author',
message: 'author',
default: `${user} ${email}`
},
];
}
const axios = require('axios')
axios.interceptors.response.use(res => res.data)
const ORG = 'zhu-cli'
// const ORG = 'vuejs'
module.exports = {
async fetchRepoList (){
const repoApi = `https://api.github.com/orgs/${ORG}/repos`
const params = { username: 'slevin57' }
return await axios.get(repoApi)
},
async fetchTagList(repo) {
const tagApi = `https://api.github.com/repos/${ORG}/${repo}/tags`
return await axios.get(tagApi)
},
}
\ No newline at end of file
const ora = require('ora')
const chalk = require('chalk')
const fs = require('fs-extra');
const semver = require('semver')
const exec = require('child_process').exec;
const validateProjectName = require('validate-npm-package-name')
/**
* child_process执行shell
*/
function execute(cmd) {
exec(cmd, function (error, stdout, stderr) {
if (error) {
console.error(error);
}
else {
console.log("success");
}
});
}
/**
* sleep
* @param {Number} ms 毫秒
* @returns promise
*/
async function sleep(ms) {
return new Promise((resolve, rejecrt) => setTimeout(resolve(), ms))
}
/**
*
* @param {*} fn
* @param {*} message
* @param {...any} args
* @returns
*/
async function wrapLoading(fn, message, ...args) {
const spinner = ora(message)
spinner.start()
try {
const result = await fn(...args)
spinner.succeed()
return result
} catch (error) {
spinner.fail(`${error}, request failed, refetch...`)
await sleep(3000)
return wrapLoading(fn, message, ...args)
}
}
/**
*
* @param {String} wanted 指定的运行环境版本
* @param {String} id 包名
*/
function checkNodeVersion(wanted, id) {
if (!semver.satisfies(process.version, wanted, { includePrerelease: true })) {
console.log(chalk.red(
'You are using Node ' + process.version + ', but this version of ' + id +
' requires Node ' + wanted + '.\nPlease upgrade your Node version.'
))
process.exit(1)
}
}
/**
* 检查包名/项目名是否可用
* @param {String} name 包/项目名称
*/
function validatePkgName(name) {
const result = validateProjectName(name)
if (!result.validForNewPackages) {
console.error(chalk.red(`Invalid project name: "${name}"`))
result.errors && result.errors.forEach(err => {
console.error(chalk.red.dim('Error: ' + err))
})
result.warnings && result.warnings.forEach(warn => {
console.error(chalk.red.dim('Warning: ' + warn))
})
process.exit(1)
}
}
module.exports = {
sleep,
wrapLoading,
execute,
checkNodeVersion,
validatePkgName,
}
\ No newline at end of file
{
"name": "@lhfe/cli",
"version": "1.0.0",
"description": "",
"main": "index.js",
"bin": {
"lhfe": "./bin/cli"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^0.21.1",
"chalk": "^4.1.0",
"commander": "^7.1.0",
"consolidate": "^0.16.0",
"download-git-repo": "^3.0.2",
"ejs": "^3.1.6",
"fs-extra": "^9.1.0",
"handlebars": "^4.7.7",
"inquirer": "^8.0.0",
"leven": "^3.1.0",
"metalsmith": "^2.3.0",
"ncp": "^2.0.0",
"ora": "^5.4.0",
"semver": "^7.3.5",
"shelljs": "^0.8.4",
"validate-npm-package-name": "^3.0.0"
},
"engines": {
"node": "^12.0.0 || >= 14.0.0",
"npm": ">= 3.0.0"
}
}
项目模板目录
\ No newline at end of file
const fse = require('fs-extra')
const _ = require('lodash');
/**
* 修改package.json
* @param {string} source 需要修改的package.json地址
* @param {object} extra 需要添加的属性对象
* @param {string} target 修改后的pkg地址,如果不填则是在源文件上修改
*/
module.exports = async function (source, extra, target) {
let pkg = await fse.readJSON(source, {});
pkg = _.merge(pkg, extra);
await fse.outputJson(target || source, pkg, { spaces: 4 });
}
\ No newline at end of file
const descriptions = {
build: 'Compiles and minifies for production',
serve: 'Compiles and hot-reloads for development',
lint: 'Lints and fixes files',
'test:e2e': 'Run your end-to-end tests',
'test:unit': 'Run your unit tests'
}
function printScripts(pkg, packageManager) {
return Object.keys(pkg.scripts || {}).map(key => {
if (!descriptions[key]) return ''
return [
`\n### ${descriptions[key]}`,
'```',
`${packageManager} ${packageManager !== 'yarn' ? 'run ' : ''}${key}`,
'```',
''
].join('\n')
}).join('')
}
module.exports = function generateReadme(pkg, packageManager) {
return [
`# ${pkg.name}\n`,
'## Project setup',
'```',
`${packageManager} install`,
'```',
printScripts(pkg, packageManager),
'### Customize configuration',
'See [Configuration Reference](https://cli.vuejs.org/config/).',
''
].join('\n')
}
const fs = require('fs-extra')
const path = require('path')
function deleteRemovedFiles(directory, newFiles, previousFiles) {
// get all files that are not in the new filesystem and are still existing
const filesToDelete = Object.keys(previousFiles)
.filter(filename => !newFiles[filename])
// delete each of these files
return Promise.all(filesToDelete.map(filename => {
return fs.unlink(path.join(directory, filename))
}))
}
/**
*
* @param {string} dir
* @param {Record<string,string|Buffer>} files
* @param {Record<string,string|Buffer>} [previousFiles]
* @param {Set<string>} [include]
*/
module.exports = async function writeFileTree(dir, files, previousFiles, include) {
if (process.env.VUE_CLI_SKIP_WRITE) {
return
}
if (previousFiles) {
await deleteRemovedFiles(dir, files, previousFiles)
}
Object.keys(files).forEach((name) => {
if (include && !include.has(name)) return
const filePath = path.join(dir, name)
fs.ensureDirSync(path.dirname(filePath))
fs.writeFileSync(filePath, files[name])
})
}
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment