Skip to content

Commit 74a3f23

Browse files
committed
feat: detect version information from node_modules when not specified in package.json, like pnpm’s catalog
1 parent 20a2f32 commit 74a3f23

File tree

3 files changed

+153
-31
lines changed

3 files changed

+153
-31
lines changed

.changeset/two-poets-wait.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'eslint-plugin-svelte': minor
3+
---
4+
5+
feat: detect version information from node_modules when not specified in package.json, like pnpm’s catalog
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
import { createCache } from './cache.js';
4+
5+
const isRunOnBrowser = !fs.readFileSync;
6+
const nodeModuleCache = createCache<string | null>();
7+
const nodeModulesCache = createCache<string[]>();
8+
9+
/**
10+
* Find package directory in node_modules
11+
*/
12+
function findPackageInNodeModules(dir: string, packageName: string): string | null {
13+
if (isRunOnBrowser) return null;
14+
15+
const nodeModulesPath = path.join(dir, 'node_modules');
16+
const packagePath = path.join(nodeModulesPath, packageName);
17+
18+
try {
19+
const stats = fs.statSync(packagePath);
20+
if (stats.isDirectory()) {
21+
return packagePath;
22+
}
23+
} catch {
24+
// ignore if directory not found
25+
}
26+
27+
return null;
28+
}
29+
30+
/**
31+
* Get first found package path from node_modules
32+
*/
33+
export function getNodeModule(packageName: string, startPath = 'a.js'): string | null {
34+
if (isRunOnBrowser) return null;
35+
36+
const cacheKey = `${startPath}:${packageName}`;
37+
const cached = nodeModulesCache.get(cacheKey);
38+
if (cached) {
39+
return cached[0] ?? null;
40+
}
41+
42+
const startDir = path.dirname(path.resolve(startPath));
43+
let dir = startDir;
44+
let prevDir = '';
45+
46+
do {
47+
// check cache
48+
const cachePath = nodeModuleCache.get(`${dir}:${packageName}`);
49+
if (cachePath) {
50+
if (cachePath !== null) {
51+
nodeModulesCache.set(cacheKey, [cachePath]);
52+
return cachePath;
53+
}
54+
} else {
55+
// search new
56+
const packagePath = findPackageInNodeModules(dir, packageName);
57+
nodeModuleCache.set(`${dir}:${packageName}`, packagePath);
58+
if (packagePath) {
59+
nodeModulesCache.set(cacheKey, [packagePath]);
60+
return packagePath;
61+
}
62+
}
63+
64+
// go to parent
65+
prevDir = dir;
66+
dir = path.resolve(dir, '..');
67+
} while (dir !== prevDir);
68+
69+
nodeModulesCache.set(cacheKey, []);
70+
return null;
71+
}

packages/eslint-plugin-svelte/src/utils/svelte-context.ts

+77-31
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { RuleContext } from '../types.js';
22
import fs from 'fs';
33
import path from 'path';
44
import { getPackageJsons } from './get-package-json.js';
5+
import { getNodeModule } from './get-node-module.js';
56
import { getFilename, getSourceCode } from './compat.js';
67
import { createCache } from './cache.js';
78

