UNPKG

@oazmi/esbuild-plugin-deno

Version:

A portable non-invasive suite of esbuild plugins for loading http, jsr, npm, and import-maps. Supports Deno, Node, Bun, Web. Alternate to @luca/esbuild-deno-loader , while compatible with other plugins and native esbuild resolvers (e.g. css loader).

220 lines (219 loc) 14.9 kB
/** a utility submodule for resolving the import/export-map aliases of deno and [jsr](https://jsr.io) packages. * * @module * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * const my_deno_json: DenoJsonSchema = { * name: "@scope/lib", * version: "0.1.0", * exports: { * ".": "./src/mod.ts", * "./hello": "./src/nyaa.ts", * "./world": "./src/ligma.ts", * "./utils/cli/": "./src/cli/", * }, * imports: { * "my-lib": "jsr:@scope/my-lib", * "my-lib-types": "jsr:@scope/my-lib/typedefs", * "jsr-pkg": "jsr:@scope/jsr-pkg", * "jsr-pkg/": "jsr:@scope/jsr-pkg/dir/", * "npm-pkg": "npm:boomer-package", * "npm-pkg-utils": "npm:boomer-package/utilities", * } * } * * const pkg_metadata = new DenoPackage(my_deno_json, "") * * // aliasing our functions, methods, and configurations for brevity * const * eq = assertEquals, * resIm = pkg_metadata.resolveImport.bind(pkg_metadata), * resEx = pkg_metadata.resolveExport.bind(pkg_metadata), * config_1 = { basePathDir: "" }, * config_2 = { baseAliasDir: "jsr:@scope/lib" }, * config_3 = { baseAliasDir: "", basePathDir: "" } * * * // testing out the import alias-path resolution of the package own export-map (i.e. self-referenced imports). * eq(resIm("@scope/lib"), "https://jsr.io/@scope/lib/0.1.0/src/mod.ts") * eq(resIm("@scope/lib/"), "https://jsr.io/@scope/lib/0.1.0/src/mod.ts") * eq(resIm("@scope/lib", config_1), "./src/mod.ts") * eq(resIm("@scope/lib", { basePathDir: "./" }), "./src/mod.ts") * // the result below is `undefined` because, internally, `resolveImport` will only concern itself with * // self-references that deno itself recognizes, and not just any arbitrary `baseAliasDir`. * // even though this path alias would get resolved by the `resolveExport` method (which you will see later). * eq(resIm("SELF", { baseAliasDir: "SELF" }), undefined) * eq(resIm("@scope/lib/hello", config_1), "./src/nyaa.ts") * eq(resIm("@scope/lib/world", config_1), "./src/ligma.ts") * eq(resIm("@scope/lib/utils/cli/", config_1), "./src/cli/") * eq(resIm("@scope/lib/utils/cli/script.ts", config_1), "./src/cli/script.ts") * eq(resIm("@scope/lib/utils/cli/../../hello", config_1), "./src/nyaa.ts") * eq(resIm("@scope/lib/utils/cli/../../hello.js", config_1), undefined) * eq(resIm("jsr:@scope/lib", config_1), "./src/mod.ts") * eq(resIm("jsr:@scope/lib@0.1.0", config_1), "./src/mod.ts") * eq(resIm(".", config_1), "./src/mod.ts") * eq(resIm("./hello", config_1), "./src/nyaa.ts") * * // testing out the import alias-path resolution of the package's externally referenced import-map entries. * eq(resIm("my-lib"), "jsr:@scope/my-lib") * eq(resIm("my-lib/"), "jsr:@scope/my-lib/") * eq(resIm("my-lib-types"), "jsr:@scope/my-lib/typedefs") * eq(resIm("my-lib/funcdefs"), "jsr:@scope/my-lib/funcdefs") * eq(resIm("jsr-pkg"), "jsr:@scope/jsr-pkg") * eq(resIm("jsr-pkg/"), "jsr:@scope/jsr-pkg/dir/") * eq(resIm("jsr-pkg/file"), "jsr:@scope/jsr-pkg/dir/file") * eq(resIm("npm-pkg"), "npm:boomer-package") * eq(resIm("npm-pkg-utils"), "npm:boomer-package/utilities") * eq(resIm("npm-pkg/utils/cli"), "npm:boomer-package/utils/cli") * * // testing out the alias-path resolution of the package's exported entries. * eq(resEx("jsr:@scope/lib"), undefined) // by default, you must provide the version number as well * eq(resEx("jsr:@scope/lib", config_2), "https://jsr.io/@scope/lib/0.1.0/src/mod.ts") * eq(resEx("jsr:@scope/lib/", config_2), "https://jsr.io/@scope/lib/0.1.0/src/mod.ts") * eq(resEx("jsr:@scope/lib@0.1.0", config_1), "./src/mod.ts") * eq(resEx(".", config_3), "./src/mod.ts") * eq(resEx(".", { baseAliasDir: "" }), "https://jsr.io/@scope/lib/0.1.0/src/mod.ts") * eq(resEx("SELF", { baseAliasDir: "SELF" }), "https://jsr.io/@scope/lib/0.1.0/src/mod.ts") * eq(resEx("jsr:@scope/lib@0.1.0"), "https://jsr.io/@scope/lib/0.1.0/src/mod.ts") * eq(resEx("jsr:@scope/lib@0.1.0/"), "https://jsr.io/@scope/lib/0.1.0/src/mod.ts") * eq(resEx("jsr:@scope/lib@0.1.0/hello"), "https://jsr.io/@scope/lib/0.1.0/src/nyaa.ts") * eq(resEx("jsr:@scope/lib@0.1.0/world"), "https://jsr.io/@scope/lib/0.1.0/src/ligma.ts") * eq(resEx("jsr:@scope/lib@0.1.0/utils/cli/"), "https://jsr.io/@scope/lib/0.1.0/src/cli/") * eq(resEx("jsr:@scope/lib@0.1.0/utils/cli/file.js"), "https://jsr.io/@scope/lib/0.1.0/src/cli/file.js") * ``` */ import { defaultFetchConfig, ensureEndSlash, isString, json_stringify, memorize, normalizePath, object_entries, parsePackageUrl, pathToPosixPath, replacePrefix, resolveAsUrl, semverMaxSatisfying } from "../deps.js"; import { compareImportMapEntriesByLength } from "../importmap/mod.js"; import { RuntimePackage } from "./base.js"; const get_dir_path_of_file_path = (file_path) => normalizePath(file_path.endsWith("/") ? file_path : file_path + "/../"); export class DenoPackage extends RuntimePackage { importMapSortedEntries; exportMapSortedEntries; getName() { return this.packageInfo.name; } getVersion() { return this.packageInfo.version; } getPath() { const package_path = this.packagePath; return package_path ? package_path : `${jsr_base_url}/${this.getName()}/${this.getVersion()}/deno.json`; } constructor(package_object, package_path) { super(package_object, package_path); const { exports = {}, imports = {} } = package_object, exports_object = isString(exports) ? (exports.endsWith("/") ? { "./": exports } : { ".": exports }) : exports, imports_object = { ...imports }; // below, we clone all non-directory aliases (such as "jsr:@scope/lib"), into directory aliases as well (i.e. "jsr:@scope/lib/"). // this is so that import statements with an alias + subpath (such as "jsr:@scope/lib/my-subpath") // can be resolved by the `resolveImport` method, instead of being declared unresolvable (`undefined`). // this is absolutely necessary for regular operation, although, strictly speaking, we are kind of malforming the original import map entries. for (const [alias, path] of object_entries(imports_object)) { const alias_dir_variant = ensureEndSlash(alias); // only assign the directory variant of the alias if such a key does not already exist in `imports_object`. // because otherwise, we would be overwriting the original package creator's own alias key (which would be intrusive). if (alias !== alias_dir_variant && !(alias_dir_variant in imports_object)) { imports_object[alias_dir_variant] = ensureEndSlash(path); } } this.exportMapSortedEntries = object_entries(exports_object).toSorted(compareImportMapEntriesByLength); this.importMapSortedEntries = object_entries(imports_object).toSorted(compareImportMapEntriesByLength); } resolveExport(path_alias, config) { const name = this.getName(), version = this.getVersion(), package_json_path = pathToPosixPath(this.getPath()), { baseAliasDir = `jsr:${name}@${version}`, basePathDir = get_dir_path_of_file_path(package_json_path), ...rest_config } = config ?? {}, residual_path_alias = replacePrefix(path_alias, baseAliasDir)?.replace(/^\/+/, "/"); if (residual_path_alias !== undefined) { path_alias = baseAliasDir + (residual_path_alias === "/" ? "" : residual_path_alias); } return super.resolveExport(path_alias, { baseAliasDir, basePathDir, ...rest_config }); } resolveImport(path_alias, config) { const name = this.getName(), version = this.getVersion(), package_json_path = pathToPosixPath(this.getPath()), basePathDir = get_dir_path_of_file_path(package_json_path), path_alias_is_relative = path_alias.startsWith("./") || path_alias.startsWith("../"), local_package_reference_aliases = path_alias_is_relative ? [""] : [`jsr:${name}@${version}`, `jsr:${name}`, `${name}`]; // resolving the `path_alias` as a locally self-referenced package export. // here is the list of possible combinations of base alias paths that can be performed within this package to reference its own export endpoints: // - "@scope/lib/pathname" // - "jsr:@scope/lib/pathname" // - "jsr:@scope/lib@version/pathname" let locally_resolved_export = undefined; for (const base_alias_dir of local_package_reference_aliases) { locally_resolved_export = this.resolveExport(path_alias, { ...config, baseAliasDir: base_alias_dir }); if (locally_resolved_export) { break; } } // if the `path_alias` is not a local export, then resolve it based on the package's import-map. // do note that if the import-map specifies an entry where the alias's path directs to a relative path (for instance: `"@server": "./src/server.ts"`), // then we would want the base-path to be the directory of the "deno.json" file (acquired via `this.getPath()`) return locally_resolved_export ?? super.resolveImport(path_alias, { ...config, basePathDir }); } static async fromUrl(jsr_package) { // TODO: ideally, we should also memorize the resulting instance of `DenoPackage` that gets created via this static method, // so that subsequent calls with the same `jsr_package` will return an existing instance. // it'll be nice if we could use a memorization decorator for such a thing, but I don't have any experience with writing them, so I'll look into it in the future. const package_jsonc_path_str = isString(jsr_package) ? jsr_package : jsr_package.href, url_is_jsr_protocol = package_jsonc_path_str.startsWith("jsr:"); if (url_is_jsr_protocol) { // by only extracting the hostname (and stripping away any `pathname`), // we get to reduce the number of inputs that our function will memorize. // (since the outputs are invariant of the pathname). const { host } = parsePackageUrl(jsr_package); jsr_package = await memorized_jsrPackageToMetadataUrl(`jsr:${host}`); } return super.fromUrl(jsr_package); } } const jsr_base_url = "https://jsr.io"; /** given a jsr schema uri (such as `jsr:@std/assert/assert-equals`), this function resolves the http url of the package's metadata file (i.e. `deno.json(c)`). * * @example * ```ts * import { assertEquals, assertMatch } from "jsr:@std/assert" * * // aliasing our functions for brevity * const * fn = jsrPackageToMetadataUrl, * eq = assertEquals, * re = assertMatch * * eq((await fn("jsr:@oazmi/kitchensink@0.9.1")).href, "https://jsr.io/@oazmi/kitchensink/0.9.1/deno.json") * eq((await fn("jsr:@oazmi/kitchensink@0.9.1/typedefs")).href, "https://jsr.io/@oazmi/kitchensink/0.9.1/deno.json") * re((await fn("jsr:@oazmi/kitchensink")).href, /^https:\/\/jsr.io\/@oazmi\/kitchensink\/.*?\/deno.json$/) * re((await fn("jsr:@oazmi/kitchensink/typedefs")).href, /^https:\/\/jsr.io\/@oazmi\/kitchensink\/.*?\/deno.json$/) * * // currently, in version `0.8`, we have the following release versions available: * // `["0.8.6", "0.8.5", "0.8.5-a", "0.8.4", "0.8.3", "0.8.3-d", "0.8.3-b", "0.8.3-a", "0.8.2", "0.8.1", "0.8.0"]` * // so, a query for version "^0.8.0" should return "0.8.6", and "<0.8.6" would return "0.8.5", etc... * eq((await fn("jsr:@oazmi/kitchensink@^0.8.0")).href, "https://jsr.io/@oazmi/kitchensink/0.8.6/deno.json") * eq((await fn("jsr:@oazmi/kitchensink@<0.8.6")).href, "https://jsr.io/@oazmi/kitchensink/0.8.5/deno.json") * eq((await fn("jsr:@oazmi/kitchensink@0.8.2 - 0.8.4")).href, "https://jsr.io/@oazmi/kitchensink/0.8.4/deno.json") * ``` */ export const jsrPackageToMetadataUrl = async (jsr_package) => { const { protocol, scope, pkg, pathname, version: desired_semver } = parsePackageUrl(jsr_package); if (protocol !== "jsr:") { throw new Error(`expected path protocol to be "jsr:", found "${protocol}" instead, for package: "${jsr_package}"`); } if (!scope) { throw new Error(`expected jsr package to contain a scope, but found "${scope}" instead, for package: "${jsr_package}"`); } const meta_json_url = resolveAsUrl(`@${scope}/${pkg}/meta.json`, jsr_base_url), meta_json = await (await fetch(meta_json_url, defaultFetchConfig)).json(), unyanked_versions = object_entries(meta_json.versions) .filter(([version_str, { yanked }]) => (!yanked)) .map(([version_str]) => version_str); // semantic version resolution const resolved_semver = semverMaxSatisfying(unyanked_versions, desired_semver ?? meta_json.latest); if (!resolved_semver) { throw new Error(`failed to find the desired version "${desired_semver}" of the jsr package "${jsr_package}", with available versions "${json_stringify(meta_json.versions)}"`); } const base_host = resolveAsUrl(`@${scope}/${pkg}/${resolved_semver}/`, jsr_base_url), deno_json_url = resolveAsUrl("./deno.json", base_host), deno_jsonc_url = resolveAsUrl("./deno.jsonc", base_host), jsr_json_url = resolveAsUrl("./jsr.json", base_host), jsr_jsonc_url = resolveAsUrl("./jsr.jsonc", base_host), package_json_url = resolveAsUrl("./package.json", base_host), package_jsonc_url = resolveAsUrl("./package.jsonc", base_host); // as if such a thing will ever exist, lol // TODO: the `package_json_url` (i.e. `package.json`) is unused for now, since it will complicate the parsing of the import/export-maps (due to having a different structure). // in the future, I might write a `npmPackageToDenoJson` function to transform the imports (dependencies) and exports // trying to fetch the package's `deno.json` file (via HEAD method), and if it fails (does not exist), // then we will try fetching `deno.jsonc`, `jsr.json`, and `jsr.jsonc` files, in order, as a replacement. const urls = [deno_json_url, deno_jsonc_url, jsr_json_url, jsr_jsonc_url]; for (const url of urls) { if ((await fetch(url, { ...defaultFetchConfig, method: "HEAD" })).ok) { return url; } } throw new Error(`Network Error: couldn't locate "${jsr_package}"'s package json file. searched in the following locations:\n${json_stringify(urls)}`); }; const memorized_jsrPackageToMetadataUrl = memorize(jsrPackageToMetadataUrl);