2
2
* base on @volar/jsdelivr
3
3
* MIT License https://github.com/volarjs/volar.js/blob/master/packages/jsdelivr/LICENSE
4
4
*/
5
- import type { FileSystem , FileType } from '@volar/language-service'
5
+ import type { FileStat , FileSystem , FileType } from '@volar/language-service'
6
6
import type { URI } from 'vscode-uri'
7
7
8
8
const textCache = new Map < string , Promise < string | undefined > > ( )
@@ -20,17 +20,8 @@ export function createNpmFileSystem(
20
20
onFetch ?: ( path : string , content : string ) => void ,
21
21
) : FileSystem {
22
22
const fetchResults = new Map < string , Promise < string | undefined > > ( )
23
- const flatResults = new Map <
24
- string ,
25
- Promise <
26
- {
27
- name : string
28
- size : number
29
- time : string
30
- hash : string
31
- } [ ]
32
- >
33
- > ( )
23
+ const statCache = new Map < string , { type : FileType } > ( )
24
+ const dirCache = new Map < string , [ string , FileType ] [ ] > ( )
34
25
35
26
return {
36
27
async stat ( uri ) {
@@ -65,7 +56,16 @@ export function createNpmFileSystem(
65
56
}
66
57
67
58
async function _stat ( path : string ) {
68
- const [ modName , pkgName , pkgVersion , pkgFilePath ] = resolvePackageName ( path )
59
+ if ( statCache . has ( path ) ) {
60
+ return {
61
+ ...statCache . get ( path ) ,
62
+ ctime : - 1 ,
63
+ mtime : - 1 ,
64
+ size : - 1 ,
65
+ } as FileStat
66
+ }
67
+
68
+ const [ modName , pkgName , , pkgFilePath ] = resolvePackageName ( path )
69
69
if ( ! pkgName ) {
70
70
if ( modName . startsWith ( '@' ) ) {
71
71
return {
@@ -82,72 +82,111 @@ export function createNpmFileSystem(
82
82
return
83
83
}
84
84
85
- if ( ! pkgFilePath ) {
86
- // perf: skip flat request
87
- return {
88
- type : 2 satisfies FileType . Directory ,
89
- ctime : - 1 ,
90
- mtime : - 1 ,
91
- size : - 1 ,
85
+ if ( ! pkgFilePath || pkgFilePath === '/' ) {
86
+ const result = {
87
+ type : 2 as FileType . Directory ,
92
88
}
89
+ statCache . set ( path , result )
90
+ return { ...result , ctime : - 1 , mtime : - 1 , size : - 1 }
93
91
}
94
92
95
- if ( ! flatResults . has ( modName ) ) {
96
- flatResults . set ( modName , flat ( pkgName , pkgVersion ) )
97
- }
93
+ try {
94
+ const parentDir = path . substring ( 0 , path . lastIndexOf ( '/' ) )
95
+ const fileName = path . substring ( path . lastIndexOf ( '/' ) + 1 )
98
96
99
- const flatResult = await flatResults . get ( modName ) !
100
- const filePath = path . slice ( modName . length )
101
- const file = flatResult . find ( ( file ) => file . name === filePath )
102
- if ( file ) {
103
- return {
104
- type : 1 satisfies FileType . File ,
105
- ctime : new Date ( file . time ) . valueOf ( ) ,
106
- mtime : new Date ( file . time ) . valueOf ( ) ,
107
- size : file . size ,
108
- }
109
- } else if (
110
- flatResult . some ( ( file ) => file . name . startsWith ( filePath + '/' ) )
111
- ) {
112
- return {
113
- type : 2 satisfies FileType . Directory ,
114
- ctime : - 1 ,
115
- mtime : - 1 ,
116
- size : - 1 ,
97
+ const dirContent = await _readDirectory ( parentDir )
98
+ const fileEntry = dirContent . find ( ( [ name ] ) => name === fileName )
99
+
100
+ if ( fileEntry ) {
101
+ const result = {
102
+ type : fileEntry [ 1 ] as FileType ,
103
+ }
104
+ statCache . set ( path , result )
105
+ return { ...result , ctime : - 1 , mtime : - 1 , size : - 1 }
117
106
}
107
+
108
+ return
109
+ } catch {
110
+ return
118
111
}
119
112
}
120
113
121
114
async function _readDirectory ( path : string ) : Promise < [ string , FileType ] [ ] > {
122
- const [ modName , pkgName , pkgVersion ] = resolvePackageName ( path )
115
+ if ( dirCache . has ( path ) ) {
116
+ return dirCache . get ( path ) !
117
+ }
118
+
119
+ const [ , pkgName , pkgVersion , pkgPath ] = resolvePackageName ( path )
120
+
123
121
if ( ! pkgName || ! ( await isValidPackageName ( pkgName ) ) ) {
124
122
return [ ]
125
123
}
126
124
127
- if ( ! flatResults . has ( modName ) ) {
128
- flatResults . set ( modName , flat ( pkgName , pkgVersion ) )
125
+ const resolvedVersion = pkgVersion || 'latest'
126
+
127
+ let actualVersion = resolvedVersion
128
+ if ( resolvedVersion === 'latest' ) {
129
+ try {
130
+ const data = await fetchJson < { version : string } > (
131
+ `https://unpkg.com/${ pkgName } @${ resolvedVersion } /package.json` ,
132
+ )
133
+ if ( data ?. version ) {
134
+ actualVersion = data . version
135
+ }
136
+ } catch {
137
+ // ignore
138
+ }
129
139
}
130
140
131
- const flatResult = await flatResults . get ( modName ) !
132
- const dirPath = path . slice ( modName . length )
133
- const files = flatResult
134
- . filter ( ( f ) => f . name . substring ( 0 , f . name . lastIndexOf ( '/' ) ) === dirPath )
135
- . map ( ( f ) => f . name . slice ( dirPath . length + 1 ) )
136
- const dirs = flatResult
137
- . filter (
138
- ( f ) =>
139
- f . name . startsWith ( dirPath + '/' ) &&
140
- f . name . substring ( dirPath . length + 1 ) . split ( '/' ) . length >= 2 ,
141
- )
142
- . map ( ( f ) => f . name . slice ( dirPath . length + 1 ) . split ( '/' ) [ 0 ] )
143
-
144
- return [
145
- ...files . map < [ string , FileType ] > ( ( f ) => [ f , 1 satisfies FileType . File ] ) ,
146
- ...[ ...new Set ( dirs ) ] . map < [ string , FileType ] > ( ( f ) => [
147
- f ,
148
- 2 satisfies FileType . Directory ,
149
- ] ) ,
150
- ]
141
+ const endpoint = `https://unpkg.com/${ pkgName } @${ actualVersion } /${ pkgPath } /?meta`
142
+
143
+ try {
144
+ const data = await fetchJson < {
145
+ files : {
146
+ path : string
147
+ type : 'file' | 'directory'
148
+ size ?: number
149
+ } [ ]
150
+ } > ( endpoint )
151
+
152
+ if ( ! data ?. files ) {
153
+ return [ ]
154
+ }
155
+
156
+ const result : [ string , FileType ] [ ] = data . files . map ( ( file ) => {
157
+ const type =
158
+ file . type === 'directory'
159
+ ? ( 2 as FileType . Directory )
160
+ : ( 1 as FileType . File )
161
+
162
+ const fullPath = file . path
163
+ statCache . set ( fullPath , { type } )
164
+
165
+ return [ _getNameFromPath ( file . path ) , type ]
166
+ } )
167
+
168
+ dirCache . set ( path , result )
169
+ return result
170
+ } catch {
171
+ return [ ]
172
+ }
173
+ }
174
+
175
+ function _getNameFromPath ( path : string ) : string {
176
+ if ( ! path ) return ''
177
+
178
+ const trimmedPath = path . endsWith ( '/' ) ? path . slice ( 0 , - 1 ) : path
179
+
180
+ const lastSlashIndex = trimmedPath . lastIndexOf ( '/' )
181
+
182
+ if (
183
+ lastSlashIndex === - 1 ||
184
+ ( lastSlashIndex === 0 && trimmedPath . length === 1 )
185
+ ) {
186
+ return trimmedPath
187
+ }
188
+
189
+ return trimmedPath . slice ( lastSlashIndex + 1 )
151
190
}
152
191
153
192
async function _readFile ( path : string ) : Promise < string | undefined > {
@@ -163,7 +202,7 @@ export function createNpmFileSystem(
163
202
if ( ( await _stat ( path ) ) ?. type !== ( 1 satisfies FileType . File ) ) {
164
203
return
165
204
}
166
- const text = await fetchText ( `https://cdn.jsdelivr.net/npm /${ path } ` )
205
+ const text = await fetchText ( `https://unpkg.com /${ path } ` )
167
206
if ( text !== undefined ) {
168
207
onFetch ?.( path , text )
169
208
}
@@ -175,35 +214,6 @@ export function createNpmFileSystem(
175
214
return await fetchResults . get ( path ) !
176
215
}
177
216
178
- async function flat ( pkgName : string , version : string | undefined ) {
179
- version ??= 'latest'
180
-
181
- // resolve latest tag
182
- if ( version === 'latest' ) {
183
- const data = await fetchJson < { version : string | null } > (
184
- `https://data.jsdelivr.com/v1/package/resolve/npm/${ pkgName } @${ version } ` ,
185
- )
186
- if ( ! data ?. version ) {
187
- return [ ]
188
- }
189
- version = data . version
190
- }
191
-
192
- const flat = await fetchJson < {
193
- files : {
194
- name : string
195
- size : number
196
- time : string
197
- hash : string
198
- } [ ]
199
- } > ( `https://data.jsdelivr.com/v1/package/npm/${ pkgName } @${ version } /flat` )
200
- if ( ! flat ) {
201
- return [ ]
202
- }
203
-
204
- return flat . files
205
- }
206
-
207
217
async function isValidPackageName ( pkgName : string ) {
208
218
// ignore @aaa /node_modules
209
219
if ( pkgName . endsWith ( '/node_modules' ) ) {
0 commit comments