Skip to content

Commit 04daf03

Browse files
committed
refactor(deps): @volar/jsdelivr -> local
1 parent 5e092b6 commit 04daf03

File tree

4 files changed

+321
-8
lines changed

4 files changed

+321
-8
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
"@types/hash-sum": "^1.0.2",
8787
"@types/node": "^22.13.4",
8888
"@vitejs/plugin-vue": "^5.2.1",
89-
"@volar/jsdelivr": "~2.4.11",
89+
"@volar/language-service": "~2.4.11",
9090
"@volar/monaco": "~2.4.11",
9191
"@vue/babel-plugin-jsx": "^1.2.5",
9292
"@vue/language-service": "~2.2.2",

pnpm-lock.yaml

Lines changed: 1 addition & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/monaco/resource.ts

Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
/**
2+
* base on @volar/jsdelivr
3+
* MIT License https://github.com/volarjs/volar.js/blob/master/packages/jsdelivr/LICENSE
4+
*/
5+
import type { FileSystem, FileType } from '@volar/language-service'
6+
import type { URI } from 'vscode-uri'
7+
8+
const textCache = new Map<string, Promise<string | undefined>>()
9+
const jsonCache = new Map<string, Promise<any>>()
10+
11+
export function createNpmFileSystem(
12+
getCdnPath = (uri: URI): string | undefined => {
13+
if (uri.path === '/node_modules') {
14+
return ''
15+
} else if (uri.path.startsWith('/node_modules/')) {
16+
return uri.path.slice('/node_modules/'.length)
17+
}
18+
},
19+
getPackageVersion?: (pkgName: string) => string | undefined,
20+
onFetch?: (path: string, content: string) => void,
21+
): FileSystem {
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+
>()
34+
35+
return {
36+
async stat(uri) {
37+
const path = getCdnPath(uri)
38+
if (path === undefined) {
39+
return
40+
}
41+
if (path === '') {
42+
return {
43+
type: 2 satisfies FileType.Directory,
44+
size: -1,
45+
ctime: -1,
46+
mtime: -1,
47+
}
48+
}
49+
return await _stat(path)
50+
},
51+
async readFile(uri) {
52+
const path = getCdnPath(uri)
53+
if (path === undefined) {
54+
return
55+
}
56+
return await _readFile(path)
57+
},
58+
readDirectory(uri) {
59+
const path = getCdnPath(uri)
60+
if (path === undefined) {
61+
return []
62+
}
63+
return _readDirectory(path)
64+
},
65+
}
66+
67+
async function _stat(path: string) {
68+
const [modName, pkgName, pkgVersion, pkgFilePath] = resolvePackageName(path)
69+
if (!pkgName) {
70+
if (modName.startsWith('@')) {
71+
return {
72+
type: 2 satisfies FileType.Directory,
73+
ctime: -1,
74+
mtime: -1,
75+
size: -1,
76+
}
77+
} else {
78+
return
79+
}
80+
}
81+
if (!(await isValidPackageName(pkgName))) {
82+
return
83+
}
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,
92+
}
93+
}
94+
95+
if (!flatResults.has(modName)) {
96+
flatResults.set(modName, flat(pkgName, pkgVersion))
97+
}
98+
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,
117+
}
118+
}
119+
}
120+
121+
async function _readDirectory(path: string): Promise<[string, FileType][]> {
122+
const [modName, pkgName, pkgVersion] = resolvePackageName(path)
123+
if (!pkgName || !(await isValidPackageName(pkgName))) {
124+
return []
125+
}
126+
127+
if (!flatResults.has(modName)) {
128+
flatResults.set(modName, flat(pkgName, pkgVersion))
129+
}
130+
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+
]
151+
}
152+
153+
async function _readFile(path: string): Promise<string | undefined> {
154+
const [_modName, pkgName, _version, pkgFilePath] = resolvePackageName(path)
155+
if (!pkgName || !pkgFilePath || !(await isValidPackageName(pkgName))) {
156+
return
157+
}
158+
159+
if (!fetchResults.has(path)) {
160+
fetchResults.set(
161+
path,
162+
(async () => {
163+
if ((await _stat(path))?.type !== (1 satisfies FileType.File)) {
164+
return
165+
}
166+
const text = await fetchText(`https://cdn.jsdelivr.net/npm/${path}`)
167+
if (text !== undefined) {
168+
onFetch?.(path, text)
169+
}
170+
return text
171+
})(),
172+
)
173+
}
174+
175+
return await fetchResults.get(path)!
176+
}
177+
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+
async function isValidPackageName(pkgName: string) {
208+
// ignore @aaa/node_modules
209+
if (pkgName.endsWith('/node_modules')) {
210+
return false
211+
}
212+
// hard code to skip known invalid package
213+
if (
214+
pkgName.endsWith('.d.ts') ||
215+
pkgName.startsWith('@typescript/') ||
216+
pkgName.startsWith('@types/typescript__')
217+
) {
218+
return false
219+
}
220+
// don't check @types if original package already having types
221+
if (pkgName.startsWith('@types/')) {
222+
let originalPkgName = pkgName.slice('@types/'.length)
223+
if (originalPkgName.indexOf('__') >= 0) {
224+
originalPkgName = '@' + originalPkgName.replace('__', '/')
225+
}
226+
const packageJson = await _readFile(`${originalPkgName}/package.json`)
227+
if (!packageJson) {
228+
return false
229+
}
230+
const packageJsonObj = JSON.parse(packageJson)
231+
if (packageJsonObj.types || packageJsonObj.typings) {
232+
return false
233+
}
234+
const indexDts = await _stat(`${originalPkgName}/index.d.ts`)
235+
if (indexDts?.type === (1 satisfies FileType.File)) {
236+
return false
237+
}
238+
}
239+
return true
240+
}
241+
242+
/**
243+
* @example
244+
* "a/b/c" -> ["a", "a", undefined, "b/c"]
245+
* "@a" -> ["@a", undefined, undefined, ""]
246+
* "@a/b/c" -> ["@a/b", "@a/b", undefined, "c"]
247+
* "@a/[email protected]/c" -> ["@a/[email protected]", "@a/b", "1.2.3", "c"]
248+
*/
249+
function resolvePackageName(
250+
input: string,
251+
): [
252+
modName: string,
253+
pkgName: string | undefined,
254+
version: string | undefined,
255+
path: string,
256+
] {
257+
const parts = input.split('/')
258+
let modName = parts[0]
259+
let path: string
260+
if (modName.startsWith('@')) {
261+
if (!parts[1]) {
262+
return [modName, undefined, undefined, '']
263+
}
264+
modName += '/' + parts[1]
265+
path = parts.slice(2).join('/')
266+
} else {
267+
path = parts.slice(1).join('/')
268+
}
269+
let pkgName = modName
270+
let version: string | undefined
271+
if (modName.lastIndexOf('@') >= 1) {
272+
pkgName = modName.substring(0, modName.lastIndexOf('@'))
273+
version = modName.substring(modName.lastIndexOf('@') + 1)
274+
}
275+
if (!version && getPackageVersion) {
276+
getPackageVersion?.(pkgName)
277+
}
278+
return [modName, pkgName, version, path]
279+
}
280+
}
281+
282+
async function fetchText(url: string) {
283+
if (!textCache.has(url)) {
284+
textCache.set(
285+
url,
286+
(async () => {
287+
try {
288+
const res = await fetch(url)
289+
if (res.status === 200) {
290+
return await res.text()
291+
}
292+
} catch {
293+
// ignore
294+
}
295+
})(),
296+
)
297+
}
298+
return await textCache.get(url)!
299+
}
300+
301+
async function fetchJson<T>(url: string) {
302+
if (!jsonCache.has(url)) {
303+
jsonCache.set(
304+
url,
305+
(async () => {
306+
try {
307+
const res = await fetch(url)
308+
if (res.status === 200) {
309+
return await res.json()
310+
}
311+
} catch {
312+
// ignore
313+
}
314+
})(),
315+
)
316+
}
317+
return (await jsonCache.get(url)!) as T
318+
}

src/monaco/vue.worker.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
type LanguageServiceEnvironment,
66
createTypeScriptWorkerLanguageService,
77
} from '@volar/monaco/worker'
8-
import { createNpmFileSystem } from '@volar/jsdelivr'
8+
import { createNpmFileSystem } from './resource'
99
import {
1010
type VueCompilerOptions,
1111
getFullLanguageServicePlugins,

0 commit comments

Comments
 (0)