前言
玩过 Angular 的同学都晓得 Angular 作为一个 Framework,领有一套齐备的生态,还集成了弱小的 CLI。而 React 则仅仅是一个轻量级的 Library,官网社区只定义了一套组件的周期规定,而周边社区能够基于此规定实现本人的组件,React 并不会提供给你一套开箱即用的计划,而须要本人在第三方市场筛选称心的组件造成“全家桶”,这也是 React 社区沉闷的起因之一。
最近工作中在思考应用 monorepo 对我的项目进行治理,发现了一套 dev toolkit 叫做 Nx,Nx 应用 monorepo 的形式对我的项目进行治理,其外围开发者vsavkin 同时也是 Angular 我的项目的晚期核心成员之一,他把 Angular CLI 这套货色拿到 Nx,使其不仅能够反对 Angular 我的项目的开发,当初还反对 React 我的项目。
Nx 反对开发本人的plugin,一个 plugin 包含 schematics 和 builders(这两个概念也别离来自 Angular 的 schematics 以及 cli-builders),schematics 按字面意思了解就是“大纲”的意思,也就是能够基于一些模板自动化生成所需的文件;而 builders 就是能够自定义构建流程。
明天要讲的就是如何开发一个属于本人的 Nx plugin (蕴含 schematics),我会应用它来自动化创立一个页面组件,同时更新 router 配置,主动将其退出 react router 的 config。
对于 Monorepo
这篇文章不会具体介绍什么是monorepo,mono 有“单个”的意思,也就是单个仓库(所有我的项目放在一个仓库下治理),对应的就是polyrepo,也就是失常一个我的项目一个仓库。如下图所示:
更多对于 monorepo 的简介,能够浏览以下文章:
- Advantages of monorepos
- How to develop React apps like Facebook, Microsoft, and Google
- Misconceptions about Monorepos: Monorepo != Monolith
对于 Nx plugin
先贴一张脑图,一个一个解说 schematic 的相干概念:
后面提到 Nx plugin 包含了 builder(自动化构建)和 schematic(自动化我的项目代码的增删改查)。一个成型的 Nx plugin 能够应用 Nx 内置命令执行。
对于文章要介绍的 schematics,能够认为它是自动化代码生成脚本,甚至能够作为脚手架生成整个我的项目构造。
Schematics 要实现的指标
Schematics 的呈现优化了开发者的体验,晋升了效率,次要体现在以下几个方面:
- 同步式的开发体验,无需晓得外部的异步流程
Schematics 的开发“感觉”上是同步的,也就是说每个操作输出都是同步的,然而输入则可能是异步的,不过开发者能够不必关注这个,直到上一个操作的后果实现前,下一个操作都不会执行。
- 开发好的 schematics 具备高扩展性和高重用性
一个 schematic 由很多操作步骤组成,只有“步骤”划分正当,扩大只须要往里面新增步骤即可,或者删除原来的步骤。同时,一个残缺的 schematic 也能够看做是一个大步骤,作为另一个 schematic 的前置或后置步骤,例如要开发一个生成 Application 的 schematic,就能够复用原来的生成 Component 的 schematic,作为其步骤之一。
- schematic 是原子操作
传统的一些脚本,当其中一个步骤产生谬误,因为之前步骤的更改曾经利用到文件系统上,会造成许多“副作用”,须要咱们手动 FIX。然而 schematic 对于每项操作都是记录在运行内存中,当其中一项步骤确认无误后,也只会更新其外部创立的一个虚构文件系统,只有当所有步骤确认无误后,才会一次性更新文件系统,而当其中之一有误时,会撤销之前所做的所有更改,对文件系统不会有“副作用”。
接下来咱们理解下和 schematic 无关的概念。
Schematics 的相干概念
在理解相干概念前,先看看 Nx 生成的初始 plugin 目录:
your-plugin
|--.eslintrc
|--builders.json
|--collection.json
|--jest.config.js
|--package.json
|--tsconfig.json
|--tsconfig.lib.json
|--tsconfig.spec.json
|--README.md
|--src
|--builders
|--schematics
|--your-schema
|--your-schema.ts
|--your-schema.spec.ts
|--schema.json
|--schema.d.ts
Collection
Collection 蕴含了一组 Schematics,定义在 plugin 主目录下的collection.json
:
{
"$schema": "../../node_modules/@angular-devkit/schematics/collection-schema.json",
"name": "your-plugin",
"version": "0.0.1",
"schematics": {
"your-schema": {
"factory": "./src/schematics/your-schema/your-schema",
"schema": "./src/schematics/your-schema/schema.json",
"aliases": ["schema1"],
"description": "Create foo"
}
}
}
下面的 json 文件应用 @angular-devkit/schematics 下的 collection schema 来校验格局,其中最重要的是 schematics
字段,在这外面定义所有本人写的 schematics,比方这里定义了一个叫做 ”your-schema” 的 schematic,每个 schematic 下须要申明一个 rule factory(对于 rule
之后介绍),该 factory 指向一个文件中的默认导出函数,如果不应用默认导出,还能够应用 your-schema#foo
的格局指定以后文件中导出的 foo
函数。
aliases
申明了以后 schematic 的别名,除了应用 your-schema
的名字执行指令外,还能够应用 schema1
。description
示意一段可选的形容内容。
schema
定义了以后 schematic 的 schema json 定义,nx 执行该 schematic 指令时能够读取外面设置的默认选项,进行终端交互提醒等等,上面是一份schema.json
:
{
"$schema": "http://json-schema.org/schema",
"id": "your-schema",
"title": "Create foo",
"examples": [
{
"command": "g your-schema --project=my-app my-foo",
"description": "Generate foo in apps/my-app/src/my-foo"
}
],
"type": "object",
"properties": {
"project": {
"type": "string",
"description": "The name of the project.",
"alias": "p",
"$default": {"$source": "projectName"},
"x-prompt": "What is the name of the project for this foo?"
},
"name": {
"type": "string",
"description": "The name of the schema.",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "What name would you like to use for the schema?"
},"prop3": {
"type": "boolean",
"description": "prop3 description",
"default": true
}
},
"required": ["name", "project"]
}
properties
示意 schematic 指令执行时的选项,第一个选项 project
示意我的项目名,别名 p
, 应用$default
示意 Angular 内置的一些操作,例如 $source: projectName
则示意如果没有申明 project
,会应用 Angular workspaceSchema
(nx 中为workspace.json
)中的defaultProject
选项,而第二个选项的 $default
则表明应用命令时的第一个参数作为name
。
x-prompt
会在用户不键入选项值时的交互,用来提醒用户输出,用户能够不必事后晓得所有选项也能实现操作,更简单的 x-prompt
配置请查阅官网。
说了这么多,以下是几个直观交互的例子,帮忙大家了解:
nx 应用 generate
选项来调用 plugin 中的 schematic 或者 builder,和 Angular 的 ng generate
统一:
# 示意在 apps/app1/src/ 下生成一个名为 bar 的文件
$ nx g your-plugin:your-schema bar -p=app1
# 或者
$ nx g your-plugin:your-schema -name=bar -project app1
如果应用交互(不键入选项)
# 示意在 apps/app1/src/ 下生成一个名为 bar 的文件
$ nx g your-plugin:your-schema
? What is the name of the project for this foo?
$ app1
? What name would you like to use for the schema?
$ bar
接下来看看 Schematics 的两个外围概念:Tree和Rule
Tree
依据官网对 Tree
的介绍:
The virtual file system is represented by a Tree. The Tree data structure contains a base (a set of files that already exists) and a staging area (a list of changes to be applied to the base). When making modifications, you don’t actually change the base, but add those modifications to the staging area.
Tree
这一构造蕴含了两个局部:VFS 和 Staging area,VFS 是以后文件系统的一个虚构构造,Staging area 则寄存 schematics 中所做的更改。值得注意的是,当做出更改时,并不是对文件系统的及时更改,而只是将这些操作放在 Staging area,之后会把更改逐渐同步到 VFS,晓得确认无误后,才会一次性对文件系统做出变更。
Rule
A Rule object defines a function that takes a Tree, applies transformations, and returns a new Tree. The main file for a schematic, index.ts, defines a set of rules that implement the schematic’s logic.
Rule
是一个函数,接管 Tree
和Context
作为参数,返回一个新的 Tree
,在 schematics 的主文件index.ts
中,能够定义一系列的 Rule
,最初将这些Rule
作为一个综合的 Rule
在主函数中返回,就实现了一个 schematic。上面是 Tree
的残缺定义:
export declare type Rule = (tree: Tree, context: SchematicContext) => Tree | Observable<Tree> | Rule | Promise<void> | Promise<Rule> | void;
来看看一个简略的 schematic 主函数,咱们在函数中返回一个 Rule
,Rule
的操作是新建一个默认名为 hello
的文件,文件中蕴含一个字符串world
,最初将这个 Tree 返回。
// src/schematics/your-schema/index.ts
import {Rule, SchematicContext, Tree} from '@angular-devkit/schematics';
// You don't have to export the function as default. You can also have more than one rule factory
// per file.
export function myComponent(options: any): Rule {return (tree: Tree, _context: SchematicContext) => {tree.create(options.name || 'hello', 'world');
return tree;
};
}
Context
最初是 Context
,下面曾经提到过,对于 Schematics,是在一个名叫SchematicContext
的 Context 下执行,其中蕴含了一些默认的工具,例如context.logger
,咱们能够应用其打印一些终端信息。
如何开发一个 Nx Schematic?
上面的所有代码均能够在我的 GitHub 里下载查看,感觉不错的话,欢送大家 star。
接下来进入正题,咱们开发一个 nx plugin schematic,应用它来创立咱们的页面组件,同时更新路由配置。
假如咱们的我的项目目录构造如下:
apps
|...
|--my-blog
|...
|--src
|--components
|--pages
|--home
|--index.ts
|--index.scss
|--about
|--routers
|--config.ts
|--index.ts
|...
router/config.ts
文件内容如下:
export const routers = {
// 首页
'/': 'home',
// 个人主页
'/about': 'about'
};
当初咱们要新增一个博客页,不少同学可能就间接新建一个目录,复制首页代码,最初手动增加一条路由配置,对于这个例子倒是还好,然而如果须要更改的中央很多,就很浪费时间了,学习了 Nx plugin schematics,这所有都能够用 Schematic 实现。
搭建 Nx 环境并应用 Nx 默认的 Schematic 创立一个 plugin
如果之前曾经有了 Nx 我的项目,则间接在我的项目根目录下应用以下命令创立一个 plugin:
$ nx g @nrwl/nx-plugin:plugin [pluginName]
如果是刚应用 Nx,也能够应用上面的命令疾速新建一个我的项目,并主动增加一个 plugin:
$ npx create-nx-plugin my-org --pluginName my-plugin
设置好 Schematic 选项定义
当初 Nx 为咱们创立了一个默认的 plugin,首先更改packages/plugin/collection.json
,为 schema 取名叫做“page”
{
"$schema": "../../node_modules/@angular-devkit/schematics/collection-schema.json",
"name": "plugin",
"version": "0.0.1",
"schematics": {
"page": {
"factory": "./src/schematics/page/page",
"schema": "./src/schematics/page/schema.json",
"description": "Create page component"
}
}
}
接下来定义咱们提供的 schema option,这里须要批改 src/schematics/page/schema.json
和src/schematics/page/schema.d.ts
,前者作为 JSON Schema 被 Nx plugin 应用,后者作为类型定义,开发时用到。
对于 page,咱们须要提供两个必须选项:name 和对应的 project,两个可选选项:connect(是否 connect to redux)、classComponent(应用类组件还是函数组件)。
上面别离是 schema.json
和schema.d.ts
:
{
"$schema": "http://json-schema.org/draft-07/schema",
"id": "page",
"title": "Create page component",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the page component",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "What name would you like to use?"
},
"project": {
"type": "string",
"description": "The project of the page component",
"$default": {"$source": "projectName"},
"alias": "p",
"x-prompt": "Which projcet would you like to add to?"
},
"classComponent": {
"type": "boolean",
"alias": "C",
"description": "Use class components instead of functional component.",
"default": false
},
"connect": {
"type": "boolean",
"alias": "c",
"description": "Create a connected redux component",
"default": false
}
},
"required": ["name", "project"]
}
export interface PageSchematicSchema {
name: string;
project: string;
classComponent: boolean;
connected: boolean;
}
开发 Schematic
创立所需模板文件
模板文件就是通过一些模板变量来生成真正的文件。每一个页面默认有两个文件,index.ts
和index.scss
,因而创立模板文件如下:
index.ts.template
<% if (classComponent) { %>
import React, {Component} from 'react';
<% } else { %>
import React from 'react';
<% } %>
<% if (connect) { %>
import {connect} from 'react-redux';
import {IRootState, Dispatch} from '../../store';
<% } %>
import {RouteComponentProps} from 'react-router-dom';
import './index.scss';
<% if (connect) { %>
type StateProps = ReturnType<typeof mapState>;
type DispatchProps = ReturnType<typeof mapDispatch>;
type Props = StateProps & DispatchProps & RouteComponentProps;
<% } else { %>
type Props = RouteComponentProps;
<% } %>
<% if (classComponent) { %>
class <%= componentName %> extends Component<Props> {render() {
return (
<div className="<% className %>">
Welcome to <%= componentName %>!
</div>
);
}
}
<% } else { %>
const <%= componentName %> = (props: Props) => {
return (
<div className="<%= className %>">
Welcome to <%= componentName %>!
</div>
);
};
<% } %>
<% if (connect) { %>
function mapState(state: IRootState) {return {}
}
function mapDispatch(dispatch: Dispatch) {return {}
}
<% } %>
<% if (connect) { %>
export default connect<StateProps, DispatchProps, {}>(mapState, mapDispatch)(<%= componentName %>);
<% } else { %>
export default <%= componentName %>;
<% } %>
index.scss.template
.<%= className %> {}
咱们将模板文件放到 src/schematics/page/files/
下。
基于模板文件创建所需文件和目录
咱们一共须要做四件事:
- 格式化选项(把 schematic 默认的选项进行加工,加工成咱们所需的全副选项)。
- 基于模板文件创建所需文件和目录。
- 更新
app/src/routers/config.ts
。 - 应用 eslint 格式化排版。
先来实现 1 和 2:
page.ts
:
import {PageSchematicSchema} from './schema';
import {names} from '@nrwl/workspace';
import {getProjectConfig} from '@nrwl/workspace/src/utils/ast-utils';
interface NormalizedSchema extends PageSchematicSchema {
/** element className */
className: string;
componentName: string;
fileName: string;
projectSourceRoot: Path;
}
/** 加工选项 */
function normalizeOptions(
host: Tree,
options: PageSchematicSchema
): NormalizedSchema {const { name, project} = options;
const {sourceRoot: projectSourceRoot} = getProjectConfig(host, project);
// kebab-case fileName and UpperCamelCase className
const {fileName, className} = names(name);
return {
...options,
// element className
className: `${project}-${fileName}`,
projectSourceRoot,
componentName: className,
fileName,
};
}
接下来应用模板文件:
page.ts
import {join} from '@angular-devkit/core';
import {
Rule,
SchematicContext,
mergeWith,
apply,
url,
move,
applyTemplates,
} from '@angular-devkit/schematics';
import {PageSchematicSchema} from './schema';
import {names} from '@nrwl/workspace';
import {getProjectConfig} from '@nrwl/workspace/src/utils/ast-utils';
interface NormalizedSchema extends PageSchematicSchema {
/** element className */
className: string;
componentName: string;
fileName: string;
projectSourceRoot: Path;
}
/** 基于模板创立文件 */
function createFiles(options: NormalizedSchema): Rule {const { projectSourceRoot, fileName} = options;
const targetDir = join(projectSourceRoot, pagePath, fileName);
return mergeWith(apply(url('./files'), [applyTemplates(options), move(targetDir)])
);
}
原理是应用 Angular Schematics 自带的 mergeWith
的Rule
,接管一个 Source
,Source
的定义如下:
A source is a function that generates a Tree from a specific context.
也就是说 Source()
会生成一棵新的 Tree
。而后将其和原来的Tree
合并。
因为咱们须要从模板文件中加载,首先须要应用 url
加载文件,url
接管文件或文件夹的绝对地址,返回一个 Source
,而后咱们应用apply
对url
加载模板文件后的 Source
进行加工,apply
接管一个 Source
和一个 Rule
的数组,将 Rule[]
利用后返回一个新的Source
。
这里咱们须要进行两种“加工”,首先应用 options
替换模板文件中的变量,最初将这些文件应用 move
挪动到对应的目录下即可。
更新 router config
来到了最重要也是比拟难的一个步骤,咱们还须要批改 src/routers/config.ts
中的 routers
变量,在外面减少咱们刚加上的 page component。
因为这里是 TS 文件,所以须要剖析 TS 的 AST (Abstract Syntax Tree),而后批改 AST,最初应用批改的 AST 对原来内容进行笼罩即可。
批改 AST 能够应用 TS 官网的 Compiler API 联合 TypeScript AST Viewer 进行。不过因为 AST 的简单构造,TS Compiler API 也不太敌对,间接应用 API 对 AST 进行操作十分艰难。例如 AST 的每个节点都有 position 信息,做一个新的插入时,还须要对 position 进行计算,API 并没有人性化的操作形式。
因为下面的起因,我最终抉择了 ts-morph,ts-morph 以前也叫做 ts-simple-ast,它封装了 TS Compiler API,让操作 AST 变得简略易懂。
看代码之前,咱们先应用 TS AST Viewer 剖析一下 routers/config.ts
这段代码的 AST:
export const routers = {
// 首页
'/': 'home',
// 第二页
'/about': 'about'
};
AST 如下(只含根节点信息):
咱们来层层剖析:
- 从申明到赋值,整段语句作为
Variable Statement
。 - 因为
routers
是被导出的,蕴含了ExportKeyword
。 - 从
routers = xxx
作为VariableDeclarationList
中的惟一一个VariableDeclaration
。 - 最初是
Identifier
“routers”,再到字面量表达式作为它的 value。 - …
因为上面代码用到了 Initializer
,上述的对象字面量表达式ObjectLiteralExpression
就是 routers
这个 VariableDeclaration
的Initializer
:
看懂 AST 后,更新 router 后的代码就容易了解了:
import {join, Path} from '@angular-devkit/core';
import {
Rule,
Tree,
chain,
SchematicContext,
mergeWith,
apply,
url,
move,
applyTemplates,
} from '@angular-devkit/schematics';
import {PageSchematicSchema} from './schema';
import {formatFiles, names} from '@nrwl/workspace';
import {getProjectConfig} from '@nrwl/workspace/src/utils/ast-utils';
import {Project} from 'ts-morph';
/** 更新路由配置 */
function updateRouterConfig(options: NormalizedSchema): Rule {return (host: Tree, context: SchematicContext) => {const { projectSourceRoot, fileName} = options;
const filePath = join(projectSourceRoot, routerConfigPath);
const srcContent = host.read(filePath).toString('utf-8');
// 应用 ts-morph 的 project 对 AST 进行操作
const project = new Project();
const srcFile = project.createSourceFile(filePath, srcContent, {overwrite: true,});
try {
// 依据变量标识符拿到对应的 VariableDeclaration
const decl = srcFile.getVariableDeclarationOrThrow(routerConfigVariableName);
// 获取 initializer 并转换成 string
const initializer = decl.getInitializer().getText();
// 应用正则匹配对象字面量的最初一部分并做插入
const newInitializer = initializer.replace(/,?\s*}$/,
`,'/${fileName}': '${fileName}' }`
);
// 更新 initializer
decl.setInitializer(newInitializer);
// 获取最新的 TS 文件内容对源文件进行笼罩
host.overwrite(filePath, srcFile.getFullText());
} catch (e) {context.logger.error(e.message);
}
};
}
在如何对 Initializer
进行操作时,我最开始想到的是将其应用 JSON.parse()
转换成对象字面量,而后进行简略追加,前面发现这段内容里还可能蕴含正文,所以只能通过正则匹配确定字面量的“尾部局部”,而后进行匹配追加。
应用 eslint 做好排版
操作实现后咱们能够应用 Nx workspace 提供的 formatFiles
将所有文件排版有序。最初咱们只须要在默认导出函数里将上述 Rule
通过 chain
这个 Rule
进行汇总。来看看最终代码:
import {join, Path} from '@angular-devkit/core';
import {
Rule,
Tree,
chain,
SchematicContext,
mergeWith,
apply,
url,
move,
applyTemplates,
} from '@angular-devkit/schematics';
import {PageSchematicSchema} from './schema';
import {formatFiles, names} from '@nrwl/workspace';
import {getProjectConfig} from '@nrwl/workspace/src/utils/ast-utils';
import {Project} from 'ts-morph';
interface NormalizedSchema extends PageSchematicSchema {
/** element className */
className: string;
componentName: string;
fileName: string;
projectSourceRoot: Path;
}
// 页面组件目录
const pagePath = 'pages';
// 路由配置目录
const routerConfigPath = 'routers/config.ts';
// 路由配置文件中须要批改的变量名
const routerConfigVariableName = 'routers';
/** 加工选项 */
function normalizeOptions(
host: Tree,
options: PageSchematicSchema
): NormalizedSchema {const { name, project} = options;
const {sourceRoot: projectSourceRoot} = getProjectConfig(host, project);
// kebab-case fileName and UpperCamelCase className
const {fileName, className} = names(name);
return {
...options,
// element className
className: `${project}-${fileName}`,
projectSourceRoot,
componentName: className,
fileName,
};
}
/** 基于模板创立文件 */
function createFiles(options: NormalizedSchema): Rule {const { projectSourceRoot, fileName} = options;
const targetDir = join(projectSourceRoot, pagePath, fileName);
return mergeWith(apply(url('./files'), [applyTemplates(options), move(targetDir)])
);
}
/** 更新路由配置 */
function updateRouterConfig(options: NormalizedSchema): Rule {return (host: Tree, context: SchematicContext) => {const { projectSourceRoot, fileName} = options;
const filePath = join(projectSourceRoot, routerConfigPath);
const srcContent = host.read(filePath).toString('utf-8');
const project = new Project();
const srcFile = project.createSourceFile(filePath, srcContent, {overwrite: true,});
try {
const decl = srcFile.getVariableDeclarationOrThrow(routerConfigVariableName);
const initializer = decl.getInitializer().getText();
const newInitializer = initializer.replace(/,?\s*}$/,
`,'/${fileName}': '${fileName}' }`
);
decl.setInitializer(newInitializer);
host.overwrite(filePath, srcFile.getFullText());
} catch (e) {context.logger.error(e.message);
}
};
}
// 默认的 rule factory
export default function (schema: PageSchematicSchema): Rule {return function (host: Tree, context: SchematicContext) {const options = normalizeOptions(host, schema);
return chain([createFiles(options),
updateRouterConfig(options),
formatFiles({skipFormat: false}),
]);
};
}
测试
写好了 schematic,别忘了进行测试,测试代码如下:
page.spec.ts
import {Tree, Rule} from '@angular-devkit/schematics';
import {SchematicTestRunner} from '@angular-devkit/schematics/testing';
import {createEmptyWorkspace} from '@nrwl/workspace/testing';
import {join} from 'path';
import {PageSchematicSchema} from './schema';
import {updateWorkspace, names} from '@nrwl/workspace';
const testRunner = new SchematicTestRunner(
'@plugindemo/plugin',
join(__dirname, '../../../collection.json')
);
export function callRule(rule: Rule, tree: Tree) {return testRunner.callRule(rule, tree).toPromise();}
export async function createFakeApp(tree: Tree, appName: string): Promise<Tree> {const { fileName} = names(appName);
const appTree = await callRule(updateWorkspace((workspace) => {
workspace.projects.add({
name: fileName,
root: `apps/${fileName}`,
projectType: 'application',
sourceRoot: `apps/${fileName}/src`,
targets: {},});
}),
tree
);
appTree.create(
'apps/app1/src/routers/config.ts',
`
export const routers = {
// 首页
'/': 'home',
// 个人主页
'/about': 'about'
};
`
);
return Promise.resolve(appTree);
}
describe('plugin schematic', () => {
let appTree: Tree;
const options: PageSchematicSchema = {name: 'myPage', project: 'app1'};
beforeEach(async () => {appTree = createEmptyWorkspace(Tree.empty());
appTree = await createFakeApp(appTree, 'app1');
});
it('should run successfully', async () => {const tree = await testRunner.runSchematicAsync('page', options, appTree).toPromise();
// file exist
expect(tree.exists('apps/app1/src/pages/my-page/index.tsx')
).toBeTruthy();
expect(tree.exists('apps/app1/src/pages/my-page/index.scss')
).toBeTruthy();
// router modified correctly
const configContent = tree.readContent('apps/app1/src/routers/config.ts');
expect(configContent).toMatch(/,\s*'\/my-page': 'my-page'/);
});
});
测试这块能用的轮子也比拟多,我这里简略创立了一个假的 App(合乎下面说的目录构造),而后进行了一下简略测试。测试能够应用如下指令对 plugin 中的单个 schematic 进行测试:
$ nx test plugin --testFile page.spec.ts
如果写的 plugin 比较复杂,倡议再进行一遍 end2end 测试,Nx 对 e2e 的反对也很好。
公布
最初到了公布环节,应用 Nx build 之后便能够自行公布了。
$ nx build plugin
上述所有代码均能够在我的 GitHub 里下载查看,同时代码里还减少了一个实在开发环境下的 App-demo,外面将 plugin 引入了失常的开发流程,更能感触到其带来的便捷性。感觉不错的话,欢送大家 star。
总结
其实要写好这类对文件系统“增删改查”的工具,要害还是要了解文件内容,比方下面的难点就在于了解 TS 文件的 AST。应用 ts-morph 还能够做很多事件,比方咱们每减少一个文件,可能须要在进口 index.ts
中导出一次,应用 ts-morph 就一句话的事件:
const exportDeclaration = sourceFile.addExportDeclaration({namedExports: ["MyClass"],
moduleSpecifier: "./file",
});
当然,Nx 和 Angular 提供了这一套生态,能用的工具和办法十分多,然而也须要咱们急躁查阅,正当应用。目前来说 Nx 封装的办法没有具体的文档,可能用起来须要间接查阅 d.ts
文件,没那么不便。
工欲善其事,必先利其器。Happy Coding!