eggjs / egg

🥚 Born to build better enterprise frameworks and apps with Node.js & Koa
https://eggjs.org
MIT License
18.9k stars 1.82k forks source link

[RFC] egg-init refactor #2892

Open atian25 opened 6 years ago

atian25 commented 6 years ago

背景

目前的 egg-init 存在以下问题:

方案

基础骨架

脱离 Egg 的独立骨架模块,common-boilerplate

目录结构:

./boilerplate-example
├── boilerplate
│   ├── lib
│   ├── test
│   ├── README.md
│   ├── _.eslintrc
│   ├── _.gitignore
│   ├── _package.json
│   └── index.js
├── test
│   └── index.test.js
├── index.js
├── README.md
└── package.json

骨架入口:

// 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;

模板渲染

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;
  }
};

模板继承

// 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,提供 CLI 的测试支持。

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 极简化:

$ 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

配置文件

项目配置: package.json

{
  "name": "egg-showcase",
  "boilerplate": {
    "name": "egg-boilerplate-simple",
    "version": "2.0.0",
    "npm": "tnpm",
    "registry": "https://registry.npmjs.org"
  }
}

全局配置: 支持 yml / json 等格式

# ~/.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 字段即可。

{
  "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"
      }
   }
}

伪代码

// 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 <type> <name>
      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 基础骨架,方便开发者继承使用。

子命令

还没想好怎么做。

是基于 common-bin 的 sub command 还是作为 boilerplate 的一个方法,如 addXX() ?

还有就是跟 egg-bin generator 有点相关,很多子命令其实更应该由插件来提供,如 addModel 之类的,它的模板应该是在对应的插件里面。

所以 egg-initegg-bin generator 是可以考虑联动的,譬如 addModel 的时候,是固定读插件里面的某个约定的文件,或者执行某个脚本。

egg-boilerplate-legacy

用于兼容旧版本的骨架,引导安装,实现旧版 egg-init 的安装逻辑。

egg-init 新版源码里面,判断用户选择的骨架是否符合新规范,不符合的话,安装 egg-boilerplate-legacy 并引导安装。

atian25 commented 6 years ago
fengmk2 commented 6 years ago

好赞!

Runrioter commented 6 years ago

Egg当前确实需要这个

popomore commented 6 years ago

Add nyc to package.json by default

  "nyc": {
    "check-coverage": true,
    "statements": 76,
    "branches": 44,
    "functions": 71,
    "lines": 76,
    "exclude": [
      "app/dal/dao/base"
    ]
  },
popomore commented 6 years ago
  1. 全局命令建议用 egg
  2. 配置使用 ./egg/config,方便之后扩张目录
  3. 子命令和 generator 是两个维度的,generator 就是其中一个子命令
  4. 子命令放脚手架不是很好,不容易更新,还是跟着项目走,放 egg-bin 里,可以做个命令映射。
popomore commented 6 years ago

应用代码升级的问题好像没说

atian25 commented 6 years ago
popomore commented 6 years ago

egg-bin 跟着项目走比较好,插件是全局的?

atian25 commented 6 years ago

插件指的是 egg 插件,就像 generator 那样,可以在 app/scripts 目录里面支持 generator.js / boilerplate.js 的方式来执行子命令。

譬如 egg-mongooes 的 boilerplate 放在插件里面最合适

atian25 commented 6 years ago

@popomore

配置使用 ./egg/config,方便之后扩张目录

指的是 ~/.egg/config/init.yml ?

插件指的是 egg 插件,就像 generator 那样,可以在 app/scripts 目录里面支持 generator.js / boilerplate.js 的方式来执行子命令。譬如 egg-mongooes 的 boilerplate 放在插件里面最合适

这个有什么问题不?

popomore commented 6 years ago

~/.egg/config.yml 就好了

popomore commented 6 years ago

我觉得生成器作为单独命令比较好。

首先脚手架模版肯定是独立,放插件里有变成了鸡蛋问题。工具类不一定都适合放插件里,比如我们原来默认的 dev/test 属于哪个插件?放插件的可能是代码生成器和更新工具,这两个东西虽然场景不同,实属于同一个东西。

atian25 commented 6 years ago

你理解错我的意思了,egg-bin 拆分那个只是考虑如何扩展,可能是 egg 这个全局引导命令,做一个映射,跟之前没区别,不会放到插件里面,这个先不在这展开,回头再另开。

我指的是:

这几个放到插件里面合适,因为他们是跟插件强相关的。而放到骨架里面就不太合适了,如 simple 这个骨架,不能把 hsf 的 template/ sub generator(egg-init add hsf --name=User) 放进去

atian25 commented 6 years ago

回到 RFC 本身,现在有两个问题我还没想清晰,需要讨论下:

popomore commented 6 years ago

明白了,那没问题。

通用的生成可以放到框架?就是考虑在哪个层面的就放到哪个 loadUnit?但是这个就不要考虑继承和依赖了,太复杂,可以通过配置的方式扩展。

更新我觉得跟生成是一个思路,只是需要根据一个标准来更新,比如什么版本到什么版本会做什么更新,现在想想有点复杂。

atian25 commented 6 years ago

大概是这样的继承关系, base 里面实现 controller/service 等通用的

ts 里面实现 ts 版通用的,(到时再看有没有必要来个 base-ts )

框架的,目前还不是很清晰,倾向于先放到独立的 boilerplate,未来有需要再看看是抽象还是集成到框架。

更新的感觉挺复杂的,还想不清晰,先搁置。 egg-boilerplate-legacy 倒是可以实践下,一方面支持安装旧模板,一方面模板开发者可以用它来升级

atian25 commented 6 years ago

补充下,有 npm init xxx 这个机制,可以注册 create-egg / create-egg-simple 这样的 npm 包,然后开发者就可以直接 npm init egg-simple 的方式调用执行了,可以作为一个简化的方式。

image

https://github.com/eggjs/create-egg