22 * base on @volar/jsdelivr
33 * MIT License https://github.com/volarjs/volar.js/blob/master/packages/jsdelivr/LICENSE
44 */
5- import type { FileSystem , FileType } from '@volar/language-service'
5+ import type { FileStat , FileSystem , FileType } from '@volar/language-service'
66import type { URI } from 'vscode-uri'
77
88const textCache = new Map < string , Promise < string | undefined > > ( )
@@ -20,17 +20,8 @@ export function createNpmFileSystem(
2020 onFetch ?: ( path : string , content : string ) => void ,
2121) : FileSystem {
2222 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 ] [ ] > ( )
3425
3526 return {
3627 async stat ( uri ) {
@@ -65,7 +56,16 @@ export function createNpmFileSystem(
6556 }
6657
6758 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 )
6969 if ( ! pkgName ) {
7070 if ( modName . startsWith ( '@' ) ) {
7171 return {
@@ -82,72 +82,111 @@ export function createNpmFileSystem(
8282 return
8383 }
8484
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 ,
9288 }
89+ statCache . set ( path , result )
90+ return { ...result , ctime : - 1 , mtime : - 1 , size : - 1 }
9391 }
9492
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 )
9896
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 }
117106 }
107+
108+ return
109+ } catch {
110+ return
118111 }
119112 }
120113
121114 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+
123121 if ( ! pkgName || ! ( await isValidPackageName ( pkgName ) ) ) {
124122 return [ ]
125123 }
126124
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+ }
129139 }
130140
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 )
151190 }
152191
153192 async function _readFile ( path : string ) : Promise < string | undefined > {
@@ -163,7 +202,7 @@ export function createNpmFileSystem(
163202 if ( ( await _stat ( path ) ) ?. type !== ( 1 satisfies FileType . File ) ) {
164203 return
165204 }
166- const text = await fetchText ( `https://cdn.jsdelivr.net/npm /${ path } ` )
205+ const text = await fetchText ( `https://unpkg.com /${ path } ` )
167206 if ( text !== undefined ) {
168207 onFetch ?.( path , text )
169208 }
@@ -175,35 +214,6 @@ export function createNpmFileSystem(
175214 return await fetchResults . get ( path ) !
176215 }
177216
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-
207217 async function isValidPackageName ( pkgName : string ) {
208218 // ignore @aaa /node_modules
209219 if ( pkgName . endsWith ( '/node_modules' ) ) {
0 commit comments