[RFC] egg-init refactor
- Dominant language
- TypeScript
- Stars
- 19k
- Forks
- 1.8k
- PR merge metrics
- No merged PRs in 30d
Description
# 背景
目前的 egg-init 存在以下问题:
* 脚手架逻辑集中化,全部在 `egg-init` 本身,作为全局命令,更新不方便。
* 模板无法定制自己的逻辑,无法代码共享。
* 没有 sub generator,如 `egg-init add controller Test` 这样的功能。
* 脚手架只在项目初始化时用到,无法支撑升级功能,容易腐化和分裂。
* 上层封装不方便,不支持 preset 。
# 方案
## 基础骨架
脱离 Egg 的独立骨架模块,[common-boilerplate](https://github.com/node-modules/common-boilerplate/pull/1):
* 支持多级框架继承
* 提供 TestUtils 测试辅助
* 支持注册子命令
* 骨架可以单独运行,不依赖 `egg-bin` 等引导工具
* 提供生成骨架的骨架:[boilerplate-boilerplate](https://github.com/node-modules/boilerplate-boilerplate)
__目录结构:__
```bash
./boilerplate-example
├── boilerplate
│ ├── lib
│ ├── test
│ ├── README.md
│ ├── _.eslintrc
│ ├── _.gitignore
│ ├── _package.json
│ └── index.js
├── test
│ └── index.test.js
├── index.js
├── README.md
└── package.json
```
__骨架入口:__
```javascript
// index.js
const Boilerplate = require('common-boilerplate');
class MainBoilerplate extends Boilerplate {
// 类似 egg 的方式来提供骨架路径,方便继承
get [Symbol.for('boilerplate#root')]() {
return __dirname;
}
// 交互式问答,基于 Inquirer,并对其进行扩展,方便测试
initQuestions() {
const questions = super.initQuestions();
questions.push(
{
type: 'list',
name: 'type',
message: 'choose your type:',
choices: [ 'simple', 'plugin', 'framework' ],
}
);
return questions
}
};
module.exports = MainBoilerplate;
module.exports.testUtils = Boilerplate.testUtils;
```
### __模板渲染__
* 内置支持简单的渲染, `{{ name }}`
* 文件名也支持 `{{name}}.test.js` 形式
* 通过覆盖 `renderTemplate` 方法可以支持 nunjucks 之类的渲染引擎
```javascript
const nunjucks = require('nunjucks');
// could disable auto escape
nunjucks.configure({ autoescape: false });
class MainBoilerplate extends Boilerplate {
async renderTemplate(tpl, locals) {
return nunjucks.renderString(tpl, locals);
}
// custom your locals
async initLocals() {
const locals = await super.initLocals();
locals.foo = 'bar';
return locals;
}
};
```
### 模板继承
```javascript
// share.js
class ShareBoilerplate extends Boilerplate {
// must provide your directory
get [Symbol.for('boilerplate#root')]() {
return __dirname;
}
};
// child.js
class MainBoilerplate extends ShareBoilerplate {
// must provide your directory
get [Symbol.for('boilerplate#root')]() {
return __dirname;
}
// example for ignore some files from parent
async listFiles(...args) {
const files = await super.listFiles(...args);
files['github.png'] = undefined;
return files;
}
};
```
### 单元测试
扩展了 [coffee](https://github.com/node-modules/coffee),提供 CLI 的测试支持。
```js
const testUtils = require('common-boilerplate').testUtils;
describe('test/index.test.js', () => {
it('should work', () => {
return testUtils.run()
// .debug()
.waitForPrompt()
// answer to the questions
.write('example\n')
// emit `DOWN` key to select the second choise
.choose(2)
// expect README.md to be exists
.expectFile('README.md')
// check with `includes`
.expectFile('README.md', 'this is a desc')
// check with regex
.expectFile('README.md', /desc/)
// check whether contains
.expectFile('package.json', { name: 'example' })
// opposite assertion
.notExpectFile('not-exist')
.notExpectFile('README.md', 'sth')
// see others at `coffee` docs
.expect('stdout', /some console message/)
.expect('stderr', /some error message/)
.expect('code', 0)
// don't forgot to call `end()`
.end();
});
});
```
## egg-init
`egg-init` 极简化:
* 引导工具
* 全局安装,功能尽量简单,无需升级。
* 继承 `common-bin` 。
* 判断指定目录
* 空目录:根据全局配置,提示用户可用的 boilerplate 列表(preset),安装并执行对应的骨架。
* 非空目录:读取 `package.json` 的 `boilerplate` 节点,执行对应的骨架。
* 支持 `preset` ,可以不再需要封装 `@ali/egg-init` 。
```bash
$ egg-init --npm=tnpm
$ egg-init --registry='https://registry.npmjs.org'
$ egg-init --preset='egg-init-config' --type=simple
$ egg-init --package=egg-boilerplate-simple
$ egg-init --template=/path/to/boulerplate
$ egg-init add controller Test
```
### 配置文件
* 启动时将读取配置文件,作为 argv 的默认值。
* 读取顺序:`package.json` 的 `boilerplate` -> `~/.egg-init`
* 也可以命令行传递: `egg-init --config=/path/to`
**项目配置:** package.json
```json
{
"name": "egg-showcase",
"boilerplate": {
"name": "egg-boilerplate-simple",
"version": "2.0.0",
"npm": "tnpm",
"registry": "https://registry.npmjs.org"
}
}
```
__全局配置:__ 支持 `yml / json` 等格式
```yaml
# ~/.egg-init
npm: 'tnpm'
registry: 'https://registry.npmjs.org'
proxy: '127.0.1.1:8888'
preset:
- @ali/egg-init-config
- egg-init-config
```
### egg-init-config
骨架列表集合,用于 `--preset` 参数。
仅需在 `package.json` 中包含 `config.boilerplate` 字段即可。
* `package` - npm 包名
* `description` - 描述
* `category` - 分类,可选
```javascript
{
"name": "egg-init-config",
"version": "1.3.0",
"description": "egg init boilerplate config",
"config": {
"boilerplate": {
"simple": {
"package": "egg-boilerplate-simple",
"description": "Simple egg app boilerplate"
},
"ts": {
"package": "egg-boilerplate-ts",
"description": "Simple egg && typescript app boilerplate",
"category": "typescript"
}
}
}
```
### __伪代码__
```javascript
// egg-init
const { Command } = require('common-bin');
class EggInitCommand extends Command {
* run({ argv, cwd }) {
// 读取配置文件
argv = this.normalize(argv);
const dir = argv.dir;
let boilerplateName = argv.type;
let action;
// 如果目标目录不存在,则视为初始化行为
if (!fs.existSync(dir)) {
// 安装 boilerplate
this.npmInstall(boilerplateName, dir);
action = 'init';
} else {
// 从 pkg 读取当前应用使用的骨架
boilerplateName = this.getPkgInfo(dir, 'boilerplate.name');
// egg-init add
action = argv._[0];
}
// 执行 boilerplate
const boilerplate = require(path.join(dir, 'node_modules', boilerplateName));
yield boilerplate.run({ action, argv, cwd } );
}
}
module.exports = EggInitCommand;
```
### egg-boilerplate-base
提供一个 `egg-boilerplate-base` 基础骨架,方便开发者继承使用。
* 默认注册常用的命令,开发者可以覆盖
* `add controller`
* `add service`
* `add config`
* `add plugin`
* 提供模板渲染 (nunjucks)
* 提供 `helper` (或者考虑仅推荐,不集成,开发者自行引入)
* 提供 egg-ast-utils 辅助代码修改和升级
* 提供 mrm-core 相关功能
### 子命令
还没想好怎么做。
是基于 `common-bin` 的 sub command 还是作为 boilerplate 的一个方法,如 `addXX()` ?
还有就是跟 `egg-bin generator` 有点相关,很多子命令其实更应该由插件来提供,如 `addModel` 之类的,它的模板应该是在对应的插件里面。
所以 `egg-init` 和 `egg-bin generator` 是可以考虑联动的,譬如 `addModel` 的时候,是固定读插件里面的某个约定的文件,或者执行某个脚本。
### egg-boilerplate-legacy
用于兼容旧版本的骨架,引导安装,实现旧版 egg-init 的安装逻辑。
在 `egg-init` 新版源码里面,判断用户选择的骨架是否符合新规范,不符合的话,安装 `egg-boilerplate-legacy` 并引导安装。
Contributor guide
Assessment
This issue has not been assessed yet.