Skip to content

Instantly share code, notes, and snippets.

@naporin0624
Last active October 5, 2021 12:31
Show Gist options
  • Save naporin0624/2c1c187950738ef4e07a755489ba49de to your computer and use it in GitHub Desktop.
Save naporin0624/2c1c187950738ef4e07a755489ba49de to your computer and use it in GitHub Desktop.
import { Subscription } from "rxjs";
import axios from "axios";
import axiosRetry from "axios-retry";
import type { languages } from "monaco-editor";
axiosRetry(axios, { retries: 3 });
const concurrent = async <T>(promises: (() => Promise<T>)[], concurrency = 3): Promise<T[]> => {
const results: T[] = [];
let currentIndex = 0;
let chunks: (() => Promise<T>)[] = [];
do {
chunks = promises.slice(currentIndex, currentIndex + concurrency);
Array.prototype.push.apply(results, await Promise.all(chunks.map((c) => c())));
currentIndex += concurrency;
} while (chunks.length > 0);
return results;
};
type ModuleFile = {
path: string;
type: "file";
contentType: string;
integrity: string;
lastModified: string;
size: number;
};
type ModuleMeta = {
path: string;
type: "directory";
files: (ModuleMeta | ModuleFile)[];
};
type PackageJSON = {
name: string;
version: string;
dependencies: Record<string, string>;
devDependencies: Record<string, string>;
peerDependencies: Record<string, string>;
types: string;
typings: string;
};
/**
* @package
*/
export class Installer {
private packageMap: Map<string, Subscription> = new Map();
private blackList = [];
private extensions = [".ts", ".tsx"];
constructor(private defaults: Pick<languages.typescript.LanguageServiceDefaults, "addExtraLib">) {}
async install(packageName: string, version = "*"): Promise<void> {
const target = `${packageName}@${version}`;
const cache = localStorage.getItem(`monaco.installer.${target}`);
if (this.packageMap.has(packageName)) return;
if (cache) {
const list: [path: string, content: string][] = JSON.parse(cache);
this.apply(list);
const packageJSON = JSON.parse(list.find(([path]) => path.endsWith("package.json"))?.[1] ?? "{}");
await concurrent(
this.dependencies(packageJSON).map(([name, version]) => {
return () => this.install(name, version);
}),
);
return;
}
const meta = await axios.get<ModuleMeta>(`https://unpkg.com/${target}/?meta`).then((res) => res.data);
const packageJSONContent = await this.fetchFileContent(packageName, { path: "/package.json" });
const packageJSON: PackageJSON = JSON.parse(packageJSONContent[1]);
const typing = packageJSON.typings ?? packageJSON.types ?? "";
const typingDirectory = typing.split("/").slice(0, -1).join("/");
const files = this.pickLibFiles(meta.files).filter((file) => contain(typingDirectory, file.path));
const fileContentResolver = files.map((file) => () => this.fetchFileContent(packageName, file));
const fileContents = await concurrent(fileContentResolver, 20);
const list: [path: string, content: string][] =
fileContents.length > 0 ? [...fileContents, packageJSONContent] : [];
localStorage.setItem(`monaco.installer.${target}`, JSON.stringify(list));
const depsResolver = this.dependencies(packageJSON).map(([name, version]) => {
return () => this.install(name, version);
});
await concurrent(depsResolver);
this.packageMap.set(packageName, this.apply(list));
}
uninstall(packageName: string) {
const subscription = this.packageMap.get(packageName);
if (subscription === undefined) throw new Error(`${packageName} is not install`);
subscription.unsubscribe();
this.packageMap.delete(packageName);
}
private apply(deps: [path: string, content: string][]): Subscription {
const subscription = new Subscription();
deps.forEach(([fileName, content]) => {
const disposable = this.defaults.addExtraLib(content, fileName);
subscription.add(() => disposable.dispose());
});
return subscription;
}
private pickLibFiles(files: ModuleMeta["files"]): ModuleFile[] {
const list: ModuleFile[] = [];
files.forEach((file) => {
switch (file.type) {
case "directory": {
list.push(...this.pickLibFiles(file.files));
break;
}
case "file": {
const extMatched = this.extensions.some((ext) => file.path.endsWith(ext));
const inBlacklist = this.blackList.some((subStr) => file.path.includes(subStr));
if (extMatched && !inBlacklist) list.push(file);
break;
}
}
});
return list;
}
private async fetchFileContent(
packageName: string,
{ path }: Pick<ModuleFile, "path">,
): Promise<[path: string, content: string]> {
const { data } = await axios.get<string | Record<string, unknown>>(`https://unpkg.com/${packageName}${path}`);
return [`file:///node_modules/${packageName}${path}`, typeof data === "string" ? data : JSON.stringify(data)];
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private dependencies(packageJSON: any) {
const deps: [name: string, version: string][] = Object.entries(packageJSON?.dependencies ?? {});
const dependencies: [name: string, version: string][] = deps.map(([name, version]) => {
const typing = packageJSON?.devDependencies?.[`@types/${name}`];
return typing ? [name, typing] : [name, version];
});
return dependencies;
}
}
const languageDefault: Pick<languages.typescript.LanguageServiceDefaults, "addExtraLib"> = {
addExtraLib(content, filePath) {
return window?.Monaco?.languages.typescript.typescriptDefaults.addExtraLib(content, filePath);
},
};
const installer = new Installer(languageDefault);
export default installer;
const contain = (parent: string, child: string): boolean => {
const p = `/${parent}/`.replace("./", "/").replace("//", "/");
const c = `/${child}/`.replace("./", "/").replace("//", "/");
return c.startsWith(p);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment