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: "Too many classes", node });
}
},
};
},
};
const plugin = {
meta: {
name: "best-plugin-ever",
},
rules: {
"max-classes": rule,
},
};
export default plugin;{
"jsPlugins": ["./plugin.js"],
"rules": {
"best-plugin-ever/max-classes": "error"
}
}import { defineConfig } from "oxlint";
export default defineConfig({
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: "Too many classes", 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) {- ESLint의
create는 파일마다 반복 호출되지만,createOnce는 한 번만 호출됩니다. 파일 단위 준비는before훅에서 하세요.
- let classCount = 0;
+ let classCount;
return {
+ before() {
+ classCount = 0; // 카운터 초기화
+ },
ClassDeclaration(node) {
classCount++;
if (classCount === 6) {
context.report({ message: "Too many classes", node });
}
},
};eslintCompatPlugin은 무엇을 하나요?
eslintCompatPlugin은 플러그인의 각 규칙에 create 메서드를 추가하고, 그 안에서 createOnce에 위임합니다.
즉 플러그인을 Oxlint와 ESLint 어느 쪽에서도 쓸 수 있습니다.
- Oxlint에서는 더 빠른
createOnceAPI 덕에 성능 이점을 얻습니다. - ESLint에서는 원래 ESLint
createAPI로 썼을 때와 동일하게 동작합니다.
NPM에 플러그인을 게시한다면 @oxlint/plugins를 런타임 의존성으로 추가하세요(개발 의존성이 아님).
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 훅이 모든 파일에서 반드시 실행된다고 보장되지는 않습니다.
현재는 그렇지만, 향후 Rust 쪽에서 규칙이 관심 있는 AST 노드와 파일 AST 내용을 보고 규칙 실행 필요 여부를 판단하는 로직을 넣을 예정입니다. 불필요한 Rust→JS 호출을 줄여 성능을 높입니다.
위 예에서 파일에 FunctionDeclaration이 하나도 없으면 해당 파일은 규칙 실행 전체가 생략되며, before 훅도 호출되지 않습니다.
파일마다 반드시 한 번씩 실행되어야 하는 코드가 필요하면 Program 방문자를 구현하세요:
const rule = {
createOnce(context) {
return {
Program(node) {
// `FunctionDeclaration`이 없어도 모든 파일에서 실행됨
},
FunctionDeclaration(node) {
/* 작업 */
},
};
},
};after 훅
after 훅도 있습니다. 파일당 한 번, 전체 AST 순회가 끝난 뒤(Program:exit 이후) 실행됩니다.
AST 순회 동안 쓴 비용 큰 리소스를 정리할 때 사용합니다.
before가 false를 반환해 파일을 건너뛰면 after도 건너뜁니다.
before와 마찬가지로 after도 모든 파일에서 반드시 실행된다고 보장되지 않습니다(위 `before` 훅 참조).
대체 API가 더 빠른 이유
요약: 지금 당장은 아닙니다. 하지만 곧 그렇게 될 것입니다.
JS 플러그인 기술 미리보기 이전에 긴 R&D를 거치며 최적화 포인트를 많이 찾았고, 성능이 매우 좋은 차세대 Oxlint 플러그인 프로토타입도 만들었습니다.
그 최적화 상당수는 아직 현재 릴리스에 없지만, 앞으로 몇 달 안에 다듬어 Oxlint에 반영할 예정입니다.
대체 API는 이런 최적화를 활용하도록 설계되었습니다. 지금 대체 API를 쓰면 이후 oxlint 버전만 올려도 코드 변경 없이 플러그인 속도가 크게 나아질 수 있습니다.
어떤 최적화인가?
위의 「클래스 5개 초과 금지」 규칙으로 돌아가면:
const rule = {
create(context) {
let classCount = 0;
return {
ClassDeclaration(node) {
classCount++;
if (classCount === 6) {
context.report({ message: "Too many classes", node });
}
},
};
},
};create 메서드는 파일마다 호출되며, 매번 새 context 객체가 들어옵니다.
무슨 문제인가?
최대 성능을 위해서는 규칙이 관심 있는 AST 노드를 정적으로 알 수 있는 것이 이상적입니다. 그러면 두 가지 최적화가 가능합니다:
JS 쪽에서 AST 전체를 걷지 않습니다. 대신 Rust 쪽 AST 순회 중 관련 노드 포인터 목록을 만들어 JS로 보내고, JS는 전체를 찾지 않고 해당 노드로 바로 이동합니다.
파일 AST에 규칙이 관심 있는 노드가 전혀 없으면(위 예에서는 클래스 선언이 없음) 해당 파일은 JS 호출 자체를 생략합니다.
하지만 JS는 동적 언어이고 create는 무엇이든 할 수 있습니다. 호출될 때마다 완전히 다른 방문자 객체를 반환할 수도 있습니다. 그래서 create를 호출해 봐야 create가 필요했는지 알 수 있습니다!
반면 대체 API에서는 createOnce가 한 번만 호출되고 규칙 동작을 알 수 있어 위 최적화가 가능합니다.
분명히 말하면 create API가 ESLint 설계 실수였던 것은 아닙니다. Rust–JS 상호 운용이 들어오면 어려움이 생깁니다.
다음 단계
Oxlint 플러그인에서 사용할 수 있는 ESLint API는 API 지원 절을 참고하세요.