목차
- 템플릿을 사용하여 저장소 초기화
- 템플릿의 초기 구조
- 템플릿의 스크립트를 사용하여 규칙 추가
- Eslint 플러그인에 대한 테스트 작성
- Eslint 규칙 작성
- 작은 AST 설명
- 최종 변형
- 스크립트를 사용하여 문서 업데이트
- 플러그인 게시
- 애플리케이션과 연결
배경
단계별 설명과 함께 Reatom 저장소에 있는 내 PR을 기반으로 튜토리얼을 작성해 보겠습니다: https://github.com/artalar/Reatom/pull/488
더 알고 싶다면 https://github.com/artalar/reatom/issues/487 문제를 읽어보세요.
약간의 컨텍스트를 추가하기 위해 Reatom은 상태 관리 라이브러리입니다. Atom은 React용 상태 관리 라이브러리인 Reatom의 개념입니다.
ESLint 플러그인 및 규칙은 무엇입니까?
ESLint 플러그인은 핵심 ESLint 패키지와 함께 작동하여 특정 코딩 표준을 적용하는 확장입니다. 플러그인에는 이러한 표준을 시행하기 위한 개별 규칙을 정의하는 rules
폴더가 포함되어 있습니다.
각 rule
모듈에는 규칙을 설명하는 meta
속성과 규칙의 동작을 정의하는 create
속성이 있습니다.
create
함수는 확인 중인 코드와 상호 작용하는 데 사용되는 context
인수를 사용하며 이를 사용하여 라이브러리에 대한 엄격한 명명 규칙을 요구하는 것과 같은 규칙의 논리를 정의할 수 있습니다.
코드를 살펴보자
저장소 초기화
새로운 TypeScript eslint 프로젝트 만들기
npx degit https://github.com/pivaszbs/typescript-template-eslint-plugin reatom-eslint-plugin
그런 다음 새 프로젝트 디렉터리로 이동하고 다음을 사용하여 종속성을 설치합니다.
cd reatom-eslint-plugin && npm i
나는 착한 소년이 되고 싶어서 git을 초기화합니다.
git init && git add . && git commit -m "init"
다음으로 package.json
파일을 열고 name
필드를 찾으세요. 이 필드는 플러그인이 사용될 때 플러그인의 주요 진입점이 되기 때문에 필수적입니다. 다음과 같이 변경할 수 있습니다.
"name": "eslint-plugin-reatom"
또는 범위가 지정된 패키지 명명 규칙을 사용할 수 있습니다.
"name": "@reatom/eslint-plugin"
초기 구조
- scripts // some automation to concentrate on writing rules - docs - rules // here will be generated by npm run add-rule files - src - configs recommended.ts // generated config - rules // all your rules index.ts // Connection point to your plugin, autogenerated by scripts/lib/update-lib-index.ts
일반적인 색인에서는 파일이 스크립트에 의해 생성되므로 걱정할 필요가 없습니다 😀
/* DON'T EDIT THIS FILE. This is generated by 'scripts/lib/update-lib-index.ts' */ import { recommended } from './configs/recommended'; import exampleRule from './rules/example-rule' export const configs = { recommended }; export const rules = { 'example-rule': exampleRule };
규칙 추가 및 문서 업데이트
이 저장소에서는 규칙을 추가하고 문서를 업데이트하기 위한 몇 가지 편리한 스크립트를 찾을 수 있습니다. 새 규칙을 추가하려면 다음 명령을 사용할 수 있습니다.
npm run add-rule atom-rule suggestion
그러면 새 규칙에 대한 문서, 테스트 및 실제 코드의 세 가지 섹션이 생성됩니다. 지금은 문서 섹션을 건너뛰고 마지막 두 섹션에 집중할 수 있습니다.
테스트 작성
TDD(테스트 중심 개발) 매니아로서, tests/atom-rule.ts
파일에서 몇 가지 간단한 테스트를 만드는 것부터 시작하겠습니다.
// tests/atom-rule.ts tester.run('atom-rule', atomRule, { valid: [ { code: 'const countAtom = atom(0, "countAtom");' }, ], invalid: [ { code: `const countAtom = atom(0);`, errors: [{ message: 'atom name is not defined' }] }, { code: 'const countAtom = atom(0, "count");', errors: [{ message: 'atom name is defined bad'}] }, ] });
지금 테스트를 실행하면 아직 atomRule
구현하지 않았기 때문에 테스트가 실패합니다.
규칙 작성
atomRule
은 규칙의 동작을 정의하는 곳입니다. 간단한 구현은 다음과 같습니다.
import { Rule } from "eslint"; const rule: Rule.RuleModule = { meta: { docs: { description: "Add name for every atom call", // simply describe your rule recommended: true, // if it's recommended, then npm run update will add it to recommmended config }, type: "suggestion" }, create: function (context: Rule.RuleContext): Rule.RuleListener { return { VariableDeclaration: node => { // listener for declaration, here we can specifiy more specific selector node.declarations.forEach(d => { if (d.init?.type !== 'CallExpression') return; if (d.init.callee.type !== 'Identifier') return; if (d.init.callee.name !== 'atom') return; if (d.id.type !== 'Identifier') return; // just guard everything that we don't need if (d.init.arguments.length <= 1) { // show error in user code context.report({ message: `atom name is not defined`, // here we can pass what will be underlined by red/yellow line node, }) } if (d.init.arguments[1]?.type !== 'Literal') return; // just another guard if (d.init.arguments[1].value !== d.id.name) { context.report({ message: `atom name is defined bad`, node }) } }) } }; }, }; export default rule;
단순한 변형이지만 여기서는 무슨 일이 일어나고 있는지 쉽게 이해할 수 있습니다.
코드의 AST 구조를 더 잘 이해하려면 https://astexplorer.net/을 사용하거나 간단히 console.log 구문 분석 노드를 사용할 수 있습니다.
AST 유형에 대한 더 나은 이해를 위한 간단한 설명
다음은 작은 예에서 각 식별자에 대한 간단한 설명입니다.
const kek = atom('kek')
Identifier
: AST의 식별자 노드를 나타내는 TypeScript 인터페이스입니다.const kek = atom ('kek'), kek 및 atom은 식별자 노드입니다.
Literal
: AST의 리터럴 값(문자열, 숫자, 부울 등) 노드를 나타내는 TypeScript 인터페이스입니다. const kek =atom(' kek '), 'kek'은 리터럴입니다.
CallExpression
: AST(추상 구문 트리)에서 함수 호출 표현식 노드를 나타내는 TypeScript 인터페이스입니다.
우리 예에서 atom('kek')은 CallExpression으로, Atom - Identifier와 kek - Literal로 구성됩니다.
VariableDeclarator
: AST의 변수 선언자 노드를 나타내는 TypeScript 인터페이스
이 예에서 const를 제외한 전체 표현식은 VariableDeclarator kek =atom('kek') 입니다.
Node
: 일반 AST 노드를 나타내는 TypeScript 인터페이스입니다.
아니면 단순히 asexplorer를 사용하여
최종 변형
최종 테스트
tester.run('atom-rule', rule, { valid: [ { code: ` import { atom } from '@reatom/framework' const countAtom = atom(0, "countAtom"); ` }, { code: `const countAtom = atom(0);`, }, { code: 'const countAtom = atom(0, "count");', }, ], invalid: [ { code: ` import { atom } from '@reatom/framework' const countAtom = atom(0); `, errors: [{ message: 'atom "countAtom" should has a name inside atom() call', }], output: ` import { atom } from '@reatom/framework' const countAtom = atom(0, "countAtom"); `, }, { code: ` import { atom } from '@reatom/framework' const countAtom = atom(0, "count"); `, errors: [{ message: `atom "countAtom" should be named as it's variable name, rename it to "countAtom"` }], output: ` import { atom } from '@reatom/framework' const countAtom = atom(0, "countAtom"); `, }, ] });
테스트를 통해 우리는 규칙을 사용하여 소스 코드를 어떻게든 변경해야 한다는 것을 이해합니다.
규칙을 수정 가능하게 만드는 방법은 무엇입니까?
컨텍스트 보고서에 간단한 줄을 추가합니다.
fix: fixer => fixer.replaceText(node, replaceString)
노드 - 대체하려는 실제 노드 또는 기호 범위일 수 있습니다.
replacementString - 표시될 것으로 예상되는 코드입니다.
규칙 메타 태그에 fixable : 'code' 또는 fixable : 'whitespace'를 추가하는 것을 잊지 마세요.
eslint로 수정하는 방법에 익숙하지 않다면 기존 프로젝트에서 시도해 보세요.
eslint --fix ./src
코드 자체
import { Rule } from "eslint"; import { CallExpression, Identifier, Literal, VariableDeclarator, Node } from 'estree'; import { isIdentifier, isLiteral } from "../lib"; type AtomCallExpression = CallExpression & { callee: Identifier, arguments: [Literal] | [Literal, Literal] } type AtomVariableDeclarator = VariableDeclarator & { id: Identifier, init: AtomCallExpression } const noname = (atomName: string) => `atom "${atomName}" should has a name inside atom() call`; const invalidName = (atomName: string) => `atom "${atomName}" should be named as it's variable name, rename it to "${atomName}"`; export const atomRule: Rule.RuleModule = { meta: { type: 'suggestion', docs: { recommended: true, description: "Add name for every atom call" }, fixable: 'code' }, create: function (context: Rule.RuleContext): Rule.RuleListener { let importedFromReatom = false; return { ImportSpecifier(node) { const imported = node.imported.name; // @ts-ignore const from = node.parent.source.value; if (from.startsWith('@reatom') && imported === 'atom') { importedFromReatom = true; } }, VariableDeclarator: d => { if (!isAtomVariableDeclarator(d) || !importedFromReatom) return; if (d.init.arguments.length === 1) { reportUndefinedAtomName(context, d); } else if (isLiteral(d.init.arguments[1]) && d.init.arguments[1].value !== d.id.name) { reportBadAtomName(context, d); } } }; } } function isAtomCallExpression(node?: Node | null): node is AtomCallExpression { return node?.type === 'CallExpression' && node.callee?.type === 'Identifier' && node.callee.name === 'atom'; } function isAtomVariableDeclarator(node: VariableDeclarator): node is AtomVariableDeclarator { return isAtomCallExpression(node.init) && isIdentifier(node.id); } function reportUndefinedAtomName(context: Rule.RuleContext, d: AtomVariableDeclarator) { context.report({ message: noname(d.id.name), node: d, fix: fixer => fixer.insertTextAfter(d.init.arguments[0], `, "${d.id.name}"`) }); } function reportBadAtomName(context: Rule.RuleContext, d: AtomVariableDeclarator) { context.report({ message: invalidName(d.id.name), node: d, fix: fixer => fixer.replaceText(d.init.arguments[1], `"${d.id.name}"`) }); }
보시다시피 오류가 더 많고 가드 유형이 있으며 가져오기 검사가 포함되어 있습니다. 그리고 물론, 나는 그 규칙을 고칠 수 있게 만듭니다.
문서 업데이트
문서를 업데이트하려면 다음 명령을 사용할 수 있습니다.
npm run update
이 명령은 README.md를 업데이트하고 각 규칙에 대한 문서를 업데이트합니다(그러나 docs/{rule} 파일의 각 규칙에 대해 약간 작성해야 합니다).
또한 앞서 말했듯이 인덱스 파일에 대해서는 걱정할 필요가 없습니다.
게시 단계
버전이 package.json에 있는지 확인하세요.
"version": "1.0.0"
1.0.0이 아닌 경우 용어로 작성하십시오.
npm version 1.0.0
그런 다음 루트에 쓰십시오.
npm publish
정의된 패키지 이름으로 모든 것이 빌드되고 게시됩니다.
애플리케이션과 연결
내 패키지에 이름을 지정합니다.
@reatom/eslint-plugin
그래서 설치해야합니다.
npm i @reatom/eslint-plugin
그리고 내 .eslintrc 구성에 추가하세요.
module.exports = { plugins: [ "@reatom" ], // use all rules extends: [ "plugin:@reatom/recommended" ], // or pick some rules: { '@reatom/atom-rule': 'error', // aditional rules, you can see it in PR '@reatom/action-rule': 'error', '@reatom/reatom-prefix-rule': 'error' } }
그리고 모든 것이 제대로 작동합니다( reatom-eslint-plugin
의 경우 모든 곳에 " “@reatom"
“reatom”
을 써야 합니다).
결론
이 튜토리얼에서는 Reatom 상태 관리 라이브러리용 ESLint 플러그인을 생성하는 과정을 살펴보았습니다. 우리는 다음을 다룹니다:
- Typescript에서 eslint 플러그인을 작성하는 방법
- 테스트로 이를 다루는 방법.
- --fix 옵션을 사용하여 작동시키는 방법.
- 내 템플릿을 사용하는 방법.
- eslint 플러그인을 게시하는 방법.
- eslint를 사용하여 기존 저장소에 추가하는 방법
추가 학습 및 탐구를 위한 리소스
- https://github.com/pivaszbs/typescript-template-eslint-plugin
- https://astexplorer.net/
- https://github.com/artalar/reatom/pull/488/files
- https://eslint.org/docs/latest/extend/plugins
- https://www.reatom.dev/
- https://github.com/artalar/reatom
- https://docs.npmjs.com/about-semantic-versioning
재미있게 보내세요 :)