eggjs / eggjs/egg

[RFC] egg-init refactor

Open
#2,892 17 comments 3 reactions 3 assignees Claimed by @dead-horse View on GitHub
type: proposals
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.