@oazmi/build-tools
Version:
general deno build tool scripts which I practically use in all of my typescript repos
178 lines (177 loc) • 7.44 kB
JavaScript
import * as dntShim from "../../../../../../_dnt.shims.js";
import { dirname, fromFileUrl, join } from "../../../../@std/path/1.0.8/mod.js";
import { encodeBase32 } from "../../../../@std/encoding/1.0.7/base32.js";
import { lastIndexOfNeedle } from "../../../../@std/bytes/1.0.5/mod.js";
import * as deno from "./deno.js";
import { rootInfo } from "./deno.js";
import { mapContentType, mediaTypeFromSpecifier, mediaTypeToLoader, parseNpmSpecifier, } from "./shared.js";
let ROOT_INFO_OUTPUT;
export const DENO_CACHE_METADATA = new TextEncoder()
.encode("\n// denoCacheMetadata=");
export class NativeLoader {
#nodeModulesDirManual;
#infoCache;
#linkDirCache = new Map(); // mapping from package id -> link dir
constructor(options) {
this.#nodeModulesDirManual =
options.infoOptions?.nodeModulesDir === "manual";
this.#infoCache = new deno.InfoCache(options.infoOptions);
}
async resolve(specifier) {
// Workaround for https://github.com/denoland/deno/issues/25903
if (this.#nodeModulesDirManual && specifier.protocol === "npm:") {
const npmSpecifier = parseNpmSpecifier(specifier);
return {
kind: "npm",
packageId: "",
packageName: npmSpecifier.name,
path: npmSpecifier.path ?? "",
};
}
const entry = await this.#infoCache.get(specifier.href);
if ("error" in entry) {
if (specifier.protocol === "file:" &&
mediaTypeFromSpecifier(specifier) === "Unknown") {
return { kind: "esm", specifier: new URL(entry.specifier) };
}
throw new Error(entry.error);
}
if (entry.kind === "npm") {
// TODO(lucacasonato): remove parsing once https://github.com/denoland/deno/issues/18043 is resolved
const parsed = parseNpmSpecifier(new URL(entry.specifier));
return {
kind: "npm",
packageId: entry.npmPackage,
packageName: parsed.name,
path: parsed.path ?? "",
};
}
else if (entry.kind === "node") {
return {
kind: "node",
path: entry.specifier,
};
}
return { kind: "esm", specifier: new URL(entry.specifier) };
}
async loadEsm(specifier) {
if (specifier.protocol === "data:") {
const resp = await fetch(specifier);
const contents = new Uint8Array(await resp.arrayBuffer());
const contentType = resp.headers.get("content-type");
const mediaType = mapContentType(specifier, contentType);
const loader = mediaTypeToLoader(mediaType);
if (loader === null)
return undefined;
return { contents, loader };
}
const entry = await this.#infoCache.get(specifier.href);
if ("error" in entry && specifier.protocol !== "file:" &&
mediaTypeFromSpecifier(specifier) !== "Unknown")
throw new Error(entry.error);
if (!("local" in entry)) {
throw new Error("[unreachable] Not an ESM module.");
}
if (!entry.local)
throw new Error("Module not downloaded yet.");
const loader = mediaTypeToLoader(entry.mediaType);
if (loader === null)
return undefined;
let contents = await dntShim.Deno.readFile(entry.local);
const denoCacheMetadata = lastIndexOfNeedle(contents, DENO_CACHE_METADATA);
if (denoCacheMetadata !== -1) {
contents = contents.subarray(0, denoCacheMetadata);
}
const res = { contents, loader };
if (specifier.protocol === "file:") {
res.watchFiles = [fromFileUrl(specifier)];
}
return res;
}
async nodeModulesDirForPackage(npmPackageId) {
const npmPackage = this.#infoCache.getNpmPackage(npmPackageId);
if (!npmPackage)
throw new Error("NPM package not found.");
let linkDir = this.#linkDirCache.get(npmPackageId);
if (!linkDir) {
linkDir = await this.#nodeModulesDirForPackageInner(npmPackageId, npmPackage);
this.#linkDirCache.set(npmPackageId, linkDir);
}
return linkDir;
}
async #nodeModulesDirForPackageInner(npmPackageId, npmPackage) {
let name = npmPackage.name;
if (name.toLowerCase() !== name) {
name = `_${encodeBase32(new TextEncoder().encode(name))}`;
}
if (ROOT_INFO_OUTPUT === undefined) {
ROOT_INFO_OUTPUT = rootInfo();
}
if (ROOT_INFO_OUTPUT instanceof Promise) {
ROOT_INFO_OUTPUT = await ROOT_INFO_OUTPUT;
}
const { denoDir, npmCache } = ROOT_INFO_OUTPUT;
const registryUrl = npmPackage.registryUrl ?? "https://registry.npmjs.org";
const registry = new URL(registryUrl);
const packageDir = join(npmCache, registry.hostname, name, npmPackage.version);
const linkDir = join(denoDir, "deno_esbuild", registry.hostname, npmPackageId, "node_modules", name);
const linkDirParent = dirname(linkDir);
const tmpDirParent = join(denoDir, "deno_esbuild_tmp");
// check if the package is already linked, if so, return the link and skip
// a bunch of work
try {
await dntShim.Deno.stat(linkDir);
this.#linkDirCache.set(npmPackageId, linkDir);
return linkDir;
}
catch {
// directory does not yet exist
}
// create a temporary directory, recursively hardlink the package contents
// into it, and then rename it to the final location
await dntShim.Deno.mkdir(tmpDirParent, { recursive: true });
const tmpDir = await dntShim.Deno.makeTempDir({ dir: tmpDirParent });
await linkRecursive(packageDir, tmpDir);
try {
await dntShim.Deno.mkdir(linkDirParent, { recursive: true });
await dntShim.Deno.rename(tmpDir, linkDir);
}
catch (err) {
// the directory may already have been created by someone else - check if so
try {
await dntShim.Deno.stat(linkDir);
}
catch {
throw err;
}
}
return linkDir;
}
packageIdFromNameInPackage(name, parentPackageId) {
const parentPackage = this.#infoCache.getNpmPackage(parentPackageId);
if (!parentPackage)
throw new Error("NPM package not found.");
if (parentPackage.name === name)
return parentPackageId;
for (const dep of parentPackage.dependencies) {
const depPackage = this.#infoCache.getNpmPackage(dep);
if (!depPackage)
throw new Error("NPM package not found.");
if (depPackage.name === name)
return dep;
}
return null;
}
}
async function linkRecursive(from, to) {
const fromStat = await dntShim.Deno.stat(from);
if (fromStat.isDirectory) {
await dntShim.Deno.mkdir(to, { recursive: true });
for await (const entry of dntShim.Deno.readDir(from)) {
await linkRecursive(join(from, entry.name), join(to, entry.name));
}
}
else {
await dntShim.Deno.link(from, to);
}
}