Skip to content

Commit 01aba4a

Browse files
committed
perf(fmt): avoid loading Prettier Babel plugin
1 parent ebb48ca commit 01aba4a

2 files changed

Lines changed: 218 additions & 18 deletions

File tree

packages/rstack/src/fmt/yukuPlugin.ts

Lines changed: 99 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { parsers as prettierBabelParsers } from 'prettier/plugins/babel';
21
import * as prettierEstreePlugin from 'prettier/plugins/estree';
32
import type { Parser, ParserOptions, Plugin, SupportLanguage } from 'prettier';
43
import {
@@ -18,7 +17,15 @@ const SOURCE_TYPE_COMBINATIONS: SourceType[] = ['module', 'commonjs'];
1817
type Range = [start: number, end: number];
1918

2019
type Locatable = {
20+
__contentEnd?: number;
21+
alternate?: Locatable | null;
22+
body?: Locatable;
23+
consequent?: Locatable;
24+
declaration?: { decorators?: Locatable[] };
25+
declarations?: Locatable[];
26+
decorators?: Locatable[];
2127
end?: number;
28+
label?: Locatable | null;
2229
range?: Range;
2330
start?: number;
2431
type?: string;
@@ -40,9 +47,95 @@ type EstreePlugin = typeof prettierEstreePlugin & {
4047

4148
const estreePlugin = prettierEstreePlugin as EstreePlugin;
4249
const estreePrinter = estreePlugin.printers.estree;
43-
const babelParser = prettierBabelParsers.babel;
44-
const locStart = babelParser.locStart as (node: Locatable) => number;
45-
const locEnd = babelParser.locEnd as (node: Locatable) => number;
50+
51+
const CONTENT_END_NODE_TYPES = new Set([
52+
'ExpressionStatement',
53+
'Directive',
54+
'ImportDeclaration',
55+
'ExportDefaultDeclaration',
56+
'ExportNamedDeclaration',
57+
'ExportAllDeclaration',
58+
'ReturnStatement',
59+
'ThrowStatement',
60+
'DoWhileStatement',
61+
]);
62+
63+
/** Mirrors Prettier's JavaScript location helpers without loading its Babel plugin. */
64+
const locStart = (node: Locatable): number => {
65+
const start = (node.range?.[0] ?? node.start) as number;
66+
const firstDecorator = (node.declaration?.decorators ?? node.decorators)?.[0];
67+
68+
return firstDecorator ? Math.min(locStart(firstDecorator), start) : start;
69+
};
70+
71+
const locEndWithFullText = (node: Locatable): number => (node.range?.[1] ?? node.end) as number;
72+
73+
const locEnd = (node: Locatable): number => {
74+
switch (node.type) {
75+
case 'IfStatement':
76+
return locEnd((node.alternate ?? node.consequent) as Locatable);
77+
78+
case 'ForInStatement':
79+
case 'ForOfStatement':
80+
case 'ForStatement':
81+
case 'LabeledStatement':
82+
case 'WithStatement':
83+
case 'WhileStatement':
84+
return locEnd(node.body as Locatable);
85+
86+
case 'BreakStatement':
87+
return node.label ? locEnd(node.label) : locStart(node) + 'break'.length;
88+
89+
case 'ContinueStatement':
90+
return node.label ? locEnd(node.label) : locStart(node) + 'continue'.length;
91+
92+
case 'DebuggerStatement':
93+
return locStart(node) + 'debugger'.length;
94+
95+
case 'VariableDeclaration':
96+
return locEnd(node.declarations?.at(-1) as Locatable);
97+
98+
default:
99+
return CONTENT_END_NODE_TYPES.has(node.type ?? '')
100+
? (node.__contentEnd ?? locEndWithFullText(node))
101+
: locEndWithFullText(node);
102+
}
103+
};
104+
105+
const DOCBLOCK_REGEXP = /^\s*(\/\*\*?(.|\r?\n)*?\*\/)/;
106+
const COMMENT_END_REGEXP = /\*\/$/;
107+
const COMMENT_START_REGEXP = /^\/\*\*?/;
108+
const DOCBLOCK_LINE_START_REGEXP = /(\r?\n|^) *\* ?/g;
109+
const PRAGMA_REGEXP = /(?:^|\r?\n) *@(\S+) *([^\n\r]*)/g;
110+
const FORMAT_PRAGMAS = new Set(['format', 'prettier']);
111+
const FORMAT_IGNORE_PRAGMAS = new Set(['noformat', 'noprettier']);
112+
113+
/** Matches Prettier's leading JavaScript docblock pragma handling. */
114+
const hasPragmaFrom = (originalText: string, pragmas: Set<string>): boolean => {
115+
let text = originalText;
116+
117+
if (text.startsWith('#!')) {
118+
const lineEnd = text.indexOf('\n');
119+
text = text.slice((lineEnd === -1 ? text.length : lineEnd) + 1);
120+
}
121+
122+
const docblock = (text.match(DOCBLOCK_REGEXP)?.[0] ?? '')
123+
.trimStart()
124+
.replace(COMMENT_START_REGEXP, '')
125+
.replace(COMMENT_END_REGEXP, '')
126+
.replaceAll(DOCBLOCK_LINE_START_REGEXP, '$1');
127+
128+
for (const match of docblock.matchAll(PRAGMA_REGEXP)) {
129+
if (pragmas.has(match[1])) {
130+
return true;
131+
}
132+
}
133+
134+
return false;
135+
};
136+
137+
const hasPragma = (text: string): boolean => hasPragmaFrom(text, FORMAT_PRAGMAS);
138+
const hasIgnorePragma = (text: string): boolean => hasPragmaFrom(text, FORMAT_IGNORE_PRAGMAS);
46139

47140
const getVisitorKeys = estreePrinter.getVisitorKeys as ((node: AstNode) => string[]) | undefined;
48141

@@ -118,18 +211,6 @@ const stripComments = (originalText: string, comments: PrettierComment[]): strin
118211
return text;
119212
};
120213

121-
const CONTENT_END_NODE_TYPES = new Set([
122-
'ExpressionStatement',
123-
'Directive',
124-
'ImportDeclaration',
125-
'ExportDefaultDeclaration',
126-
'ExportNamedDeclaration',
127-
'ExportAllDeclaration',
128-
'ReturnStatement',
129-
'ThrowStatement',
130-
'DoWhileStatement',
131-
]);
132-
133214
const setContentEnd = (
134215
node: AstNode,
135216
originalText: string,
@@ -437,8 +518,8 @@ const createParser = (
437518
parse: (text: string, options: ParserOptions<AstNode>) => AstNode,
438519
): Parser<AstNode> => ({
439520
astFormat: AST_FORMAT,
440-
hasIgnorePragma: babelParser.hasIgnorePragma,
441-
hasPragma: babelParser.hasPragma,
521+
hasIgnorePragma,
522+
hasPragma,
442523
locEnd,
443524
locStart,
444525
parse,

packages/rstack/tests/fmt/yukuPlugin.test.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,125 @@ test('reuses Prettier options and pragma handling', async () => {
110110
).resolves.toBe('/** @noformat */\nconst value={answer:"yes"}');
111111
});
112112

113+
test.each([
114+
{
115+
source: '/** @prettier */\nconst value=1',
116+
hasPragma: true,
117+
hasIgnorePragma: false,
118+
},
119+
{
120+
source: '/* @format */\nconst value=1',
121+
hasPragma: true,
122+
hasIgnorePragma: false,
123+
},
124+
{
125+
source: '#!/usr/bin/env node\r\n/** @format */\r\nconst value=1',
126+
hasPragma: true,
127+
hasIgnorePragma: false,
128+
},
129+
{
130+
source: '/**\n * @prettier\n * @noformat\n */\nconst value=1',
131+
hasPragma: true,
132+
hasIgnorePragma: true,
133+
},
134+
{
135+
source: '/** @prettier @noformat */\nconst value=1',
136+
hasPragma: true,
137+
hasIgnorePragma: false,
138+
},
139+
{
140+
source: '/** text @prettier */\nconst value=1',
141+
hasPragma: false,
142+
hasIgnorePragma: false,
143+
},
144+
{
145+
source: '// before\n/** @prettier */\nconst value=1',
146+
hasPragma: false,
147+
hasIgnorePragma: false,
148+
},
149+
])('matches Prettier pragma detection for $source', ({ source, hasPragma, hasIgnorePragma }) => {
150+
const parser = yukuPlugin.parsers?.yuku;
151+
if (!parser?.hasPragma || !parser.hasIgnorePragma) {
152+
throw new Error('The Yuku parser does not expose pragma handlers.');
153+
}
154+
155+
expect(parser.hasPragma(source)).toBe(hasPragma);
156+
expect(parser.hasIgnorePragma(source)).toBe(hasIgnorePragma);
157+
});
158+
159+
test('matches Prettier JavaScript location overrides', () => {
160+
const parser = yukuPlugin.parsers?.yuku;
161+
if (!parser) {
162+
throw new Error('The Yuku parser is not registered.');
163+
}
164+
165+
expect(
166+
parser.locStart({
167+
type: 'ClassDeclaration',
168+
range: [10, 80],
169+
decorators: [{ type: 'Decorator', range: [2, 9] }],
170+
}),
171+
).toBe(2);
172+
173+
expect(
174+
parser.locStart({
175+
type: 'ExportNamedDeclaration',
176+
range: [10, 80],
177+
declaration: { decorators: [{ type: 'Decorator', range: [2, 9] }] },
178+
}),
179+
).toBe(2);
180+
181+
const endCases = [
182+
{
183+
expected: 44,
184+
node: {
185+
type: 'IfStatement',
186+
range: [0, 50],
187+
consequent: { type: 'BlockStatement', range: [3, 20] },
188+
alternate: { type: 'BlockStatement', range: [21, 44] },
189+
},
190+
},
191+
{
192+
expected: 45,
193+
node: {
194+
type: 'ForStatement',
195+
range: [0, 50],
196+
body: { type: 'BlockStatement', range: [20, 45] },
197+
},
198+
},
199+
{ expected: 15, node: { type: 'BreakStatement', range: [10, 50] } },
200+
{
201+
expected: 21,
202+
node: {
203+
type: 'BreakStatement',
204+
range: [10, 50],
205+
label: { type: 'Identifier', range: [16, 21] },
206+
},
207+
},
208+
{ expected: 18, node: { type: 'ContinueStatement', range: [10, 50] } },
209+
{ expected: 18, node: { type: 'DebuggerStatement', range: [10, 50] } },
210+
{
211+
expected: 22,
212+
node: {
213+
type: 'VariableDeclaration',
214+
range: [0, 30],
215+
declarations: [
216+
{ type: 'VariableDeclarator', range: [4, 10] },
217+
{ type: 'VariableDeclarator', range: [12, 22] },
218+
],
219+
},
220+
},
221+
{
222+
expected: 10,
223+
node: { type: 'ExpressionStatement', range: [0, 12], __contentEnd: 10 },
224+
},
225+
];
226+
227+
for (const { node, expected } of endCases) {
228+
expect(parser.locEnd(node)).toBe(expected);
229+
}
230+
});
231+
113232
test('supports CommonJS source semantics for .cjs files', async () => {
114233
await expect(
115234
formatWithYuku('return require("example")', {

0 commit comments

Comments
 (0)