Skip to content

Commit d7b8b68

Browse files
committed
feat(npmmirror): ATA by npmmirror (new pkg) (#265)
1 parent ba0f18b commit d7b8b68

File tree

5 files changed

+708
-369
lines changed

5 files changed

+708
-369
lines changed

packages/npmmirror/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './lib/npm';

packages/npmmirror/lib/npm.ts

Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
import type { FileStat, FileSystem, FileType } from "@volar/language-service";
2+
import type { URI } from "vscode-uri";
3+
4+
const textCache = new Map<string, Promise<string | undefined>>();
5+
const jsonCache = new Map<string, Promise<any>>();
6+
7+
export function createNpmFileSystem(
8+
getCdnPath = (uri: URI): string | undefined => {
9+
if (uri.path === "/node_modules") {
10+
return "";
11+
} else if (uri.path.startsWith("/node_modules/")) {
12+
return uri.path.slice("/node_modules/".length);
13+
}
14+
},
15+
getPackageVersion?: (pkgName: string) => string | undefined,
16+
onFetch?: (path: string, content: string) => void
17+
): FileSystem {
18+
const fetchResults = new Map<string, Promise<string | undefined>>();
19+
const statCache = new Map<string, { type: FileType; }>();
20+
const dirCache = new Map<string, [string, FileType][]>();
21+
22+
return {
23+
async stat(uri) {
24+
const path = getCdnPath(uri);
25+
if (path === undefined) {
26+
return;
27+
}
28+
if (path === "") {
29+
return {
30+
type: 2 satisfies FileType.Directory,
31+
size: -1,
32+
ctime: -1,
33+
mtime: -1,
34+
};
35+
}
36+
return await _stat(path);
37+
},
38+
async readFile(uri) {
39+
const path = getCdnPath(uri);
40+
if (path === undefined) {
41+
return;
42+
}
43+
return await _readFile(path);
44+
},
45+
readDirectory(uri) {
46+
const path = getCdnPath(uri);
47+
if (path === undefined) {
48+
return [];
49+
}
50+
return _readDirectory(path);
51+
},
52+
};
53+
54+
async function _stat(path: string) {
55+
if (statCache.has(path)) {
56+
return {
57+
...statCache.get(path),
58+
ctime: -1,
59+
mtime: -1,
60+
size: -1,
61+
} as FileStat;
62+
}
63+
64+
const [modName, pkgName, , pkgFilePath] = resolvePackageName(path);
65+
if (!pkgName) {
66+
if (modName.startsWith("@")) {
67+
return {
68+
type: 2 satisfies FileType.Directory,
69+
ctime: -1,
70+
mtime: -1,
71+
size: -1,
72+
};
73+
} else {
74+
return;
75+
}
76+
}
77+
if (!(await isValidPackageName(pkgName))) {
78+
return;
79+
}
80+
81+
if (!pkgFilePath || pkgFilePath === "/") {
82+
const result = {
83+
type: 2 as FileType.Directory,
84+
};
85+
statCache.set(path, result);
86+
return { ...result, ctime: -1, mtime: -1, size: -1 };
87+
}
88+
89+
try {
90+
const parentDir = path.substring(0, path.lastIndexOf("/"));
91+
const fileName = path.substring(path.lastIndexOf("/") + 1);
92+
93+
const dirContent = await _readDirectory(parentDir);
94+
const fileEntry = dirContent.find(([name]) => name === fileName);
95+
96+
if (fileEntry) {
97+
const result = {
98+
type: fileEntry[1],
99+
};
100+
statCache.set(path, result);
101+
return { ...result, ctime: -1, mtime: -1, size: -1 };
102+
}
103+
104+
return;
105+
} catch {
106+
return;
107+
}
108+
}
109+
110+
async function _readDirectory(path: string): Promise<[string, FileType][]> {
111+
if (dirCache.has(path)) {
112+
return dirCache.get(path)!;
113+
}
114+
115+
const [, pkgName, pkgVersion, pkgPath] = resolvePackageName(path);
116+
117+
if (!pkgName || !(await isValidPackageName(pkgName))) {
118+
return [];
119+
}
120+
121+
const resolvedVersion = pkgVersion || "latest";
122+
123+
let actualVersion = resolvedVersion;
124+
if (resolvedVersion === "latest") {
125+
try {
126+
const data = await fetchJson<{ version: string; }>(
127+
`https://registry.npmmirror.com/${pkgName}/latest/files/package.json`
128+
);
129+
if (data?.version) {
130+
actualVersion = data.version;
131+
}
132+
} catch {
133+
// ignore
134+
}
135+
}
136+
137+
const endpoint = `https://registry.npmmirror.com/${pkgName}/${actualVersion}/files/${pkgPath}/?meta`;
138+
try {
139+
const data = await fetchJson<{
140+
files: {
141+
path: string;
142+
type: "file" | "directory";
143+
size?: number;
144+
}[];
145+
}>(endpoint);
146+
147+
if (!data?.files) {
148+
return [];
149+
}
150+
151+
const result: [string, FileType][] = data.files.map(file => {
152+
const type =
153+
file.type === "directory"
154+
? (2 as FileType.Directory)
155+
: (1 as FileType.File);
156+
157+
const fullPath = file.path;
158+
statCache.set(fullPath, { type });
159+
160+
return [_getNameFromPath(file.path), type];
161+
});
162+
163+
dirCache.set(path, result);
164+
return result;
165+
} catch {
166+
return [];
167+
}
168+
}
169+
170+
function _getNameFromPath(path: string): string {
171+
if (!path) {
172+
return "";
173+
}
174+
175+
const trimmedPath = path.endsWith("/") ? path.slice(0, -1) : path;
176+
177+
const lastSlashIndex = trimmedPath.lastIndexOf("/");
178+
179+
if (
180+
lastSlashIndex === -1 ||
181+
(lastSlashIndex === 0 && trimmedPath.length === 1)
182+
) {
183+
return trimmedPath;
184+
}
185+
186+
return trimmedPath.slice(lastSlashIndex + 1);
187+
}
188+
189+
async function _readFile(path: string): Promise<string | undefined> {
190+
const [_modName, pkgName, _version, pkgFilePath] = resolvePackageName(path);
191+
if (!pkgName || !pkgFilePath || !(await isValidPackageName(pkgName))) {
192+
return;
193+
}
194+
195+
if (!fetchResults.has(path)) {
196+
fetchResults.set(
197+
path,
198+
(async () => {
199+
if ((await _stat(path))?.type !== (1 satisfies FileType.File)) {
200+
return;
201+
}
202+
const text = await fetchText(
203+
`https://registry.npmmirror.com/${pkgName}/${_version || "latest"
204+
}/files/${pkgFilePath}`
205+
);
206+
if (text !== undefined) {
207+
onFetch?.(path, text);
208+
}
209+
return text;
210+
})()
211+
);
212+
}
213+
214+
return await fetchResults.get(path)!;
215+
}
216+
217+
async function isValidPackageName(pkgName: string) {
218+
// ignore @aaa/node_modules
219+
if (pkgName.endsWith("/node_modules")) {
220+
return false;
221+
}
222+
// hard code to skip known invalid package
223+
if (
224+
pkgName.endsWith(".d.ts") ||
225+
pkgName.startsWith("@typescript/") ||
226+
pkgName.startsWith("@types/typescript__")
227+
) {
228+
return false;
229+
}
230+
// don't check @types if original package already having types
231+
if (pkgName.startsWith("@types/")) {
232+
let originalPkgName = pkgName.slice("@types/".length);
233+
if (originalPkgName.indexOf("__") >= 0) {
234+
originalPkgName = "@" + originalPkgName.replace("__", "/");
235+
}
236+
const packageJson = await _readFile(`${originalPkgName}/package.json`);
237+
if (!packageJson) {
238+
return false;
239+
}
240+
const packageJsonObj = JSON.parse(packageJson);
241+
if (packageJsonObj.types || packageJsonObj.typings) {
242+
return false;
243+
}
244+
const indexDts = await _stat(`${originalPkgName}/index.d.ts`);
245+
if (indexDts?.type === (1 satisfies FileType.File)) {
246+
return false;
247+
}
248+
}
249+
return true;
250+
}
251+
252+
/**
253+
* @example
254+
* "a/b/c" -> ["a", "a", undefined, "b/c"]
255+
* "@a" -> ["@a", undefined, undefined, ""]
256+
* "@a/b/c" -> ["@a/b", "@a/b", undefined, "c"]
257+
* "@a/[email protected]/c" -> ["@a/[email protected]", "@a/b", "1.2.3", "c"]
258+
*/
259+
function resolvePackageName(
260+
input: string
261+
): [
262+
modName: string,
263+
pkgName: string | undefined,
264+
version: string | undefined,
265+
path: string
266+
] {
267+
const parts = input.split("/");
268+
let modName = parts[0];
269+
let path: string;
270+
if (modName.startsWith("@")) {
271+
if (!parts[1]) {
272+
return [modName, undefined, undefined, ""];
273+
}
274+
modName += "/" + parts[1];
275+
path = parts.slice(2).join("/");
276+
} else {
277+
path = parts.slice(1).join("/");
278+
}
279+
let pkgName = modName;
280+
let version: string | undefined;
281+
if (modName.lastIndexOf("@") >= 1) {
282+
pkgName = modName.substring(0, modName.lastIndexOf("@"));
283+
version = modName.substring(modName.lastIndexOf("@") + 1);
284+
}
285+
if (!version && getPackageVersion) {
286+
version = getPackageVersion?.(pkgName);
287+
}
288+
return [modName, pkgName, version, path];
289+
}
290+
}
291+
292+
async function fetchText(url: string) {
293+
if (!textCache.has(url)) {
294+
textCache.set(
295+
url,
296+
(async () => {
297+
try {
298+
const res = await fetch(url);
299+
if (res.status === 200) {
300+
return await res.text();
301+
}
302+
} catch {
303+
// ignore
304+
}
305+
})()
306+
);
307+
}
308+
return await textCache.get(url)!;
309+
}
310+
311+
async function fetchJson<T>(url: string) {
312+
if (!jsonCache.has(url)) {
313+
jsonCache.set(
314+
url,
315+
(async () => {
316+
try {
317+
const res = await fetch(url);
318+
if (res.status === 200) {
319+
return await res.json();
320+
}
321+
} catch {
322+
// ignore
323+
}
324+
})()
325+
);
326+
}
327+
return (await jsonCache.get(url)!) as T;
328+
}

packages/npmmirror/package.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "@volar/npmmirror",
3+
"version": "2.4.17",
4+
"license": "MIT",
5+
"files": [
6+
"**/*.js",
7+
"**/*.d.ts"
8+
],
9+
"repository": {
10+
"type": "git",
11+
"url": "https://github.com/volarjs/volar.js.git",
12+
"directory": "packages/npmmirror"
13+
},
14+
"devDependencies": {
15+
"@volar/language-service": "2.4.17",
16+
"vscode-uri": "^3.0.8"
17+
}
18+
}

packages/npmmirror/tsconfig.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": "../../tsconfig.base.json",
3+
"include": [ "*", "lib/**/*" ],
4+
"references": [
5+
{ "path": "../language-service/tsconfig.json" },
6+
],
7+
}

0 commit comments

Comments
 (0)