UNPKG

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