@@ -170,6 +171,23 @@ function getSvelteKitContext(
170171

171172
const svelteVersionCache = createCache<SvelteContext['svelteVersion']>();
172173

174+
function checkAndSetSvelteVersion(
175+
version: string,
176+
filePath: string
177+
): SvelteContext['svelteVersion'] | null {
178+
const major = extractMajorVersion(version, false);
179+
if (major == null) {
180+
svelteVersionCache.set(filePath, null);
181+
return null;
182+
}
183+
if (major === '3' || major === '4') {
184+
svelteVersionCache.set(filePath, '3/4');
185+
return '3/4';
186+
}
187+
svelteVersionCache.set(filePath, major as SvelteContext['svelteVersion']);
188+
return major as SvelteContext['svelteVersion'];
189+
}
190+
173191
export function getSvelteVersion(filePath: string): SvelteContext['svelteVersion'] {
174192
const cached = svelteVersionCache.get(filePath);
175193
if (cached) return cached;
@@ -180,20 +198,32 @@ export function getSvelteVersion(filePath: string): SvelteContext['svelteVersion
180198
return '5';
181199
}
182200

201+
const nodeModule = getNodeModule('svelte', filePath);
202+
if (nodeModule) {
203+
try {
204+
const packageJson = JSON.parse(
205+
fs.readFileSync(path.join(nodeModule, 'package.json'), 'utf8')
206+
);
207+
const result = checkAndSetSvelteVersion(packageJson.version, filePath);
208+
if (result != null) {
209+
return result;
210+
}
211+
} catch {
212+
/** do nothing */
213+
}
214+
}
215+
183216
try {
184217
const packageJsons = getPackageJsons(filePath);
185218
for (const packageJson of packageJsons) {
186219
const version = packageJson.dependencies?.svelte ?? packageJson.devDependencies?.svelte;
187220
if (typeof version !== 'string') {
188221
continue;
189222
}
190-
const major = extractMajorVersion(version, false);
191-
if (major === '3' || major === '4') {
192-
svelteVersionCache.set(filePath, '3/4');
193-
return '3/4';
223+
const result = checkAndSetSvelteVersion(version, filePath);
224+
if (result != null) {
225+
return result;
194226
}
195-
svelteVersionCache.set(filePath, major as SvelteContext['svelteVersion']);
196-
return major as SvelteContext['svelteVersion'];
197227
}
198228
} catch {
199229
/** do nothing */
@@ -205,14 +235,15 @@ export function getSvelteVersion(filePath: string): SvelteContext['svelteVersion
205235

206236
const svelteKitVersionCache = createCache<SvelteContext['svelteKitVersion']>();
207237

208-
/**
209-
* Check givin file is under SvelteKit project.
210-
*
211-
* If it runs on browser, it always returns true.
212-
*
213-
* @param filePath A file path.
214-
* @returns
215-
*/
238+
function checkAndSetSvelteKitVersion(
239+
version: string,
240+
filePath: string
241+
): SvelteContext['svelteKitVersion'] {
242+
const major = extractMajorVersion(version, true) as SvelteContext['svelteKitVersion'];
243+
svelteKitVersionCache.set(filePath, major);
244+
return major;
245+
}
246+
216247
function getSvelteKitVersion(filePath: string): SvelteContext['svelteKitVersion'] {
217248
const cached = svelteKitVersionCache.get(filePath);
218249
if (cached) return cached;
@@ -223,27 +254,42 @@ function getSvelteKitVersion(filePath: string): SvelteContext['svelteKitVersion'
223254
return '2';
224255
}
225256

257+
const nodeModule = getNodeModule('@sveltejs/kit', filePath);
258+
if (nodeModule) {
259+
try {
260+
const packageJson = JSON.parse(
261+
fs.readFileSync(path.join(nodeModule, 'package.json'), 'utf8')
262+
);
263+
const result = checkAndSetSvelteKitVersion(packageJson.version, filePath);
264+
if (result != null) {
265+
return result;
266+
}
267+
} catch {
268+
/** do nothing */
269+
}
270+
}
271+
226272
try {
227273
const packageJsons = getPackageJsons(filePath);
228-
if (packageJsons.length === 0) return null;
229-
if (packageJsons[0].name === 'eslint-plugin-svelte') {
230-
// Hack: CI removes `@sveltejs/kit` and it returns false and test failed.
231-
// So always it returns 2 if it runs on the package.
232-
svelteKitVersionCache.set(filePath, '2');
233-
return '2';
234-
}
274+
if (packageJsons.length > 0) {
275+
if (packageJsons[0].name === 'eslint-plugin-svelte') {
276+
// Hack: CI removes `@sveltejs/kit` and it returns false and test failed.
277+
// So always it returns 2 if it runs on the package.
278+
svelteKitVersionCache.set(filePath, '2');
279+
return '2';
280+
}
235281

236-
for (const packageJson of packageJsons) {
237-
const version =
238-
packageJson.dependencies?.['@sveltejs/kit'] ??
239-
packageJson.devDependencies?.['@sveltejs/kit'];
240-
if (typeof version !== 'string') {
241-
svelteKitVersionCache.set(filePath, null);
242-
return null;
282+
for (const packageJson of packageJsons) {
283+
const version =
284+
packageJson.dependencies?.['@sveltejs/kit'] ??
285+
packageJson.devDependencies?.['@sveltejs/kit'];
286+
if (typeof version === 'string') {
287+
const result = checkAndSetSvelteKitVersion(version, filePath);
288+
if (result != null) {
289+
return result;
290+
}
291+
}
243292
}
244-
const major = extractMajorVersion(version, true) as SvelteContext['svelteKitVersion'];
245-
svelteKitVersionCache.set(filePath, major);
246-
return major;
247293
}
248294
} catch {
249295
/** do nothing */

0 commit comments

Comments
 (0)