Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(npmmirror): ATA by npmmirror (new pkg) #265

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/npmmirror/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './lib/npm';
326 changes: 326 additions & 0 deletions packages/npmmirror/lib/npm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,326 @@
import type { FileStat, FileSystem, FileType } from "@volar/language-service";
import type { URI } from "vscode-uri";

const textCache = new Map<string, Promise<string | undefined>>();
const jsonCache = new Map<string, Promise<any>>();

export function createNpmFileSystem(
getCdnPath = (uri: URI): string | undefined => {
if (uri.path === "/node_modules") {
return "";
} else if (uri.path.startsWith("/node_modules/")) {
return uri.path.slice("/node_modules/".length);
}
},
getPackageVersion?: (pkgName: string) => string | undefined,
onFetch?: (path: string, content: string) => void
): FileSystem {
const fetchResults = new Map<string, Promise<string | undefined>>();
const statCache = new Map<string, { type: FileType; }>();
const dirCache = new Map<string, [string, FileType][]>();

return {
async stat(uri) {
const path = getCdnPath(uri);
if (path === undefined) {
return;
}
if (path === "") {
return {
type: 2 satisfies FileType.Directory,
size: -1,
ctime: -1,
mtime: -1,
};
}
return await _stat(path);
},
async readFile(uri) {
const path = getCdnPath(uri);
if (path === undefined) {
return;
}
return await _readFile(path);
},
readDirectory(uri) {
const path = getCdnPath(uri);
if (path === undefined) {
return [];
}
return _readDirectory(path);
},
};

async function _stat(path: string) {
if (statCache.has(path)) {
return {
...statCache.get(path),
ctime: -1,
mtime: -1,
size: -1,
} as FileStat;
}

const [modName, pkgName, , pkgFilePath] = resolvePackageName(path);
if (!pkgName) {
if (modName.startsWith("@")) {
return {
type: 2 satisfies FileType.Directory,
ctime: -1,
mtime: -1,
size: -1,
};
} else {
return;
}
}
if (!(await isValidPackageName(pkgName))) {
return;
}

if (!pkgFilePath || pkgFilePath === "/") {
const result = {
type: 2 as FileType.Directory,
};
statCache.set(path, result);
return { ...result, ctime: -1, mtime: -1, size: -1 };
}

try {
const parentDir = path.substring(0, path.lastIndexOf("/"));
const fileName = path.substring(path.lastIndexOf("/") + 1);

const dirContent = await _readDirectory(parentDir);
const fileEntry = dirContent.find(([name]) => name === fileName);

if (fileEntry) {
const result = {
type: fileEntry[1],
};
statCache.set(path, result);
return { ...result, ctime: -1, mtime: -1, size: -1 };
}

return;
} catch {
return;
}
}

async function _readDirectory(path: string): Promise<[string, FileType][]> {
if (dirCache.has(path)) {
return dirCache.get(path)!;
}

const [, pkgName, pkgVersion, pkgPath] = resolvePackageName(path);

if (!pkgName || !(await isValidPackageName(pkgName))) {
return [];
}

const resolvedVersion = pkgVersion || "latest";

let actualVersion = resolvedVersion;
if (resolvedVersion === "latest") {
try {
const data = await fetchJson<{ version: string; }>(
`https://registry.npmmirror.com/${pkgName}/latest/files/package.json`
);
if (data?.version) {
actualVersion = data.version;
}
} catch {
// ignore
}
}

const endpoint = `https://registry.npmmirror.com/${pkgName}/${actualVersion}/files/${pkgPath}/?meta`;
try {
const data = await fetchJson<{
files: {
path: string;
type: "file" | "directory";
size?: number;
}[];
}>(endpoint);

if (!data?.files) {
return [];
}

const result: [string, FileType][] = data.files.map((file) => {
const type =
file.type === "directory"
? (2 as FileType.Directory)
: (1 as FileType.File);

const fullPath = file.path;
statCache.set(fullPath, { type });

return [_getNameFromPath(file.path), type];
});

dirCache.set(path, result);
return result;
} catch {
return [];
}
}

function _getNameFromPath(path: string): string {
if (!path) return "";

const trimmedPath = path.endsWith("/") ? path.slice(0, -1) : path;

const lastSlashIndex = trimmedPath.lastIndexOf("/");

if (
lastSlashIndex === -1 ||
(lastSlashIndex === 0 && trimmedPath.length === 1)
) {
return trimmedPath;
}

return trimmedPath.slice(lastSlashIndex + 1);
}

async function _readFile(path: string): Promise<string | undefined> {
const [_modName, pkgName, _version, pkgFilePath] = resolvePackageName(path);
if (!pkgName || !pkgFilePath || !(await isValidPackageName(pkgName))) {
return;
}

if (!fetchResults.has(path)) {
fetchResults.set(
path,
(async () => {
if ((await _stat(path))?.type !== (1 satisfies FileType.File)) {
return;
}
const text = await fetchText(
`https://registry.npmmirror.com/${pkgName}/${_version || "latest"
}/files/${pkgFilePath}`
);
if (text !== undefined) {
onFetch?.(path, text);
}
return text;
})()
);
}

return await fetchResults.get(path)!;
}

async function isValidPackageName(pkgName: string) {
// ignore @aaa/node_modules
if (pkgName.endsWith("/node_modules")) {
return false;
}
// hard code to skip known invalid package
if (
pkgName.endsWith(".d.ts") ||
pkgName.startsWith("@typescript/") ||
pkgName.startsWith("@types/typescript__")
) {
return false;
}
// don't check @types if original package already having types
if (pkgName.startsWith("@types/")) {
let originalPkgName = pkgName.slice("@types/".length);
if (originalPkgName.indexOf("__") >= 0) {
originalPkgName = "@" + originalPkgName.replace("__", "/");
}
const packageJson = await _readFile(`${originalPkgName}/package.json`);
if (!packageJson) {
return false;
}
const packageJsonObj = JSON.parse(packageJson);
if (packageJsonObj.types || packageJsonObj.typings) {
return false;
}
const indexDts = await _stat(`${originalPkgName}/index.d.ts`);
if (indexDts?.type === (1 satisfies FileType.File)) {
return false;
}
}
return true;
}

/**
* @example
* "a/b/c" -> ["a", "a", undefined, "b/c"]
* "@a" -> ["@a", undefined, undefined, ""]
* "@a/b/c" -> ["@a/b", "@a/b", undefined, "c"]
* "@a/[email protected]/c" -> ["@a/[email protected]", "@a/b", "1.2.3", "c"]
*/
function resolvePackageName(
input: string
): [
modName: string,
pkgName: string | undefined,
version: string | undefined,
path: string
] {
const parts = input.split("/");
let modName = parts[0];
let path: string;
if (modName.startsWith("@")) {
if (!parts[1]) {
return [modName, undefined, undefined, ""];
}
modName += "/" + parts[1];
path = parts.slice(2).join("/");
} else {
path = parts.slice(1).join("/");
}
let pkgName = modName;
let version: string | undefined;
if (modName.lastIndexOf("@") >= 1) {
pkgName = modName.substring(0, modName.lastIndexOf("@"));
version = modName.substring(modName.lastIndexOf("@") + 1);
}
if (!version && getPackageVersion) {
version = getPackageVersion?.(pkgName);
}
return [modName, pkgName, version, path];
}
}

async function fetchText(url: string) {
if (!textCache.has(url)) {
textCache.set(
url,
(async () => {
try {
const res = await fetch(url);
if (res.status === 200) {
return await res.text();
}
} catch {
// ignore
}
})()
);
}
return await textCache.get(url)!;
}

async function fetchJson<T>(url: string) {
if (!jsonCache.has(url)) {
jsonCache.set(
url,
(async () => {
try {
const res = await fetch(url);
if (res.status === 200) {
return await res.json();
}
} catch {
// ignore
}
})()
);
}
return (await jsonCache.get(url)!) as T;
}
18 changes: 18 additions & 0 deletions packages/npmmirror/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "@volar/npmmirror",
"version": "2.4.12",
"license": "MIT",
"files": [
"**/*.js",
"**/*.d.ts"
],
"repository": {
"type": "git",
"url": "https://github.com/volarjs/volar.js.git",
"directory": "packages/npmmirror"
},
"devDependencies": {
"@volar/language-service": "2.4.12",
"vscode-uri": "^3.0.8"
}
}
7 changes: 7 additions & 0 deletions packages/npmmirror/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"include": [ "*", "lib/**/*" ],
"references": [
{ "path": "../language-service/tsconfig.json" },
],
}
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.