export interface RootInfoOutput { denoDir: string; npmCache: string; } export async function rootInfo(): Promise { if (!tmpDir) tmpDir = Deno.makeTempDirSync(); const opts = { args: ["info", "--json", "--no-config", "--no-lock"], cwd: tmpDir, env: { DENO_NO_PACKAGE_JSON: "true" } as Record, 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; npmPackages?: Record; } 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 { 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, 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; specifiers: Set | null } | null = null; #modules: Map = new Map(); #redirects: Map = new Map(); #npmPackages: Map = new Map(); constructor(options: InfoOptions = {}) { this.#options = options; } async get(specifier: string): Promise { 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 { 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); } } } }