Skip to content

Commit 173b5be

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

File tree

3 files changed

+138
-32
lines changed

3 files changed

+138
-32
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

+62-32
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,19 @@ function getSvelteKitContext(
170171

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

174+
function checkAndSetSvelteVersion(
175+
version: string,
176+
filePath: string
177+
): SvelteContext['svelteVersion'] {
178+
const major = extractMajorVersion(version, false);
179+
if (major === '3' || major === '4') {
180+
svelteVersionCache.set(filePath, '3/4');
181+
return '3/4';
182+
}
183+
svelteVersionCache.set(filePath, major as SvelteContext['svelteVersion']);
184+
return major as SvelteContext['svelteVersion'];
185+
}
186+
173187
export function getSvelteVersion(filePath: string): SvelteContext['svelteVersion'] {
174188
const cached = svelteVersionCache.get(filePath);
175189
if (cached) return cached;
@@ -187,32 +201,39 @@ export function getSvelteVersion(filePath: string): SvelteContext['svelteVersion
187201
if (typeof version !== 'string') {
188202
continue;
189203
}
190-
const major = extractMajorVersion(version, false);
191-
if (major === '3' || major === '4') {
192-
svelteVersionCache.set(filePath, '3/4');
193-
return '3/4';
194-
}
195-
svelteVersionCache.set(filePath, major as SvelteContext['svelteVersion']);
196-
return major as SvelteContext['svelteVersion'];
204+
return checkAndSetSvelteVersion(version, filePath);
197205
}
198206
} catch {
199207
/** do nothing */
200208
}
201209

210+
const nodeModule = getNodeModule('svelte', filePath);
211+
if (nodeModule) {
212+
try {
213+
const packageJson = JSON.parse(
214+
fs.readFileSync(path.join(nodeModule, 'package.json'), 'utf8')
215+
);
216+
return checkAndSetSvelteVersion(packageJson.version, filePath);
217+
} catch {
218+
/** do nothing */
219+
}
220+
}
221+
202222
svelteVersionCache.set(filePath, null);
203223
return null;
204224
}
205225

206226
const svelteKitVersionCache = createCache<SvelteContext['svelteKitVersion']>();
207227

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-
*/
228+
function checkAndSetSvelteKitVersion(
229+
version: string,
230+
filePath: string
231+
): SvelteContext['svelteKitVersion'] {
232+
const major = extractMajorVersion(version, true) as SvelteContext['svelteKitVersion'];
233+
svelteKitVersionCache.set(filePath, major);
234+
return major;
235+
}
236+
216237
function getSvelteKitVersion(filePath: string): SvelteContext['svelteKitVersion'] {
217238
const cached = svelteKitVersionCache.get(filePath);
218239
if (cached) return cached;
@@ -225,30 +246,39 @@ function getSvelteKitVersion(filePath: string): SvelteContext['svelteKitVersion'
225246

226247
try {
227248
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-
}
249+
if (packageJsons.length > 0) {
250+
if (packageJsons[0].name === 'eslint-plugin-svelte') {
251+
// Hack: CI removes `@sveltejs/kit` and it returns false and test failed.
252+
// So always it returns 2 if it runs on the package.
253+
svelteKitVersionCache.set(filePath, '2');
254+
return '2';
255+
}
235256

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;
257+
for (const packageJson of packageJsons) {
258+
const version =
259+
packageJson.dependencies?.['@sveltejs/kit'] ??
260+
packageJson.devDependencies?.['@sveltejs/kit'];
261+
if (typeof version === 'string') {
262+
return checkAndSetSvelteKitVersion(version, filePath);
263+
}
243264
}
244-
const major = extractMajorVersion(version, true) as SvelteContext['svelteKitVersion'];
245-
svelteKitVersionCache.set(filePath, major);
246-
return major;
247265
}
248266
} catch {
249267
/** do nothing */
250268
}
251269

270+
const nodeModule = getNodeModule('@sveltejs/kit', filePath);
271+
if (nodeModule) {
272+
try {
273+
const packageJson = JSON.parse(
274+
fs.readFileSync(path.join(nodeModule, 'package.json'), 'utf8')
275+
);
276+
return checkAndSetSvelteKitVersion(packageJson.version, filePath);
277+
} catch {
278+
/** do nothing */
279+
}
280+
}
281+
252282
svelteKitVersionCache.set(filePath, null);
253283
return null;
254284
}

0 commit comments

Comments
 (0)