284 lines
7.1 KiB
TypeScript
284 lines
7.1 KiB
TypeScript
export interface RootInfoOutput {
|
|
denoDir: string;
|
|
npmCache: string;
|
|
}
|
|
|
|
export async function rootInfo(): Promise<RootInfoOutput> {
|
|
if (!tmpDir) tmpDir = Deno.makeTempDirSync();
|
|
const opts = {
|
|
args: ["info", "--json", "--no-config", "--no-lock"],
|
|
cwd: tmpDir,
|
|
env: { DENO_NO_PACKAGE_JSON: "true" } as Record<string, string>,
|
|
stdout: "piped",
|
|
stderr: "inherit",
|
|
};
|
|
|
|
const output = await new Deno.Command(
|
|
Deno.execPath(),
|
|
opts as Deno.CommandOptions,
|
|
).output();
|
|
if (!output.success) {
|
|
throw new Error(`Failed to call 'deno info'`);
|
|
}
|
|
const txt = new TextDecoder().decode(output.stdout);
|
|
return JSON.parse(txt);
|
|
}
|
|
|
|
// Lifted from https://raw.githubusercontent.com/denoland/deno_graph/89affe43c9d3d5c9165c8089687c107d53ed8fe1/lib/media_type.ts
|
|
export type MediaType =
|
|
| "JavaScript"
|
|
| "Mjs"
|
|
| "Cjs"
|
|
| "JSX"
|
|
| "TypeScript"
|
|
| "Mts"
|
|
| "Cts"
|
|
| "Dts"
|
|
| "Dmts"
|
|
| "Dcts"
|
|
| "TSX"
|
|
| "Json"
|
|
| "Wasm"
|
|
| "TsBuildInfo"
|
|
| "SourceMap"
|
|
| "Unknown";
|
|
|
|
interface InfoOutput {
|
|
roots: string[];
|
|
modules: ModuleEntry[];
|
|
redirects: Record<string, string>;
|
|
npmPackages?: Record<string, NpmPackage>;
|
|
}
|
|
|
|
export type ModuleEntry =
|
|
| ModuleEntryError
|
|
| ModuleEntryEsm
|
|
| ModuleEntryJson
|
|
| ModuleEntryNpm
|
|
| ModuleEntryNode;
|
|
|
|
export interface ModuleEntryBase {
|
|
specifier: string;
|
|
}
|
|
|
|
export interface ModuleEntryError extends ModuleEntryBase {
|
|
error: string;
|
|
}
|
|
|
|
export interface ModuleEntryEsm extends ModuleEntryBase {
|
|
kind: "esm";
|
|
local: string | null;
|
|
emit: string | null;
|
|
map: string | null;
|
|
mediaType: MediaType;
|
|
size: number;
|
|
}
|
|
|
|
export interface ModuleEntryJson extends ModuleEntryBase {
|
|
kind: "asserted" | "json";
|
|
local: string | null;
|
|
mediaType: MediaType;
|
|
size: number;
|
|
}
|
|
|
|
export interface ModuleEntryNpm extends ModuleEntryBase {
|
|
kind: "npm";
|
|
npmPackage: string;
|
|
}
|
|
|
|
export interface ModuleEntryNode extends ModuleEntryBase {
|
|
kind: "node";
|
|
moduleName: string;
|
|
}
|
|
|
|
export interface NpmPackage {
|
|
name: string;
|
|
version: string;
|
|
dependencies: string[];
|
|
registryUrl?: string;
|
|
}
|
|
|
|
export interface InfoOptions {
|
|
cwd?: string;
|
|
config?: string;
|
|
importMap?: string;
|
|
lock?: string;
|
|
nodeModulesDir?: "auto" | "manual" | "none";
|
|
}
|
|
|
|
let tmpDir: string | undefined;
|
|
|
|
async function info(
|
|
specifier: string,
|
|
options: InfoOptions,
|
|
): Promise<InfoOutput> {
|
|
const args = ["info", "--json"];
|
|
if (!Deno.version.deno.startsWith("1.")) {
|
|
args.push("--allow-import");
|
|
}
|
|
const opts = {
|
|
args,
|
|
cwd: undefined as string | undefined,
|
|
env: { DENO_NO_PACKAGE_JSON: "true" } as Record<string, string>,
|
|
stdout: "piped",
|
|
stderr: "inherit",
|
|
};
|
|
if (typeof options.config === "string") {
|
|
opts.args.push("--config", options.config);
|
|
} else {
|
|
opts.args.push("--no-config");
|
|
}
|
|
if (options.importMap) {
|
|
opts.args.push("--import-map", options.importMap);
|
|
}
|
|
if (typeof options.lock === "string") {
|
|
opts.args.push("--lock", options.lock);
|
|
} else if (!options.cwd) {
|
|
opts.args.push("--no-lock");
|
|
}
|
|
if (options.nodeModulesDir !== undefined) {
|
|
if (Deno.version.deno.startsWith("1.")) {
|
|
if (options.nodeModulesDir === "auto") {
|
|
opts.args.push("--node-modules-dir");
|
|
} else if (options.nodeModulesDir === "manual") {
|
|
opts.args.push("--unstable-byonm");
|
|
}
|
|
} else {
|
|
opts.args.push(`--node-modules-dir=${options.nodeModulesDir}`);
|
|
}
|
|
}
|
|
if (options.cwd) {
|
|
opts.cwd = options.cwd;
|
|
} else {
|
|
if (!tmpDir) tmpDir = Deno.makeTempDirSync();
|
|
opts.cwd = tmpDir;
|
|
}
|
|
|
|
opts.args.push(specifier);
|
|
|
|
const output = await new Deno.Command(
|
|
Deno.execPath(),
|
|
opts as Deno.CommandOptions,
|
|
).output();
|
|
if (!output.success) {
|
|
throw new Error(`Failed to call 'deno info' on '${specifier}'`);
|
|
}
|
|
const txt = new TextDecoder().decode(output.stdout);
|
|
return JSON.parse(txt);
|
|
}
|
|
|
|
export class InfoCache {
|
|
#options: InfoOptions;
|
|
|
|
#pending: { done: Promise<void>; specifiers: Set<string> | null } | null =
|
|
null;
|
|
|
|
#modules: Map<string, ModuleEntry> = new Map();
|
|
#redirects: Map<string, string> = new Map();
|
|
#npmPackages: Map<string, NpmPackage> = new Map();
|
|
|
|
constructor(options: InfoOptions = {}) {
|
|
this.#options = options;
|
|
}
|
|
|
|
async get(specifier: string): Promise<ModuleEntry> {
|
|
let entry = this.#getCached(specifier);
|
|
if (entry !== undefined) return entry;
|
|
|
|
await this.#queueLoad(specifier);
|
|
|
|
entry = this.#getCached(specifier);
|
|
if (entry === undefined) {
|
|
throw new Error(`Unreachable: '${specifier}' loaded but not reachable`);
|
|
}
|
|
return entry;
|
|
}
|
|
|
|
getNpmPackage(id: string): NpmPackage | undefined {
|
|
return this.#npmPackages.get(id);
|
|
}
|
|
|
|
#resolve(specifier: string): string {
|
|
const original = specifier;
|
|
let counter = 0;
|
|
while (counter++ < 10) {
|
|
const redirect = this.#redirects.get(specifier);
|
|
if (redirect === undefined) return specifier;
|
|
specifier = redirect;
|
|
}
|
|
throw new Error(`Too many redirects for '${original}'`);
|
|
}
|
|
|
|
#getCached(specifier: string): ModuleEntry | undefined {
|
|
specifier = this.#resolve(specifier);
|
|
return this.#modules.get(specifier);
|
|
}
|
|
|
|
async #queueLoad(specifier: string) {
|
|
while (true) {
|
|
if (this.#pending === null) {
|
|
this.#pending = {
|
|
specifiers: new Set([specifier]),
|
|
done: (async () => {
|
|
await new Promise((r) => setTimeout(r, 5));
|
|
const specifiers = this.#pending!.specifiers!;
|
|
this.#pending!.specifiers = null;
|
|
await this.#load([...specifiers]);
|
|
this.#pending = null;
|
|
})(),
|
|
};
|
|
await this.#pending.done;
|
|
return;
|
|
} else if (this.#pending.specifiers !== null) {
|
|
this.#pending.specifiers.add(specifier);
|
|
await this.#pending.done;
|
|
return;
|
|
} else {
|
|
await this.#pending.done;
|
|
}
|
|
}
|
|
}
|
|
|
|
async #load(specifiers: string[]): Promise<void> {
|
|
await this.#populate(specifiers);
|
|
for (let specifier of specifiers) {
|
|
specifier = this.#resolve(specifier);
|
|
const entry = this.#modules.get(specifier);
|
|
if (entry === undefined && specifier.startsWith("npm:")) {
|
|
// we hit https://github.com/denoland/deno/issues/18043, so we have to
|
|
// perform another load to get the actual data of the redirected specifier
|
|
await this.#populate([specifier]);
|
|
}
|
|
}
|
|
}
|
|
|
|
async #populate(specifiers: string[]) {
|
|
let specifier;
|
|
if (specifiers.length === 1) {
|
|
specifier = specifiers[0];
|
|
} else {
|
|
specifier = `data:application/javascript,${
|
|
encodeURIComponent(
|
|
specifiers.map((s) => `import ${JSON.stringify(s)};`).join(""),
|
|
)
|
|
}`;
|
|
}
|
|
const { modules, redirects, npmPackages } = await info(
|
|
specifier,
|
|
this.#options,
|
|
);
|
|
for (const module of modules) {
|
|
if (specifiers.length !== 1 && module.specifier === specifier) continue;
|
|
this.#modules.set(module.specifier, module);
|
|
}
|
|
for (const [from, to] of Object.entries(redirects)) {
|
|
this.#redirects.set(from, to);
|
|
}
|
|
if (npmPackages !== undefined) {
|
|
for (const [id, npmPackage] of Object.entries(npmPackages)) {
|
|
this.#npmPackages.set(id, npmPackage);
|
|
}
|
|
}
|
|
}
|
|
}
|