静的サイトをラッピングしてAppStoreやGoogle Playから配信するモバイルアプリにすることができるCapacitorというライブラリがあります。考え方としてはCordovaの後継であり、HTML5ハイブリッドアプリやガワアプリというようなアプリをつくることができます。
そのCapacitorが、ng add
に対応する @capacitor/angular
というパッケージを提供しているので、そのコードを読んでみましょう。
ionic-team/capacitor-angular-toolkit
https://github.com/ionic-team/capacitor-angular-toolkit/blob/master/schematics/add/index.ts
コードリーディング
構成
まず、何によって @capacitor/angular
パッケージができているかをみるために、package.jsonをみます(抜粋)
{
..中略..
"peerDependencies": {
"@angular-devkit/core": "^8.0.0",
"@angular-devkit/schematics": "^8.0.0",
"rxjs": "~6.4.0",
"typescript": "~3.4.3"
},
..中略..
"schematics": "./schematics/collection.json"
}
rxjsとtypescriptを利用している以外に、 @angular-devkit
を利用しています。Angularは、Angular を拡張するための開発パッケージとして、 @angular-devkit
、 schematics
を提供しており、 @angular-devkit/core
はそのコアファイル、 @angular-devkit/schematics
はAngular CLIの機能拡張などに利用することができます。今回取り上げて ng add
の拡張だけではなく、 ng generate
で任意のテンプレートを追加したりすることも可能です。
そして schematics
キーで、このパッケージのschematicsの内容を記述した collection.json
を指定しているので、続いてこちらをみることにします。
{
"$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
"schematics": {
"ng-add": {
"description": "Add Capacitor to your project",
"factory": "./add",
"schema": "./add/schema.json"
},
"cap-init": {
"description": "Run Cap init",
"factory": "./cap-init",
"schema": "./cap-init/schema.json"
}
}
}
$scheme
キーは、このcollectionが何に従って記述されてるかを示しています(IDEの補完などに利用されます)。そして、2つの schematics
を定義しています。ng add
が実行されると ng-add
が自動的に実行され、 cap-init
は ng-add
の実行中に RunSchematicTask
で実行されます。では、 ng-add
で具体的にどんな処理が行われるかをみてみましょう。
処理
100行近くありますが、関数が多く、実際に実行されているのはこの部分です。
export default function ngAdd(options: CapAddOptions): Rule {
return async (host: Tree) => {
const workspace = await getWorkspace(host);
if (!options.project) {
options.project = workspace.extensions.defaultProject as string;
}
const projectTree = workspace.projects.get(options.project);
if (projectTree.extensions['projectType'] !== 'application') {
throw new SchematicsException(
`Capacitor Add requires a project type of "application".`
);
}
const packageMgm = getPackageManager(projectTree.root);
const distTarget = projectTree.targets.get('build').options[
'outputPath'
] as string;
const sourcePath = projectTree.sourceRoot;
return chain([
addCapacitorToPackageJson(),
addCapPluginsToAppComponent(sourcePath),
capInit(options.project, packageMgm, distTarget),
]);
};
}
getWorkspace()
は @schematics/angular/utility/workspace
からAngularのworkspace(※ angular.jsonで設定されています)を取得することができます。このように、様々なメソッドが用意されています。
まずworkspaceを取得して、workspaceにprojectがない場合はデフォルトのものを追加しています。(Ionicの場合は app
。 angular.json
で設定されている値です)
そして、その projectType
が application
以外の場合はCapacitorを利用できないとエラーを返します。問題ない場合は、 getPackageManager
メソッドからパッケージを取得してきて、またビルドの outputPath
をprojectから取得してきます。
そして、自作している addCapacitorToPackageJson()
(dependenciesに@capacitor/core、devDependenciesに@capacitor/cliを追加)と、 addCapPluginsToAppComponent
メソッド(app.component.tsに import { Plugins } from '@capacitor/core';
を追加)するメソッドとともに、Capacitor CLIをつかって npx cap init
を行う capInit
メソッドを実行しています。
Prettierパッケージをつくってみる
何となくどのように ng add
が実装されてるかがわかりました。では、 @capacitor/angular
をベースに、Prettierの自作パッケージをつくってみましょう。
まず @capacitor/angular
をForkします。パッケージ名は prettier-angular-toolkit
とします。
1. パッケージ名などの変更
package.jsonの情報を変更します。ゼロからつくるより、コードリーディングしてOSSの他パッケージをForkしてつくった方が土台がしっかりしてるので、結構好みだったりします。自分で実装することによって、元コードの理解も深まりますし。
2. 不要なフォルダを削除
schematics/cap-init
は今回使わないので削除します。
3. パッケージの変更
インストールするパッケージを変更します。 schematics/add/index.ts
でインストールするパッケージを、Capacitor関係からPrettierまわりに変更します。今回は、 dependencies
はないので当該行は削除して、 devDependencies
を以下に書き換えます。
function addFormatterToPackageJson(): Rule {
return (host: Tree) => {
addPackageToPackageJson(
host,
'devDependencies',
'prettier',
'latest'
);
addPackageToPackageJson(
host,
'devDependencies',
'lint-staged',
'latest'
);
addPackageToPackageJson(
host,
'devDependencies',
'@kaizenplatform/prettier-config',
'latest'
);
return host;
};
}
addPackageToPackageJson
はIonicTeamの自作ライブラリですが、とても汎用性が高く便利です。
lint-staged
はPrettierの範囲をGitでステージにあがったファイルに限定するために利用します(対象ファイル数が多くなるとフォーマットに時間がかかるため)。 @kaizenplatform/prettier-config
はPrettierのフォーマットとして利用します(もっといいフォーマットがある場合コメントにて教えてください)
また、 addCapPluginsToAppComponent
を変更して、 addPrettierConfig
として、prettier.config.js
作成用のメソッドにします。hostのprojectRootに prettier.config.js
があるかないかを判定して、ない場合追加する簡単なメソッドです。
function addPrettierConfig(projectRoot: string): Rule {
return (host: Tree) => {
const sourcePath = `${projectRoot}/prettier.config.js`;
if (!host.exists(sourcePath)) {
host.create(sourcePath, 'module.exports =require(\'@kaizenplatform/prettier-config\');')
}
return host;
};
}
またPackage.jsonをアップデートして、タスクとlint-stagedを設定する必要があります。これはメソッドが存在しないので、自作しましょう。 schematics/utils/package.ts
に追加します。
/**
* Adds a method to the package.json
*/
export function addMethodToPackageJson(host: Tree, scriptsName: string, method: string) {
if (host.exists('package.json')) {
const sourceText = host.read('package.json')!.toString('utf-8');
const json = JSON.parse(sourceText);
if (!json['scripts']) {
json['scripts'] = {};
}
if (!json['scripts'][scriptsName]) {
json['scripts'][scriptsName] = method;
}
host.overwrite('package.json', JSON.stringify(json, null, 2));
}
return host;
}
/**
* Adds a method to the package.json
*/
export function addKeyToPackageJson(host: Tree, key: string, method: string | object) {
if (host.exists('package.json')) {
const sourceText = host.read('package.json')!.toString('utf-8');
const json = JSON.parse(sourceText);
if (!json[key]) {
json[key] = method;
}
host.overwrite('package.json', JSON.stringify(json, null, 2));
}
return host;
}
似たようなメソッドですが、どちらもpackage.jsonへの追記のためのメソッドです。前者をオリジナルのscriptsの追加、後者をオリジナルのキーの追加に利用します。それでは、活用していきましょう。 addFormatterToPackageJson()
に更に追加します。
addScriptsToPackageJson(
host,
'lint-staged',
'lint-staged'
);
addScriptsToPackageJson(
host,
'formatter',
'prettier --parser typescript --write \"./**/*.ts\" && prettier --parser html --write \"./**/*.html\"'
);
addKeyToPackageJson(
host,
'pre-commit',
[
'lint-staged',
]
);
addKeyToPackageJson(
host,
'lint-staged',
{
'*.ts': [
'prettier --parser typescript --write',
'git add',
],
'*.html': [
'prettier --parser html --write',
'git add',
],
}
);
まだnpm installを実行していないので、最後にnpm installを行いましょう。 spawn
を使って直接実行します。
function doNpmInstall(): Rule {
return (host: Tree) => {
return new Observable<Tree>(subscriber => {
const child = spawn('npm', ['install'], { stdio: 'inherit' });
child.on('error', error => {
subscriber.error(error);
});
child.on('close', () => {
subscriber.next(host);
subscriber.complete();
});
return () => {
child.kill();
return host;
};
});
};
}
実装できました。 npm publish
で公開しましょう。これで、 ng add @rdlabo/ng-add-formatter
が利用できるようになりました。簡単ですね。
まとめ
Angularは、 @schematics
や @angular-devkit
などのパッケージを通して、開発者がAngularを土台にして自作ツールや開発の効率化を図りやすいように様々な支援をしています。
Angularのルールがちょっとしんどい、もしくはもっと業務を効率化したいという人は、ぜひ自作ツールづくりにチャレンジしてみてください!