@oazmi/build-tools
Version:
general deno build tool scripts which I practically use in all of my typescript repos
182 lines (181 loc) • 7.22 kB
JavaScript
import * as dntShim from "../../../../../../_dnt.shims.js";
import { fromFileUrl } from "../../../../@std/path/1.0.8/mod.js";
import { mapContentType, mediaTypeToLoader, parseJsrSpecifier, parseNpmSpecifier, } from "./shared.js";
import { instantiate } from "./wasm/loader.generated.js";
const JSR_URL = dntShim.Deno.env.get("JSR_URL") ?? "https://jsr.io";
async function readLockfile(path) {
try {
const data = await dntShim.Deno.readTextFile(path);
const instance = instantiate();
return new instance.WasmLockfile(path, data);
}
catch (err) {
if (err instanceof dntShim.Deno.errors.NotFound) {
return null;
}
throw err;
}
}
export class PortableLoader {
#options;
#fetchOngoing = new Map();
#lockfile;
#fetchModules = new Map();
#fetchRedirects = new Map();
constructor(options) {
this.#options = options;
}
[Symbol.dispose]() {
if (this.#lockfile != null && "free" in this.#lockfile) {
this.#lockfile.free();
}
}
async resolve(specifier) {
switch (specifier.protocol) {
case "file:": {
return { kind: "esm", specifier };
}
case "http:":
case "https:":
case "data:": {
const module = await this.#loadRemote(specifier.href);
return { kind: "esm", specifier: new URL(module.specifier) };
}
case "npm:": {
const npmSpecifier = parseNpmSpecifier(specifier);
return {
kind: "npm",
packageId: "",
packageName: npmSpecifier.name,
path: npmSpecifier.path ?? "",
};
}
case "node:": {
return { kind: "node", path: specifier.pathname };
}
case "jsr:": {
const resolvedSpecifier = await this.#resolveJsrSpecifier(specifier);
return { kind: "esm", specifier: resolvedSpecifier };
}
default:
throw new Error(`Unsupported scheme: '${specifier.protocol}'`);
}
}
async #resolveJsrSpecifier(specifier) {
// parse the JSR specifier.
const jsrSpecifier = parseJsrSpecifier(specifier);
// Attempt to load the lockfile.
if (this.#lockfile === undefined) {
this.#lockfile = typeof this.#options.lock === "string"
? readLockfile(this.#options.lock)
: null;
}
if (this.#lockfile instanceof Promise) {
this.#lockfile = await this.#lockfile;
}
if (this.#lockfile === null) {
throw new Error("jsr: specifiers are not supported in the portable loader without a lockfile");
}
const lockfile = this.#lockfile;
// Look up the package + constraint in the lockfile.
const id = `jsr:${jsrSpecifier.name}${jsrSpecifier.version ? `@${jsrSpecifier.version}` : ""}`;
const resolvedVersion = lockfile.package_version(id);
if (!resolvedVersion) {
throw new Error(`Specifier not found in lockfile: ${id}`);
}
// Load the JSR manifest to find the export path.
const manifestUrl = new URL(`./${jsrSpecifier.name}/${resolvedVersion}_meta.json`, JSR_URL);
const manifest = await this.#loadRemote(manifestUrl.href);
if (manifest.mediaType !== "Json") {
throw new Error(`Expected JSON media type for JSR manifest, got: ${manifest.mediaType}`);
}
const manifestData = new TextDecoder().decode(manifest.data);
const manifestJson = JSON.parse(manifestData);
// Look up the export path in the manifest.
const exportEntry = `.${jsrSpecifier.path ?? ""}`;
const exportPath = manifestJson.exports[exportEntry];
if (!exportPath) {
throw new Error(`Package 'jsr:${jsrSpecifier.name}@${resolvedVersion}' has no export named '${exportEntry}'`);
}
// Return the resolved URL.
return new URL(`./${jsrSpecifier.name}/${resolvedVersion}/${exportPath}`, JSR_URL);
}
async loadEsm(url) {
let module;
switch (url.protocol) {
case "file:": {
module = await this.#loadLocal(url);
break;
}
case "http:":
case "https:":
case "data:": {
module = await this.#loadRemote(url.href);
break;
}
default:
throw new Error("[unreachable] unsupported esm scheme " + url.protocol);
}
const loader = mediaTypeToLoader(module.mediaType);
if (loader === null)
return undefined;
const res = { contents: module.data, loader };
if (url.protocol === "file:") {
res.watchFiles = [fromFileUrl(module.specifier)];
}
return res;
}
#resolveRemote(specifier) {
return this.#fetchRedirects.get(specifier) ?? specifier;
}
async #loadRemote(specifier) {
for (let i = 0; i < 10; i++) {
specifier = this.#resolveRemote(specifier);
const module = this.#fetchModules.get(specifier);
if (module)
return module;
let promise = this.#fetchOngoing.get(specifier);
if (!promise) {
promise = this.#fetch(specifier);
this.#fetchOngoing.set(specifier, promise);
}
await promise;
}
throw new Error("Too many redirects. Last one: " + specifier);
}
async #fetch(specifier) {
const resp = await fetch(specifier, {
redirect: "manual",
});
if (resp.status < 200 && resp.status >= 400) {
throw new Error(`Encountered status code ${resp.status} while fetching ${specifier}.`);
}
if (resp.status >= 300 && resp.status < 400) {
await resp.body?.cancel();
const location = resp.headers.get("location");
if (!location) {
throw new Error(`Redirected without location header while fetching ${specifier}.`);
}
const url = new URL(location, specifier);
if (url.protocol !== "https:" && url.protocol !== "http:") {
throw new Error(`Redirected to unsupported protocol '${url.protocol}' while fetching ${specifier}.`);
}
this.#fetchRedirects.set(specifier, url.href);
return;
}
const contentType = resp.headers.get("content-type");
const mediaType = mapContentType(new URL(specifier), contentType);
const data = new Uint8Array(await resp.arrayBuffer());
this.#fetchModules.set(specifier, {
specifier,
mediaType,
data,
});
}
async #loadLocal(specifier) {
const path = fromFileUrl(specifier);
const mediaType = mapContentType(specifier, null);
const data = await dntShim.Deno.readFile(path);
return { specifier: specifier.href, mediaType, data };
}
}