JS 플러그인
Oxlint는 자바스크립트로 작성된 플러그인을 지원합니다 — 자체 개발한 것 또는 npm에서 가져온 것입니다.
Oxlint의 플러그인 API는 ESLint v9 이상과 호환되므로, 대부분의 기존 ESLint 플러그인은 Oxlint에서 바로 사용할 수 있습니다.
우리는 현재 모든 ESLint 플러그인 API를 구현하기 위해 노력 중이며, 곧 Oxlint가 어떤 ESLint 플러그인도 실행할 수 있게 될 것입니다.
WARNING
JS 플러그인은 현재 기술적 사전 데모 단계에 있으며, 여전히 활발히 개발 중입니다. 대부분의 ESLint 플러그인 API는 이미 구현되었습니다 (아래 지원 항목 참조).
모든 API는 ESLint와 동일하게 동작해야 합니다. 만약 동작상 차이를 발견하시면 이는 버그입니다 — 문제 보고하기를 통해 알려주세요.
JS 플러그인 사용 방법
.oxlintrc.json설정 파일 내jsPlugins아래에 플러그인 경로를 추가합니다.rules아래에서 플러그인의 규칙을 추가합니다.
경로는 유효한 불러오기 식별자라면 어떤 것이든 가능합니다. 예: ./plugin.js, eslint-plugin-foo, 또는 @foo/eslint-plugin. 경로는 설정 파일 자체를 기준으로 해석됩니다.
// .oxlintrc.json
{
"jsPlugins": ["./path/to/my-plugin.js", "eslint-plugin-whatever", "@foobar/eslint-plugin"],
"rules": {
"my-plugin/rule1": "error",
"my-plugin/rule2": "warn",
"whatever/rule1": "error",
"whatever/rule2": "warn",
"@foobar/rule1": "error"
}
// ... 기타 설정 ...
}플러그인 별칭
플러그인에 다른 이름(별칭)을 정의할 수도 있습니다. 다음 경우에 유용합니다:
- 기본 플러그인 이름이 내장된 Oxlint 플러그인 이름과 충돌하는 경우 (예: jsdoc, react 등).
- 기본 플러그인 이름이 매우 길 때.
- Oxlint가 내장으로 지원하는 플러그인을 사용하려 하지만, 필요로 하는 특정 규칙이 아직 Oxlint 내장 버전에서 구현되지 않은 경우.
{
"jsPlugins": [
// `jsdoc`는 예약어이며, Oxlint이 내장으로 지원하기 때문
{
"name": "jsdoc-js",
"specifier": "eslint-plugin-jsdoc"
},
// 이름을 줄임
{
"name": "short",
"specifier": "eslint-plugin-with-name-so-very-very-long"
},
// 별칭을 지정하지 않으려는 플러그인은 단순 식별자로 나열
"eslint-plugin-whatever"
],
"rules": {
"jsdoc-js/check-alignment": "error",
"short/rule1": "error",
"whatever/rule2": "error"
}
}JS 플러그인 작성하기
ESLint 호환성 API
Oxlint는 ESLint와 동일한 플러그인 API를 제공합니다. 플러그인 생성 및 사용자 정의 규칙에 대한 ESLint 문서를 참고하세요.
5개 이상의 클래스 선언을 포함하는 파일을 경고하는 간단한 플러그인:
// plugin.js
const rule = {
create(context) {
let classCount = 0;
return {
ClassDeclaration(node) {
classCount++;
if (classCount === 6) {
context.report({ message: "클래스가 너무 많습니다", node });
}
},
};
},
};
const plugin = {
meta: {
name: "best-plugin-ever",
},
rules: {
"max-classes": rule,
},
};
export default plugin;// .oxlintrc.json
{
"jsPlugins": ["./plugin.js"],
"rules": {
"best-plugin-ever/max-classes": "error"
}
}대안 API
Oxlint는 약간 다른 대안 API도 제공하며, 이는 더 높은 성능을 제공합니다.
이 대안 API로 생성된 규칙은 여전히 ESLint와 호환됩니다 (아래 eslintCompatPlugin은 무엇을 하나요? 참조).
위의 규칙을 대안 API로 다시 작성하면:
import { eslintCompatPlugin } from "@oxlint/plugins";
const rule = {
createOnce(context) {
// 카운터 변수 정의
let classCount;
return {
before() {
// 각 파일의 AST 탐색 전에 카운터 초기화
classCount = 0;
},
// 이전과 동일
ClassDeclaration(node) {
classCount++;
if (classCount === 6) {
context.report({ message: "클래스가 너무 많습니다", node });
}
},
};
},
};
const plugin = eslintCompatPlugin({
meta: {
name: "best-plugin-ever",
},
rules: {
"max-classes": rule,
},
});
export default plugin;주요 차이점은 다음과 같습니다:
- 플러그인 객체를
eslintCompatPlugin(...)로 래핑합니다.
- const plugin = {
+ const plugin = eslintCompatPlugin({create대신createOnce를 사용합니다.
- create(context) {
+ createOnce(context) {create(ESLint API)는 각 파일마다 반복적으로 호출되는 반면,createOnce는 오직 한 번만 호출됩니다.
각 파일에 대한 설정은before훅에서 수행하세요.
- let classCount = 0;
+ let classCount;
return {
+ before() {
+ classCount = 0; // 카운터 재설정
+ },
ClassDeclaration(node) {
classCount++;
if (classCount === 6) {
context.report({ message: "클래스가 너무 많습니다", node });
}
},
};eslintCompatPlugin는 무엇을 하나요?
eslintCompatPlugin는 플러그인 내 각 규칙에 create 메서드를 추가하여 createOnce로 위임합니다.
즉, 이 플러그인은 Oxlint이나 ESLint 모두에서 사용할 수 있습니다.
- Oxlint에서는 더 빠른
createOnceAPI 덕분에 성능 향상을 경험할 수 있습니다. - ESLint에서는 원래 ESLint의
createAPI로 작성된 것과 정확히 동일하게 작동합니다.
NPM에 플러그인을 배포하시는 경우, @oxlint/plugins를 실행 시 의존성 (devDependency가 아니라)로 추가하세요.
AST 탐색 건너뛰기
before 훅에서 false를 반환하면 해당 파일의 규칙 실행을 건너뜁니다.
// 이 규칙은 `// @skip-me` 주석으로 시작하는 파일에서는 실행되지 않습니다
const rule = {
createOnce(context) {
return {
before() {
if (context.sourceCode.text.startsWith("// @skip-me")) {
return false;
}
},
FunctionDeclaration(node) {
// 작업 수행
},
};
},
};이는 ESLint에서 다음과 같은 패턴과 동일합니다:
const rule = {
create(context) {
if (context.sourceCode.text.startsWith("// @skip-me")) {
return {};
}
return {
FunctionDeclaration(node) {
// 작업 수행
},
};
},
};before 훅
before 훅은 AST 방문 전에 실행됩니다.
중요: before 훅은 모든 파일에서 보장되어 실행되지는 않습니다.
현재는 실행되지만, 향후 우리는 룰이 관심 있는 특정 AST 노드와 파일의 실제 내용에 따라, 룰 실행 여부를 결정하는 로직을 루스트 측에 도입할 계획입니다.
이는 루스트에서 자바스크립트로의 불필요한 호출을 피함으로써 성능을 향상시킬 수 있습니다.
위 예시에서, 파일에 FunctionDeclaration가 전혀 없는 경우, 그 파일에 대해 규칙 실행을 완전히 건너뛸 것이며, 이에 따라 before 훅도 함께 건너뜁니다.
모든 파일에 대해 항상 한 번 실행되도록 코드를 수행하고 싶다면, Program 방문자(visitor)를 구현하세요:
const rule = {
createOnce(context) {
return {
Program(node) {
// 이 코드는 파일이 `FunctionDeclaration`를 포함하지 않더라도 항상 실행됩니다
},
FunctionDeclaration(node) {
/* 작업 수행 */
},
};
},
};after 훅
또한 after 훅도 존재합니다. 이 훅은 파일당 한 번씩 실행되며, 전체 AST 탐색 후 (Program:exit 이후)에 실행됩니다.
규칙의 AST 탐색 중 사용한 비용이 큰 리소스를 정리하는 데 사용하세요.
만약 before 훅이 파일의 규칙 실행을 건너뛰기 위해 false를 반환하면, after 훅도 건너뜁니다.
before 훅과 마찬가지로, after 훅도 모든 파일에서 보장되어 실행되지는 않습니다 (위 before 훅이 언제 실행되는가? 참조).
왜 대안 API가 더 빠른가요?
짧은 답변: 지금은 아닙니다. 하지만 곧 그렇게 될 것입니다.
초기 기술 사전 데모 버전 발표 전, 우리는 장기간의 "연구개발" 과정을 거쳤습니다. 많은 최적화 기회를 확인했으며, 매우 뛰어난 성능을 가진 다음 세대 Oxlint 플러그인 프로토타입을 개발했습니다.
이러한 최적화 대부분은 현재 릴리즈에는 포함되어 있지 않지만, 앞으로 몇 달 동안 정교화하여 Oxlint에 통합할 예정입니다.
대안 API는 이러한 최적화를 가능하게 하고 활용하도록 설계되었습니다. 지금부터 대안 API를 채택하면, 플러그인 작성자는 oxlint 버전만 올리면 (코드 변경 없이) 훗날 커다란 속도 향상을 자연스럽게 얻게 됩니다.
이러한 최적화는 무엇인가요?
이전 예시인 "5개 이상의 클래스를 허용하지 않는" 규칙으로 돌아가보겠습니다:
const rule = {
create(context) {
let classCount = 0;
return {
ClassDeclaration(node) {
classCount++;
if (classCount === 6) {
context.report({ message: "클래스가 너무 많습니다", node });
}
},
};
},
};create 메서드는 각 파일마다 한 번씩 호출되며, 매번 새로운 context 객체를 전달받습니다.
왜 이것이 문제인가요?
최고의 성능을 위해서는, 규칙이 "관심 있는" 특정 AST 노드를 정적으로 알고 있어야 합니다. 이를 통해 두 가지 최적화가 가능합니다:
자바스크립트 측에서 전체 트리를 탐색하지 않습니다. 대신, 루스트 측에서의 AST 탐색 중 관련된 노드들의 "포인터" 목록을 미리 컴파일합니다. 이 목록을 자바스크립트로 전달하고, 자바스크립트는 전체 트리를 검색하는 대신 관련 노드로 직접 "점프"할 수 있습니다.
규칙이 관심 있는 어떤 노드도 포함하지 않는 파일(예: 클래스 선언이 없는 파일)에 대해서는, 자바스크립트 측으로의 호출을 완전히 생략합니다.
하지만 자바스크립트는 동적 언어이므로, create는 어떤 행동이라도 할 수 있습니다. 매번 호출될 때마다 완전히 다른 방문자 객체를 반환할 수도 있습니다. 따라서 create를 호출해볼 때까지 실제로 create를 호출해야 하는지 알 수 없습니다!
반면, 대안 API에서는 createOnce는 오직 한 번만 호출되며, 이후 규칙의 동작을 정확히 알 수 있습니다. 이로 인해 위의 최적화가 가능해집니다.
명확히 말하자면, create API는 ESLint 측에서 나쁜 디자인 결정이었던 것은 아닙니다. 다만 루스트-자바스크립트 상호작용이 등장하면서 일부 어려움이 발생하는 것입니다.
지원되는 API
Oxlint는 대부분의 ESLint API 표면을 지원합니다:
- AST 탐색
- AST 탐색 (
node.parent,context.sourceCode.getAncestors) - 수정(피드백)
- 규칙 옵션
- 선택자 (ESLint 문서)
SourceCodeAPI (예:context.sourceCode.getText(node))SourceCode토큰 API (예:context.sourceCode.getTokens(node))- 스코프 분석
- 제어 흐름 분석 (코드 경로)
- 인라인 비활성 지시어 (
// oxlint-disable) - 언어 서버(통합 개발 환경) 지원 + 추천사항 (에디터 내 진단 및 빠른 수정)
아직 지원되지 않은 항목:
- 사용자 정의 파일 형식 및 파서 (예: Svelte, Vue, Angular)
- TypeScript 타입 인식에 의존하는 린트 규칙
ESLint v9 또는 이전 버전에서 제거된 ESLint API는 대부분 구현되지 않을 것입니다. 만약 어떤 ESLint 플러그인이 유지 관리되지 않고, ESLint v9용 업데이트가 이루어지지 않았다면, 플러그인 자체를 수정하거나 다른 대안을 찾아야 할 수 있습니다.
앞으로 몇 달 동안 남은 기능들을 구현할 예정이며, 궁극적으로 100%의 ESLint 플러그인 API 표면을 지원할 목표를 가지고 있습니다.
