はじめに
AngularはAngular CLIのng generate
を用いることで、サービスやコンポーネントのコードを生成することができますが、そのコード生成の仕組みであるschematicsが私たち開発者にも使える形で公開されています。
schematicsの開発は公式ドキュメントにも書いてあるとおり、@angular-devkit/schematics-cli
でリポジトリを生成してから開発できますが、開発環境をより自分好みのものに変更することもできます。
この記事では、私が実際にschematicsを開発する上でやった開発環境のカスタマイズや、ng generateから実行することを想定して公開する場合に考慮するといいと思うことを紹介します。
- ビルド生成物を出力するディレクトリを変える
- Yarn, Jest, ESLint, Prettierの導入
- TypeScriptのバージョンアップ
- 各オプションへの対応方法
- --flat
- --skip-tests
- パス付きの--name
- --lint-fix
- --project
まだschematicsを作ったことがないという人は、@puku0x さんのチュートリアル記事がわかりやすかったので参考にしてみてください。
環境を整える
下記のようにschematics-cliのコマンドを使ってリポジトリを生成するとschematicsの開発に必要なものは全て揃っていますが、開発に普段遣いのツールを使いたい場合もあると思います。
$ schematics blank my-schematics
$ tree -a my-schematics -I node_modules
my-schematics
├── .gitignore
├── .npmignore
├── README.md
├── package-lock.json
├── package.json
├── src
│ ├── collection.json
│ └── my-schematics
│ ├── index.ts
│ └── index_spec.ts
└── tsconfig.json
{
"name": "my-schematics",
"version": "0.0.0",
"description": "A blank schematics",
"scripts": {
"build": "tsc -p tsconfig.json",
"test": "npm run build && jasmine src/**/*_spec.js"
},
"keywords": [
"schematics"
],
"author": "",
"license": "MIT",
"schematics": "./src/collection.json",
"dependencies": {
"@angular-devkit/core": "^8.3.21",
"@angular-devkit/schematics": "^8.3.21",
"@types/jasmine": "^3.3.9",
"@types/node": "^8.0.31",
"jasmine": "^3.3.1",
"typescript": "~3.5.3"
}
}
ビルド結果を別のディレクトリに吐くようにする
初期生成のtsconfig.jsonだとそのまま.tsファイルのあるディレクトリにビルド結果のファイルが生成されます。index.tsに対してはindex.js, index.js.map, index.d.tsが生成されます。
このままではディレクトリ内のファイルが増えてくると目的のファイルを探し辛くなってくるので、別ディレクトリに吐き出すようにしたいです。
まずはtsconfig.jsonにoutDirを指定しましょう。注意点としては、rootDirがないとディレクトリ構造が保持されないのでrootDirは残しておいてください。
"noImplicitThis": true,
"noUnusedParameters": true,
"noUnusedLocals": true,
"rootDir": "src",
+ "outDir": "dist",
"skipDefaultLibCheck": true,
"skipLibCheck": true,
"sourceMap": true,
publishするディレクトリを指定するためにpackage.jsonに追記します。
"scripts": {
- "build": "tsc -p tsconfig.json",
+ "build": "tsc -p tsconfig.json && scripts/post-build.sh",
...
"author": "",
"license": "MIT",
- "schematics": "./src/collection.json",
+ "schematics": "./dist/collection.json",
+ "files": [
+ "dist"
+ ],
"dependencies": {
src下のcollection.jsonはsrcからの相対パスでschematisのファイルの場所を指定しているので、distディレクトリに移さなければならないです。
ビルドしてもcollection.jsonやschema.jsonはdistディレクトリに移らないので、ここではビルド後に実行される簡単なシェルスクリプトを実行するようにします。
#!/bin/bash
if [[ -f src/collection.json ]]; then
cp src/collection.json dist/
fi
find src -name files | egrep 'files$' | while read src; do
dist=$(echo $src | sed 's/src/dist/' | sed 's|/files$||')
cp -a $src $dist
done
find src -name schema.json | while read src; do
dist=$(echo $src | sed 's/src/dist/' | sed 's|/schema.json||')
cp $src $dist
done
json以外にtsconfig.jsonでビルドから除外しているテンプレートファイルも移すようにしています。
シンボリックリンクではなくファイルをコピーしているのはnpm publishするときにシンボリックリンクがパッケージされないためです。
distに吐き出すようにしたので.npmignoreはいらなくまりました。消しても大丈夫です。
.gitignoreも書き換えましょう。
# Outputs
- src/**/*.js
- src/**/*.js.map
- src/**/*.d.ts
+ dist/
Jasmineの代わりにJestを使う
schematicsの開発はJasmineのままでも問題なく開発できると思います。
ただ、普段の開発でJasmineよりもJestの方が使い慣れているのであれば代えてしまってもいいと思います。
まずは必要なパッケージをインストールします。
npm i -D jest ts-jest @types/jest
"scripts": {
"build": "tsc -p tsconfig.json",
- "test": "npm run build && jasmine src/**/*_spec.js"
+ "test": "jest",
+ "test:watch": "jest --watch"
},
"types": [
- "jasmine",
+ "jest",
"node"
]
npx ts-jest config:init
すると設定ファイルjest.config.jsが生成されます。
そして、このままだとテンプレートファイルの.spec.tsやビルド結果もテスト対象なので除外するよう追記します。
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
+ testPathIgnorePatterns: ['/node_modules/', 'files', '/dist/']
};
npmの代わりにyarnを使う
yarn派の場合速やかにyarnしましょう。package-lock.jsonを消してプロジェクトルートでyarnするだけです(この記事ではnpmで説明します)
rm package-lock.json
yarn
TypeScriptのバージョンを上げる
この記事を書いている時点でのschematics-cliでは、生成されるプロジェクトのTypeScriptのバージョンは3.5.3でした。
現在の最新は3.7.4です。必要に応じてアップデートしてもいいと思います。
npm uninstall typescript
npm install -D typescript@latest
ESLintとPrettierを設定する
普段Angular上の開発ではTSLintとPrettierを設定して使っているのですが、TSLintは非推奨となっていて(2020年末には完全に開発停止)せっかくなのでESLintを設定します。
最初に必要なパッケージをインストールしましょう。
npm i -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin prettier eslint-config-prettier eslint-plugin-prettier eslint-plugin-jest
@typescrpt-eslintの公式ドキュメントを元に.eslint.jsを作成します。
JSONやYAMLでも書けます。
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: [
'@typescript-eslint',
'jest'
],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:jest/recommended',
'plugin:prettier/recommended',
'prettier/@typescript-eslint'
],
};
このうち、plugin:prettier/recommended
はeslintのフォーマットに関するルールを無効にするもので、prettier/@typescript-eslint
は@typescript-eslintのフォーマットに関するルールを打ち消すものです。
詳しくは下記のリポジトリを参照してみてください
- https://github.com/prettier/eslint-plugin-prettier
- https://github.com/prettier/eslint-config-prettier
また、必要に応じてprettierの設定を書いてください。こちらもJSONで書いてもYAMLで書いてもいいです。
module.exports = {
singleQuote: true
}
コマンドからもlintしたい場合はpackage.jsonのscriptsに追記します。
"scripts": {
"build": "tsc -p tsconfig.json && scripts/post-build.sh",
+ "lint": "eslint 'src/**/*.ts'",
+ "lint:fix": "eslint 'src/**/*.ts' --fix",
"test": "jest",
"test:watch": "jest --watch"
},
ng generateのオプションをサポートする
もし、schematicsの実行をng generateコマンドを介して行うことを想定している場合、componentやserviceなどビルトインのschematicsで利用できるオプションは確認しておくといいです。
# ビルトインのschematicsの一覧を表示する
$ ng generate --help
# serviceのオプションを確認する
$ ng generate service --help
...
Help for schematic service
Creates a new, generic service definition in the given or default project.
arguments:
name
The name of the service.
options:
--flat
When true (the default), creates files at the top level of the project.
--lint-fix
When true, applies lint fixes after generating the service.
--project
The name of the project.
--skip-tests
When true, does not create "spec.ts" test files for the new service.
--spec
When true (the default), generates a "spec.ts" test file for the new service.
serviceを拡張するようなschematicを公開する場合、ビルトインのserviceのschematicの振る舞いはできる限りサポートしたほうがよいでしょう。
また、特筆すべきビルトインのschematicsの振る舞いとして、path/to/name
のように名前を指定するとpath/to
ディレクトリ以下に作成することができます。
$ ng generate service path/to/foo
CREATE src/app/path/to/foo.service.spec.ts (318 bytes)
CREATE src/app/path/to/foo.service.ts (132 bytes)
今回はserviceを例に各オプションの実装ポイントを解説します。他のビルトインschematicsのオプションに関しては実際の実装(のindex.tsやschema.ts)を見てください。
--flat
serviceでは--flatオプションはデフォルトtrueで、falseを指定したときにname
ディレクトリの中にファイルを生成します。
$ ng generate service user --flat false
CREATE src/app/user/user.service.spec.ts (323 bytes)
CREATE src/app/user/user.service.ts (133 bytes)
これを実装するには、serviceの実装のとおり、filesディレクトリの中に__name@dasherize@if-flat__
ディレクトリを作り、その中にテンプレートファイルを入れるとよいです。
そして、下記のようにif-flat関数をtemplate関数に入れるとよいです。
const templateSource = apply(url('./files'), [
template({
...options,
'if-flat': (s: string) => (options.flat ? '' : s),
...strings
})
});
--skip-tests
--skip-testsは.spec.tsファイルを生成しないオプションです。
Angular CLI上ではケバブケース(ハイフン区切り)で表記されていますが、schema.tsやschema.jsonではキャメルケースで定義します。
"skipTests": {
"type": "boolean",
"description": "When true, does not create \"spec.ts\" test files for the new service.",
"default": false
}
export interface ServiceOptions {
// ...
skipTests?: boolean;
}
.spec.tsを含めないためには、@angular-devkit/core
のfilter関数を使います。
import {
filter,
noop,
// ...
} from '@angular-devkit/schematics';
export default function(options: ServiceOptions): Rule {
return (tree: Tree, context: SchematicContext) => {
// ...
const templateSource = apply(url('./files'), [
options.skipTests ? filter(path => !path.endsWith('.spec.ts.template')) : noop(),
applyTemplates({
...strings,
'if-flat': (s: string) => options.flat ? '' : s,
...options,
})
]);
//...
--specオプションは--skip-testsオプションとは逆にfalseを指定すると(デフォルトtrue).spec.tsを生成しないオプションです。
おそらく後方互換性のために残しているオプションなので自分のschematicsでは--skip-testsさえ考慮しておけば良さそうです。
--name
--nameオプションは第一引数にオプションフラグ(--name)なしに指定することができ、path/to/nameのように作成するディレクトリを指定することができます。
第一引数を割り当てるためにはnameのschemaを以下のように$default
キーワードを定義します。
"name": {
"type": "string",
"description": "The name of the service.",
"$default": {
"$source": "argv",
"index": 0
}
}
ディレクトリを指定するためには、nameに入った文字列をparseして名前の部分とパスの部分に分ける必要があります。
また、service schematicの実装ではparseされたパスはoptionsオブジェクトのpathプロパティにschematic実行時に入れるような実装になっています。
"path": {
"type": "string",
"format": "path",
"description": "The path at which to create the service, relative to the workspace root.",
"visible": false
}
pathオプションは"visible": false
が指定されているので、Angular CLI上ではユーザーから見えないオプションになっています(helpに出ないだけで指定することは可能)。
export interface ServiceOptions {
// ...
path?: string
}
parse処理は@schematics/angular
にparseName関数が定義されているので、@schematics/angular
をインストールして利用しましょう。
npm i -D @schematics/angular
import { parseName } from '@schematics/angular/utility/parse-name';
export default function(options: ServiceOptions): Rule {
return (tree: Tree, context: SchematicContext) => {
// ...
const parsedPath = parseName(options.path || '', options.name);
options.name = parsedPath.name;
options.path = parsedPath.path;
const templateSource = apply(url('./files'), [
applyTemplates({/* ...*/}),
move(options.path)
]);
// ...
};
}
nameに入っていた'path/to/name'
から'/path/to'
と'name'
に分けられます。
その後テンプレートファイルが@angular-devkit/core
のmove関数により'/path/to'
ディレクトリに移されます。
--lint-fix
--lint-fixはテンプレートを生成した後プロジェクトルートのlintルールを適用するオプションです。
applylintFix関数が@schematics/angular
に定義されているので利用しましょう。
import { applyLintFix } from '@schematics/angular/utility/lint-fix';
export default function(options: ServiceOptions): Rule {
return (tree: Tree, context: SchematicContext) => {
//...
return chain([
mergeWith(templateSource),
options.lintFix ? applyLintFix(options.path) : noop()
]);
};
}
--project
Angularは1つのワークスペースの中に複数のプロジェクトをもたせることができます。
--projectは存在するプロジェクト名が指定されるとそのプロジェクトのangular.jsonに書かれているsourceRootで指定されているパス(通常/projects/プロジェクト名/src
)に生成されます。
また、複数プロジェクト構成でない場合は、Angular CLIにより--projectにデフォルトのプロジェクト名が渡されます。
service schematicでは@schematics/angular
のcreateDefaultPath関数でangular.jsonに指定されているパスを持ってこられます。
export default function (options: ServiceOptions): Rule {
return async (host: Tree) => {
if (options.path === undefined) {
options.path = await createDefaultPath(host, options.project as string);
}
const parsedPath = parseName(options.path, options.name);
// ...
};
}
ただし、schematics-cliで動作確認する場合は注意してください。
createDefaultPath関数の仕様上、angularワークスペース上でなければプロジェクトが存在せず例外が送出されるためです。
Angularプロジェクト上で動かすか、もしくは--pathを明示的に指定する必要があります。
おわりに
Schematicsの実装の仕方はやはりビルトインのSchematicsの実装を参考にするのが一番です。
例えば--moduleオプションなど、今回解説していないオプションについても同じようにして実装できるはずです。
また、テストの書き方に関しては詳しく述べていないですが、僕のschematicsリポジトリで細かくテストを書いているのでよければ参考にしてみてください。