Skip to content

Commit e4195ae

Browse files
committed
perf: compat
Cache the isUsingTranspiler per directory
1 parent 6388a9b commit e4195ae

2 files changed

Lines changed: 190 additions & 17 deletions

File tree

src/rules/compat.ts

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Rule } from "eslint";
99
import findUp from "find-up";
1010
import fs from "fs";
1111
import memoize from "lodash.memoize";
12+
import path from "path";
1213
import {
1314
determineTargetsFromConfig,
1415
lintCallExpression,
@@ -91,23 +92,27 @@ const babelConfigs = [
9192

9293
/**
9394
* Determine if a user has a babel config, which we use to infer if the linted code is polyfilled.
95+
* Memoized by directory so multiple files in the same project reuse the result.
9496
*/
95-
function isUsingTranspiler(context: Context): boolean {
96-
const dir = context.filename ?? context.getFilename();
97-
const configPath = findUp.sync(babelConfigs, {
98-
cwd: dir,
99-
});
100-
if (configPath) return true;
101-
const pkgPath = findUp.sync("package.json", {
102-
cwd: dir,
103-
});
104-
// Check if babel property exists
105-
if (pkgPath) {
106-
const pkg = JSON.parse(fs.readFileSync(pkgPath).toString());
107-
return !!pkg.babel;
108-
}
109-
return false;
110-
}
97+
const isUsingTranspiler = memoize(
98+
(filePath: string): boolean => {
99+
const dir = path.dirname(filePath);
100+
const configPath = findUp.sync(babelConfigs, {
101+
cwd: dir,
102+
});
103+
if (configPath) return true;
104+
const pkgPath = findUp.sync("package.json", {
105+
cwd: dir,
106+
});
107+
// Check if babel property exists
108+
if (pkgPath) {
109+
const pkg = JSON.parse(fs.readFileSync(pkgPath).toString());
110+
return !!pkg.babel;
111+
}
112+
return false;
113+
},
114+
(filePath: string) => path.resolve(path.dirname(filePath))
115+
);
111116

112117
type RulesFilteredByTargets = {
113118
CallExpression: AstMetadataApiWithTargetsResolver[];
@@ -182,7 +187,7 @@ export default {
182187
context.settings?.lintAllEsApis === true ||
183188
// Attempt to infer polyfilling of ES APIs from babel config
184189
(!context.settings?.polyfills?.includes("es:all") &&
185-
!isUsingTranspiler(context));
190+
!isUsingTranspiler(context.filename ?? context.getFilename()));
186191
const browserslistTargets = parseBrowsersListVersion(
187192
determineTargetsFromConfig(
188193
context.getFilename(),
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/**
2+
* Tests for isUsingTranspiler behavior: babel config detection, memoization by directory,
3+
* and backwards compatibility with file path (not directory) input.
4+
*/
5+
import { promises as fs } from "fs";
6+
import os from "os";
7+
import path from "path";
8+
import { ESLint } from "eslint";
9+
import compat from "../src";
10+
11+
describe("isUsingTranspiler (babel config detection)", () => {
12+
const eslintBaseConfig = {
13+
overrideConfigFile: true,
14+
ignore: false,
15+
baseConfig: [
16+
compat.configs["flat/recommended"],
17+
{
18+
languageOptions: {
19+
parserOptions: { ecmaVersion: 2022, sourceType: "module" },
20+
},
21+
settings: {
22+
browsers: ["ie 10"],
23+
},
24+
},
25+
],
26+
};
27+
28+
const codeWithEsApi = "Array.from([1, 2, 3]);";
29+
30+
it("does not report ES APIs when babel config exists in directory (polyfilled)", async () => {
31+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "compat-babel-"));
32+
try {
33+
await fs.writeFile(
34+
path.join(tmpDir, "babel.config.json"),
35+
JSON.stringify({ presets: ["@babel/env"] })
36+
);
37+
const filePath = path.join(tmpDir, "src", "index.js");
38+
await fs.mkdir(path.dirname(filePath), { recursive: true });
39+
40+
// @ts-expect-error Bug? ESLint flat config types
41+
const eslint = new ESLint({
42+
...eslintBaseConfig,
43+
cwd: tmpDir,
44+
});
45+
const results = await eslint.lintText(codeWithEsApi, {
46+
filePath,
47+
});
48+
49+
expect(results[0].messages).toHaveLength(0);
50+
} finally {
51+
await fs.rm(tmpDir, { recursive: true, force: true });
52+
}
53+
});
54+
55+
it("reports ES APIs when lintAllEsApis is true (backwards compatible behavior)", async () => {
56+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "compat-lintAllEsApis-"));
57+
try {
58+
const filePath = path.join(tmpDir, "index.js");
59+
await fs.writeFile(filePath, codeWithEsApi);
60+
61+
const eslint = new ESLint({
62+
...eslintBaseConfig,
63+
baseConfig: [
64+
compat.configs["flat/recommended"],
65+
{
66+
languageOptions: {
67+
parserOptions: { ecmaVersion: 2022, sourceType: "module" },
68+
},
69+
settings: {
70+
browsers: ["ie 8"],
71+
lintAllEsApis: true,
72+
},
73+
},
74+
],
75+
overrideConfigFile: true,
76+
ignore: false,
77+
cwd: tmpDir,
78+
});
79+
const results = await eslint.lintFiles([filePath]);
80+
81+
expect(results[0].messages.length).toBeGreaterThan(0);
82+
expect(results[0].messages[0].ruleId).toBe("compat/compat");
83+
} finally {
84+
await fs.rm(tmpDir, { recursive: true, force: true });
85+
}
86+
});
87+
88+
it("accepts file path (not directory) and correctly infers directory via path.dirname", async () => {
89+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "compat-filepath-"));
90+
try {
91+
await fs.writeFile(
92+
path.join(tmpDir, "babel.config.json"),
93+
JSON.stringify({ presets: ["@babel/env"] })
94+
);
95+
const filePath = path.join(tmpDir, "deep", "nested", "file.js");
96+
await fs.mkdir(path.dirname(filePath), { recursive: true });
97+
98+
// @ts-expect-error Bug? ESLint flat config types
99+
const eslint = new ESLint({
100+
...eslintBaseConfig,
101+
cwd: tmpDir,
102+
});
103+
const results = await eslint.lintText(codeWithEsApi, {
104+
filePath,
105+
});
106+
107+
expect(results[0].messages).toHaveLength(0);
108+
} finally {
109+
await fs.rm(tmpDir, { recursive: true, force: true });
110+
}
111+
});
112+
113+
it("memoizes by directory: multiple files in same directory share cached result", async () => {
114+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "compat-memo-"));
115+
try {
116+
await fs.writeFile(
117+
path.join(tmpDir, "babel.config.json"),
118+
JSON.stringify({ presets: ["@babel/env"] })
119+
);
120+
const file1 = path.join(tmpDir, "src", "a.js");
121+
const file2 = path.join(tmpDir, "src", "b.js");
122+
await fs.mkdir(path.dirname(file1), { recursive: true });
123+
124+
// @ts-expect-error Bug? ESLint flat config types
125+
const eslint = new ESLint({
126+
...eslintBaseConfig,
127+
cwd: tmpDir,
128+
});
129+
const [results1, results2] = await Promise.all([
130+
eslint.lintText(codeWithEsApi, { filePath: file1 }),
131+
eslint.lintText(codeWithEsApi, { filePath: file2 }),
132+
]);
133+
134+
expect(results1[0].messages).toHaveLength(0);
135+
expect(results2[0].messages).toHaveLength(0);
136+
} finally {
137+
await fs.rm(tmpDir, { recursive: true, force: true });
138+
}
139+
});
140+
141+
it("detects babel via package.json babel property when no babel config file", async () => {
142+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "compat-pkgbabel-"));
143+
try {
144+
await fs.writeFile(
145+
path.join(tmpDir, "package.json"),
146+
JSON.stringify({
147+
name: "test",
148+
version: "1.0.0",
149+
babel: { presets: ["@babel/env"] },
150+
})
151+
);
152+
const filePath = path.join(tmpDir, "index.js");
153+
154+
// @ts-expect-error Bug? ESLint flat config types
155+
const eslint = new ESLint({
156+
...eslintBaseConfig,
157+
cwd: tmpDir,
158+
});
159+
const results = await eslint.lintText(codeWithEsApi, {
160+
filePath,
161+
});
162+
163+
expect(results[0].messages).toHaveLength(0);
164+
} finally {
165+
await fs.rm(tmpDir, { recursive: true, force: true });
166+
}
167+
});
168+
});

0 commit comments

Comments
 (0)