Skip to content

Commit e3f3592

Browse files
authored
fix: validate package version w/ semver when traversing directories in search of a matching node module (#9424)
1 parent 6f54ec2 commit e3f3592

10 files changed

Lines changed: 23099 additions & 12048 deletions

File tree

.changeset/salty-adults-sin.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"app-builder-lib": patch
3+
---
4+
5+
fix: validate package version when traversing directories in search of a matching node module
Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,73 @@
1-
import * as fs from "fs"
1+
import { exists, log } from "builder-util"
22
import { PackageJson } from "./types"
3+
import * as fs from "fs-extra"
4+
import { resolve } from "path"
35

4-
/**
5-
* Unified cache for all file system and module resolution operations
6-
*/
7-
export type ModuleCache = {
6+
// Type aliases for clarity
7+
type PackageJsonCache = Record<string, Promise<PackageJson>>
8+
type RealPathCache = Record<string, Promise<string>>
9+
type ExistsCache = Record<string, Promise<boolean>>
10+
type LstatCache = Record<string, Promise<fs.Stats>>
11+
type RequireResolveCache = Record<string, Promise<string | null>>
12+
13+
export class ModuleCache {
814
/** Cache for package.json contents (readJson/require) */
9-
packageJson: Map<string, PackageJson>
15+
readonly packageJson: PackageJsonCache
1016
/** Cache for resolved real paths (realpath) */
11-
realPath: Map<string, string>
17+
readonly realPath: RealPathCache
1218
/** Cache for file/directory existence checks */
13-
exists: Map<string, boolean>
19+
readonly exists: ExistsCache
1420
/** Cache for lstat results */
15-
lstat: Map<string, fs.Stats>
21+
readonly lstat: LstatCache
1622
/** Cache for require.resolve results (key: "packageName::fromDir") */
17-
requireResolve: Map<string, { entry: string; packageDir: string } | null>
18-
}
23+
readonly requireResolve: RequireResolveCache
24+
25+
private readonly packageJsonMap: Map<string, PackageJson> = new Map()
26+
private readonly realPathMap: Map<string, string> = new Map()
27+
private readonly existsMap: Map<string, boolean> = new Map()
28+
private readonly lstatMap: Map<string, fs.Stats> = new Map()
29+
private readonly requireResolveMap: Map<string, string | null> = new Map()
30+
31+
constructor() {
32+
this.packageJson = this.createAsyncProxy(this.packageJsonMap, (path: string) => fs.readJson(path))
33+
this.exists = this.createAsyncProxy(this.existsMap, (path: string) => exists(path))
34+
this.lstat = this.createAsyncProxy(this.lstatMap, (path: string) => fs.lstat(path))
35+
this.requireResolve = this.createAsyncProxy(this.requireResolveMap, (path: string) => require.resolve(path))
36+
this.realPath = this.createAsyncProxy(this.realPathMap, async (path: string) => {
37+
const p = resolve(path)
38+
try {
39+
const stats = await this.lstat[p]
40+
if (stats.isSymbolicLink()) {
41+
return await fs.realpath(p)
42+
}
43+
return p
44+
} catch (error: any) {
45+
log.debug({ filePath: p, message: error.message || error.stack }, "error resolving path")
46+
}
47+
return p
48+
})
49+
}
1950

20-
/**
21-
* Creates a new empty ModuleCache instance
22-
*/
23-
export function createModuleCache(): ModuleCache {
24-
return {
25-
packageJson: new Map(),
26-
realPath: new Map(),
27-
exists: new Map(),
28-
lstat: new Map(),
29-
requireResolve: new Map(),
51+
// this allows dot-notation access while still supporting async retrieval
52+
// e.g., cache.packageJson[somePath] returns Promise<PackageJson>
53+
private createAsyncProxy<T>(map: Map<string, T>, compute: (key: string) => T | Promise<T>): Record<string, Promise<T>> {
54+
return new Proxy({} as Record<string, Promise<T>>, {
55+
async get(_, key: string) {
56+
if (map.has(key)) {
57+
return Promise.resolve(map.get(key)!)
58+
}
59+
return await Promise.resolve(compute(key)).then(value => {
60+
map.set(key, value)
61+
return value
62+
})
63+
},
64+
set(_, key: string, value: T) {
65+
map.set(key, value)
66+
return true
67+
},
68+
has(_, key: string) {
69+
return map.has(key)
70+
},
71+
})
3072
}
3173
}

0 commit comments

Comments
 (0)