2025-01-27 15:53:20 +03:00

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);
}
}
}
}