2025년 9월
Prettier - 모든 플러그인 통과시키기
Proxy로 parser 선택을 가로채고, nested format()으로 플러그인을 모두 적용시키는 방법
문제 재확인
Prettier의 이상한 구조에서 다룬 것처럼, Prettier는 parser를 고를 때
Array.findLast()실제로 import 정렬, Tailwind class 정렬, JSX attribute 정렬처럼 estree parser를 공유하는 플러그인을 함께 쓰면, 배열 순서와 관계없이 마지막 플러그인만 살아남습니다.
"prettier-plugin-tailwindcss"제작 목적
저는 prettier 플러그인을 여러 개 쓰면서, 각각의 변환을 순서대로 모두 적용하고 싶었습니다. sort-imports → organize-imports → tailwindcss → organize-attributes처럼, 배열에 적은 순서가 곧 실행 순서가 되길 원했습니다.
Prettier core를 포크하거나 플러그인을 하나로 합치는 대신, 플러그인 시스템 안에서 해결할 방법을 찾았습니다. 그 결과물이
"format-linear.mjs"// .prettierrc.mjs
const config = {
trailingComma: "all",
plugins: [
"@trivago/prettier-plugin-sort-imports",
"prettier-plugin-organize-imports",
"prettier-plugin-tailwindcss-canonical-classes",
"prettier-plugin-tailwindcss",
"@xeonlink/prettier-plugin-organize-attributes",
await import("./scripts/format-linear.mjs").then(
(module) => module.default,
), // 항상 마지막
],
};
export default config;Prettier의 관문
format-linear가 파고든 지점은 parser-and-printer.js의
function getParserPluginByParserName(...)// prettier/src/main/parser-and-printer.js
function getParserPluginByParserName(plugins, parserName) {
const plugin = plugins.findLast(
(plugin) => plugin.parsers && Object.hasOwn(plugin.parsers, parserName),
);
if (plugin) {
return plugin;
}
throw new ConfigError(`Couldn't resolve parser "${parserName}".`);
}저는 특히
Object.hasOwn(...)createAlwaysHasProxy — parser 가로채기
format-linear의 parsers 객체는 일반 객체가 아니라 Proxy입니다.
Object.hasOwn(...)function createAlwaysHasProxy(fieldValue) {
return new Proxy(
{},
{
get() {
return fieldValue;
},
has() {
return true; // Object.hasOwn(parsers, "estree") → 항상 true
},
getOwnPropertyDescriptor() {
return {
value: fieldValue,
configurable: true,
enumerable: true,
writable: true,
};
},
},
);
}has()Object.hasOwn(...)babeltypescript선형 파이프라인
format-linear의 parse 함수는 AST를 만들지 않습니다. 대신
format()parse: async (text, options) => {
const config = await resolveConfig(options.filepath);
const pluginsLength = config?.plugins?.length ?? 0;
let mergedText = text;
for (let i = -pluginsLength + 1; i < 0; i++) {
await clearConfigCache();
const config = await resolveConfig(options.filepath);
if (!config) {
return mergedText;
}
config.plugins = options.plugins?.slice(-pluginsLength, i) ?? [];
config.filepath = options.filepath;
mergedText = await format(mergedText, config);
}
return mergedText;
},루프 변수
i-pluginsLength + 1-1slice(-pluginsLength, i)[A, B, C, D, E, "format-linear"]slice(...)[...]findLast(...)slice(-6, -5)[A]Aslice(-6, -4)[A, B]Bslice(-6, -3)[A, B, C]Cslice(-6, -2)[A, B, C, D]Dslice(-6, -1)[A, B, C, D, E]E위와 같은 순서대로 순차 포맷됩니다.
하위 parser에 의존하는 플러그인
그러면 여기서 의문이 생길 수 있습니다. 왜 A → B → C → D → E 순서대로 실행하지 않고, 누적해서 실행해야할까요?
그 이유는 몇몇 플러그인들이 하위 플러그인의 parser에 의존하기 때문입니다. 예를 들어
"@xeonlink/prettier-plugin-organize-attributes""prettier-plugin-svelte"Printer - 그대로 통과
parser에서 AST를 만들지 않고, 포맷된 문자열을 바로 만들기 때문에 printer는 그대로 통과시킵니다.
// printer — AST 대신 문자열을 그대로 통과
printers: {
merge: {
print: (astPath) => astPath.node,
},
},한계점 및 정리
format-linear는 Prettier 내부 동작에 깊이 coupling되어 있습니다.
Object.hasOwn(...)플러그인 N개에 대해 최대 N-1번 format()을 중첩 호출하므로, 플러그인이 많을수록 느려집니다. vscode의 format on save로 prettier를 사용할 때는, 하나의 파일만 변경되니까 크게 문제가 없으나,
"prettier --write ."구현은 단일 파일로 두어 프로젝트에 복사해 쓸 수 있게 했습니다. 전체 소스는 아래에서 확인할 수 있습니다.