1
1
import fs from 'node:fs'
2
2
import path from 'node:path'
3
3
import { createRequire } from 'node:module'
4
- import { createDebugger , createFilter , safeRealpathSync } from './utils'
4
+ import { createFilter , safeRealpathSync } from './utils'
5
5
import type { ResolvedConfig } from './config'
6
6
import type { Plugin } from './plugin'
7
7
@@ -13,11 +13,6 @@ if (process.versions.pnp) {
13
13
} catch { }
14
14
}
15
15
16
- const isDebug = process . env . DEBUG
17
- const debug = createDebugger ( 'vite:resolve-details' , {
18
- onlyWhenFocused : true ,
19
- } )
20
-
21
16
/** Cache for package.json resolution and package.json contents */
22
17
export type PackageCache = Map < string , PackageData >
23
18
@@ -56,49 +51,99 @@ export function invalidatePackageData(
56
51
}
57
52
58
53
export function resolvePackageData (
59
- id : string ,
54
+ pkgName : string ,
60
55
basedir : string ,
61
56
preserveSymlinks = false ,
62
57
packageCache ?: PackageCache ,
63
58
) : PackageData | null {
64
- let pkg : PackageData | undefined
65
- let cacheKey : string | undefined
66
- if ( packageCache ) {
67
- cacheKey = `${ id } &${ basedir } &${ preserveSymlinks } `
68
- if ( ( pkg = packageCache . get ( cacheKey ) ) ) {
69
- return pkg
70
- }
59
+ if ( pnp ) {
60
+ const cacheKey = getRpdCacheKey ( pkgName , basedir , preserveSymlinks )
61
+ if ( packageCache ?. has ( cacheKey ) ) return packageCache . get ( cacheKey ) !
62
+
63
+ const pkg = pnp . resolveToUnqualified ( pkgName , basedir )
64
+ if ( ! pkg ) return null
65
+
66
+ const pkgData = loadPackageData ( path . join ( pkg , 'package.json' ) )
67
+ packageCache ?. set ( cacheKey , pkgData )
68
+
69
+ return pkgData
71
70
}
72
- const pkgPath = resolvePkgJsonPath ( id , basedir , preserveSymlinks )
73
- if ( ! pkgPath ) return null
74
- try {
75
- pkg = loadPackageData ( pkgPath , true , packageCache )
71
+
72
+ const originalBasedir = basedir
73
+ while ( basedir ) {
76
74
if ( packageCache ) {
77
- packageCache . set ( cacheKey ! , pkg )
78
- }
79
- return pkg
80
- } catch ( e ) {
81
- if ( e instanceof SyntaxError ) {
82
- isDebug && debug ( `Parsing failed: ${ pkgPath } ` )
75
+ const cached = getRpdCache (
76
+ packageCache ,
77
+ pkgName ,
78
+ basedir ,
79
+ originalBasedir ,
80
+ preserveSymlinks ,
81
+ )
82
+ if ( cached ) return cached
83
83
}
84
- throw e
84
+
85
+ const pkg = path . join ( basedir , 'node_modules' , pkgName , 'package.json' )
86
+ try {
87
+ if ( fs . existsSync ( pkg ) ) {
88
+ const pkgPath = preserveSymlinks ? pkg : safeRealpathSync ( pkg )
89
+ const pkgData = loadPackageData ( pkgPath )
90
+
91
+ if ( packageCache ) {
92
+ setRpdCache (
93
+ packageCache ,
94
+ pkgData ,
95
+ pkgName ,
96
+ basedir ,
97
+ originalBasedir ,
98
+ preserveSymlinks ,
99
+ )
100
+ }
101
+
102
+ return pkgData
103
+ }
104
+ } catch { }
105
+
106
+ const nextBasedir = path . dirname ( basedir )
107
+ if ( nextBasedir === basedir ) break
108
+ basedir = nextBasedir
85
109
}
110
+
111
+ return null
86
112
}
87
113
88
- export function loadPackageData (
89
- pkgPath : string ,
90
- preserveSymlinks ?: boolean ,
114
+ export function findNearestPackageData (
115
+ basedir : string ,
91
116
packageCache ?: PackageCache ,
92
- ) : PackageData {
93
- if ( ! preserveSymlinks ) {
94
- pkgPath = safeRealpathSync ( pkgPath )
95
- }
117
+ ) : PackageData | null {
118
+ const originalBasedir = basedir
119
+ while ( basedir ) {
120
+ if ( packageCache ) {
121
+ const cached = getFnpdCache ( packageCache , basedir , originalBasedir )
122
+ if ( cached ) return cached
123
+ }
124
+
125
+ const pkgPath = path . join ( basedir , 'package.json' )
126
+ try {
127
+ if ( fs . statSync ( pkgPath , { throwIfNoEntry : false } ) ?. isFile ( ) ) {
128
+ const pkgData = loadPackageData ( pkgPath )
129
+
130
+ if ( packageCache ) {
131
+ setFnpdCache ( packageCache , pkgData , basedir , originalBasedir )
132
+ }
96
133
97
- let cached : PackageData | undefined
98
- if ( ( cached = packageCache ?. get ( pkgPath ) ) ) {
99
- return cached
134
+ return pkgData
135
+ }
136
+ } catch { }
137
+
138
+ const nextBasedir = path . dirname ( basedir )
139
+ if ( nextBasedir === basedir ) break
140
+ basedir = nextBasedir
100
141
}
101
142
143
+ return null
144
+ }
145
+
146
+ export function loadPackageData ( pkgPath : string ) : PackageData {
102
147
const data = JSON . parse ( fs . readFileSync ( pkgPath , 'utf-8' ) )
103
148
const pkgDir = path . dirname ( pkgPath )
104
149
const { sideEffects } = data
@@ -147,7 +192,6 @@ export function loadPackageData(
147
192
} ,
148
193
}
149
194
150
- packageCache ?. set ( pkgPath , pkg )
151
195
return pkg
152
196
}
153
197
@@ -184,29 +228,104 @@ export function watchPackageDataPlugin(config: ResolvedConfig): Plugin {
184
228
}
185
229
}
186
230
187
- export function resolvePkgJsonPath (
231
+ /**
232
+ * Get cached `resolvePackageData` value based on `basedir`. When one is found,
233
+ * and we've already traversed some directories between `basedir` and `originalBasedir`,
234
+ * we cache the value for those in-between directories as well.
235
+ *
236
+ * This makes it so the fs is only read once for a shared `basedir`.
237
+ */
238
+ function getRpdCache (
239
+ packageCache : PackageCache ,
188
240
pkgName : string ,
189
241
basedir : string ,
190
- preserveSymlinks = false ,
191
- ) : string | undefined {
192
- if ( pnp ) {
193
- const pkg = pnp . resolveToUnqualified ( pkgName , basedir )
194
- if ( ! pkg ) return undefined
195
- return path . join ( pkg , 'package.json' )
242
+ originalBasedir : string ,
243
+ preserveSymlinks : boolean ,
244
+ ) {
245
+ const cacheKey = getRpdCacheKey ( pkgName , basedir , preserveSymlinks )
246
+ const pkgData = packageCache . get ( cacheKey )
247
+ if ( pkgData ) {
248
+ traverseBetweenDirs ( originalBasedir , basedir , ( dir ) => {
249
+ packageCache . set ( getRpdCacheKey ( pkgName , dir , preserveSymlinks ) , pkgData )
250
+ } )
251
+ return pkgData
196
252
}
253
+ }
197
254
198
- let root = basedir
199
- while ( root ) {
200
- const pkg = path . join ( root , 'node_modules' , pkgName , 'package.json' )
201
- try {
202
- if ( fs . existsSync ( pkg ) ) {
203
- return preserveSymlinks ? pkg : safeRealpathSync ( pkg )
204
- }
205
- } catch { }
206
- const nextRoot = path . dirname ( root )
207
- if ( nextRoot === root ) break
208
- root = nextRoot
255
+ function setRpdCache (
256
+ packageCache : PackageCache ,
257
+ pkgData : PackageData ,
258
+ pkgName : string ,
259
+ basedir : string ,
260
+ originalBasedir : string ,
261
+ preserveSymlinks : boolean ,
262
+ ) {
263
+ packageCache . set ( getRpdCacheKey ( pkgName , basedir , preserveSymlinks ) , pkgData )
264
+ traverseBetweenDirs ( originalBasedir , basedir , ( dir ) => {
265
+ packageCache . set ( getRpdCacheKey ( pkgName , dir , preserveSymlinks ) , pkgData )
266
+ } )
267
+ }
268
+
269
+ // package cache key for `resolvePackageData`
270
+ function getRpdCacheKey (
271
+ pkgName : string ,
272
+ basedir : string ,
273
+ preserveSymlinks : boolean ,
274
+ ) {
275
+ return `rpd_${ pkgName } _${ basedir } _${ preserveSymlinks } `
276
+ }
277
+
278
+ /**
279
+ * Get cached `findNearestPackageData` value based on `basedir`. When one is found,
280
+ * and we've already traversed some directories between `basedir` and `originalBasedir`,
281
+ * we cache the value for those in-between directories as well.
282
+ *
283
+ * This makes it so the fs is only read once for a shared `basedir`.
284
+ */
285
+ function getFnpdCache (
286
+ packageCache : PackageCache ,
287
+ basedir : string ,
288
+ originalBasedir : string ,
289
+ ) {
290
+ const cacheKey = getFnpdCacheKey ( basedir )
291
+ const pkgData = packageCache . get ( cacheKey )
292
+ if ( pkgData ) {
293
+ traverseBetweenDirs ( originalBasedir , basedir , ( dir ) => {
294
+ packageCache . set ( getFnpdCacheKey ( dir ) , pkgData )
295
+ } )
296
+ return pkgData
209
297
}
298
+ }
210
299
211
- return undefined
300
+ function setFnpdCache (
301
+ packageCache : PackageCache ,
302
+ pkgData : PackageData ,
303
+ basedir : string ,
304
+ originalBasedir : string ,
305
+ ) {
306
+ packageCache . set ( getFnpdCacheKey ( basedir ) , pkgData )
307
+ traverseBetweenDirs ( originalBasedir , basedir , ( dir ) => {
308
+ packageCache . set ( getFnpdCacheKey ( dir ) , pkgData )
309
+ } )
310
+ }
311
+
312
+ // package cache key for `findNearestPackageData`
313
+ function getFnpdCacheKey ( basedir : string ) {
314
+ return `fnpd_${ basedir } `
315
+ }
316
+
317
+ /**
318
+ * Traverse between `longerDir` (inclusive) and `shorterDir` (exclusive) and call `cb` for each dir.
319
+ * @param longerDir Longer dir path, e.g. `/User/foo/bar/baz`
320
+ * @param shorterDir Shorter dir path, e.g. `/User/foo`
321
+ */
322
+ function traverseBetweenDirs (
323
+ longerDir : string ,
324
+ shorterDir : string ,
325
+ cb : ( dir : string ) => void ,
326
+ ) {
327
+ while ( longerDir !== shorterDir ) {
328
+ cb ( longerDir )
329
+ longerDir = path . dirname ( longerDir )
330
+ }
212
331
}
0 commit comments