はじめに
Angularプロジェクトで作成したファイル名がAngularコーディングスタイルガイドに沿っているかチェックするESLintのカスタムルール『eslint-plugin-angular-file-naming』を作成してnpmパッケージとして公開しました。
- GitHubリポジトリ
- npmパッケージ
eslint-plugin-angular-file-namingの概要
eslint-plugin-angular-file-namingでは、5件のESLintのカスタムルールを提供していて、それぞれのルールでAngularのファイルタイプ(Component
, Directive
, Module
, Pipe
, Service
)に適したsuffix(.component.ts
, .directive.ts
, .module.ts
, .pipe.ts
, .service.ts
...)がファイル名に設定されているかを確認します。
- eslint-plugin-angular-file-namingのカスタムルール一覧
また、suffixについてはデフォルトで設定されている値(.component.ts
, .directive.ts
, .module.ts
, .pipe.ts
, .service.ts
...)とは別にオプションでsuffixの値を設定することもできます。
使用例
例えば、以下のように.eslintrc
を設定した場合は、
// .eslintrc.json module.exports = { "plugins": [ ..., "angular-file-naming" ], "rules": [ ..., "angular-file-naming/component-filename-suffix": [ "error", { "suffixes": ["component", "page", "view"] } ], "angular-file-naming/directive-filename-suffix": "error", "angular-file-naming/module-filename-suffix": "error", "angular-file-naming/pipe-filename-suffix": "error", "angular-file-naming/service-filename-suffix": [ "error", { "suffixes": ["service", "guard", "store"] } ], ] ..., }
以下のファイル名が誤ったファイル名になり、
app.comp.ts sample.ts test.filter.ts
以下のファイル名が正しいファイル名になります。
app.component.ts app.page.ts app.view.ts test.directive.ts app.module.ts sample.pipe.ts test.service.ts test.guard.ts test.store.ts
環境
以下のバージョンのライブラリを使用して、ESLintのカスタムルールのnpmパッケージを作成しました。
- eslint v7.28.0
- typescript v4.3.2
- jest v27.0.4
Angular v12のプロジェクトで動作確認をしています。
実装
eslint-plugin-angular-file-namingのカスタムルールの一つ、component-filename-suffixの実装内容について説明していきます。
カスタムルールのコード
component-filename-suffixは、@Component
デコレーターを持つクラスのファイル名のsuffixが.component.ts
(もしくはオプションで設定したsuffix)に設定されているかをチェックするルールになります。
以下2件のソースコードのうち、src/rules/component-filename-suffix.ts
はcomponent-filename-suffixのソースコード、src/utils/utils.ts
は、component-filename-suffixが参照しているユーティリティになります。
src/rules/component-filename-suffix.ts
import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; import { COMPONENT_CLASS_DECORATOR, getFilenameSuffix, STYLE_GUIDE_LINK, } from '../utils/utils'; type Options = { suffixes: string[] }; export const componentFilenameSuffix: TSESLint.RuleModule< 'componentFilenameSuffix', [Options] > = { meta: { type: 'suggestion', docs: { category: 'Best Practices', description: `The file name of Classes decorated with @Component must have suffix "component" (or custom) in their name. See more at ${STYLE_GUIDE_LINK}`, recommended: 'error', url: 'https://github.com/l08084/eslint-plugin-angular-file-naming/blob/main/docs/rules/component-filename-suffix.md', }, messages: { componentFilenameSuffix: `The file name of component class should end with one of these suffixes: {{suffixes}} (${STYLE_GUIDE_LINK})`, }, schema: [ { type: 'object', properties: { suffixes: { type: 'array', items: { type: 'string', }, }, }, additionalProperties: false, }, ], }, create: (context) => { return { [COMPONENT_CLASS_DECORATOR](node: TSESTree.Decorator) { const filename = context.getFilename(); const suffixes = context.options[0]?.suffixes || ['component']; const filenameSuffix = getFilenameSuffix(filename); if ( !filenameSuffix || !(filenameSuffix.length > 1) || !suffixes.find((suffix) => suffix === filenameSuffix[1]) ) { context.report({ node, messageId: 'componentFilenameSuffix', data: { suffixes }, }); } }, }; }, }; module.exports = componentFilenameSuffix; export default componentFilenameSuffix;
src/utils/utils.ts
export const STYLE_GUIDE_LINK = 'https://angular.io/guide/styleguide#style-02-03'; export const COMPONENT_CLASS_DECORATOR = 'ClassDeclaration > Decorator[expression.callee.name="Component"]'; export const DIRECTIVE_CLASS_DECORATOR = 'ClassDeclaration > Decorator[expression.callee.name="Directive"]'; export const PIPE_CLASS_DECORATOR = 'ClassDeclaration > Decorator[expression.callee.name="Pipe"]'; export const MODULE_CLASS_DECORATOR = 'ClassDeclaration > Decorator[expression.callee.name="NgModule"]'; export const INJECTABLE_CLASS_DECORATOR = 'ClassDeclaration > Decorator[expression.callee.name="Injectable"]'; /** * get filename suffix * * @export * @param {string} filename * @returns {(RegExpMatchArray | null)} */ export function getFilenameSuffix(filename: string): RegExpMatchArray | null { return filename.match(/(?:\/|\\).+\.(.+)\.ts$/); }
ソースコードに@Component
デコレーターが含まれているかどうかのチェックは、ESLintのSelector機能を使用しています。
export const COMPONENT_CLASS_DECORATOR = 'ClassDeclaration > Decorator[expression.callee.name="Component"]';
ファイル名のsuffixの抽出は、正規表現のグループ機能を使用しています。
return filename.match(/(?:\/|\\).+\.(.+)\.ts$/);
テストコード
component-filename-suffixのテストコードです。
テスティングフレームワークとして、Jestを使用しています。
tests/rules/component-filename-suffix.test.ts
import { TSESLint } from '@typescript-eslint/experimental-utils'; import componentFilenameSuffix from '../../src/rules/component-filename-suffix'; const tester = new TSESLint.RuleTester({ parser: require.resolve('@typescript-eslint/parser'), }); tester.run('componentFilenameSuffix', componentFilenameSuffix, { valid: [ { code: ` @Component({ selector: 'sg-foo-bar', templateUrl: './test.component.html', }) class TestComponent {} `, filename: '/src/app/test.component.ts', }, { code: ` @Component({ selector: 'sg-foo-bar', templateUrl: './test.component.html', }) class TestComponent {} `, filename: '/src/app/test.component.ts', options: [{ suffixes: ['component', 'page'] }], }, { code: ` @Component({ selector: 'sg-foo-bar', templateUrl: './test.page.html', }) class TestComponent {} `, filename: '/src/app/test.page.ts', options: [{ suffixes: ['component', 'page'] }], }, { code: ` @Component({ selector: 'sg-foo-bar', templateUrl: './test.page.html', }) class TestPage {} `, filename: '/src/app/test.page.ts', options: [{ suffixes: ['page'] }], }, { code: ` @Directive({ selector: '[myHighlight]' }) class TestDirective {} `, filename: '/src/app/test.directive.ts', options: [{ suffixes: ['page'] }], }, ], invalid: [ { code: ` @Component({ selector: 'sg-foo-bar', templateUrl: './test.component.html', }) class TestComponent {} `, filename: '/src/app/test.components.ts', errors: [{ messageId: 'componentFilenameSuffix' }], }, { code: ` @Component({ selector: 'sg-foo-bar', templateUrl: './test.view.html', }) class TestView {} `, filename: '/src/app/test.view.ts', options: [{ suffixes: ['component', 'page'] }], errors: [{ messageId: 'componentFilenameSuffix' }], }, { code: ` @Component({ selector: 'sg-foo-bar', templateUrl: './test.component.html', }) class TestComponent {} `, filename: '/src/app/test.component.ts', options: [{ suffixes: ['view', 'page'] }], errors: [{ messageId: 'componentFilenameSuffix' }], }, ], });
参考サイト
How can I enforce filename and folder name convention in typescript eslint? - Stack Overflow
eslint-plugin-unicorn/filename-case.md at main · sindresorhus/eslint-plugin-unicorn · GitHub