diff --git a/src/providers/deno.ts b/src/providers/deno.ts index 838c433..b3fd3bc 100644 --- a/src/providers/deno.ts +++ b/src/providers/deno.ts @@ -9,8 +9,10 @@ import { SemverRange } from "sver"; import { fetch } from "#fetch"; import { Install } from "../generator.js"; import { IImportMap, ImportMap } from "@jspm/import-map"; +import { createNpmLookupFunction } from "./shared.js"; -const cdnUrl = "https://deno.land/x/"; +const jsrCdnUrl = "https://jsr.io/"; +const denoLandCdnUrl = "https://deno.land/x/"; const stdlibUrl = "https://deno.land/std"; let denoStdVersion; @@ -53,7 +55,9 @@ export function resolveBuiltin( export async function pkgToUrl(pkg: ExactPackage): Promise<`${string}/`> { if (pkg.registry === "deno") return `${stdlibUrl}@${pkg.version}/`; if (pkg.registry === "denoland") - return `${cdnUrl}${pkg.name}@${vCache[pkg.name] ? "v" : ""}${pkg.version}/`; + return `${denoLandCdnUrl}${pkg.name}@${vCache[pkg.name] ? "v" : ""}${pkg.version}/`; + if (pkg.registry === "jsr") + return `${jsrCdnUrl}${pkg.name}@${pkg.version}/`; throw new Error( `Deno provider does not support the ${pkg.registry} registry for package "${pkg.name}" - perhaps you mean to install "denoland:${pkg.name}"?` ); @@ -189,8 +193,8 @@ export function parseUrlPkg( subpath ? (`./${subpath}/mod.ts` as `./${string}`) : "" }`, }; - } else if (url.startsWith(cdnUrl)) { - const path = url.slice(cdnUrl.length); + } else if (url.startsWith(denoLandCdnUrl)) { + const path = url.slice(denoLandCdnUrl.length); const versionIndex = path.indexOf("@"); if (versionIndex === -1) return; const sepIndex = path.indexOf("/", versionIndex); @@ -204,9 +208,23 @@ export function parseUrlPkg( subpath: null, layer: "default", }; + } else if (url.startsWith(jsrCdnUrl)) { + const path = url.slice(jsrCdnUrl.length); + const versionIndex = path.indexOf("@"); + if (versionIndex === -1) return; + const sepIndex = path.indexOf("/", versionIndex); + const name = path.slice(0, versionIndex); + const version = path.slice(versionIndex + 1, sepIndex === -1 ? path.length : sepIndex); + return { + pkg: { registry: "jsr", name, version }, + subpath: sepIndex === -1 ? null : `./${path.slice(sepIndex + 1)}`, + layer: "default", + }; } } +const resolveLatestTargetJsr = createNpmLookupFunction('https://npm.jsr.io/'); + export async function resolveLatestTarget( this: Resolver, target: LatestPackageTarget, @@ -215,6 +233,9 @@ export async function resolveLatestTarget( ): Promise { let { registry, name, range } = target; + if (target.registry === 'jsr') + return resolveLatestTargetJsr.call(this, target, _layer, parentUrl); + if (denoStdVersion && registry === "deno") return { registry, name, version: denoStdVersion }; @@ -236,7 +257,7 @@ export async function resolveLatestTarget( // "mod.ts" addition is necessary for the browser otherwise not resolving an exact module gives a CORS error const fetchUrl = registry === "denoland" - ? cdnUrl + name + "/mod.ts" + ? denoLandCdnUrl + name + "/mod.ts" : stdlibUrl + "/version.ts"; const res = await fetch(fetchUrl, fetchOpts); if (!res.ok) throw new Error(`Deno: Unable to lookup ${fetchUrl}`); diff --git a/src/providers/index.ts b/src/providers/index.ts index b4556a1..57b256c 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -81,6 +81,7 @@ export function getDefaultProviderStrings() { export const registryProviders: Record = { "denoland:": "deno", "deno:": "deno", + "jsr:": "deno", }; export const mappableSchemes = new Set(["npm", "deno", "node"]); diff --git a/src/providers/shared.ts b/src/providers/shared.ts new file mode 100644 index 0000000..249a62c --- /dev/null +++ b/src/providers/shared.ts @@ -0,0 +1,183 @@ +import { JspmError } from '../common/err.js'; +import { importedFrom } from '../common/url.js'; +import { ExactPackage, LatestPackageTarget } from '../install/package.js'; +import { Resolver } from '../trace/resolver.js'; +import { Provider } from './index.js'; +import { SemverRange } from 'sver'; + +export function createNpmLookupFunction (registryHost: `${string}/`): Provider['resolveLatestTarget'] { + let resolveCache: Record< + string, + { + latest: Promise; + majors: Record>; + minors: Record>; + tags: Record>; + } + > = {}; + + // function clearResolveCache() { + // resolveCache = {}; + // } + + async function resolveLatestTarget( + this: Resolver, + target: LatestPackageTarget, + layer: string, + parentUrl: string + ): Promise { + const { registry, name, range, unstable } = target; + + // exact version optimization + if (range.isExact && !range.version.tag) { + const pkg = { registry, name, version: range.version.toString() }; + return pkg; + } + + const cache = (resolveCache[target.registry + ":" + target.name] = + resolveCache[target.registry + ":" + target.name] || { + latest: null, + majors: Object.create(null), + minors: Object.create(null), + tags: Object.create(null), + }); + + if (range.isWildcard || (range.isExact && range.version.tag === "latest")) { + let lookup = await (cache.latest || + (cache.latest = lookupRange.call( + this, + registry, + name, + "", + unstable, + parentUrl + ))); + // Deno wat? + if (lookup instanceof Promise) lookup = await lookup; + if (!lookup) return null; + this.log( + "jspm/resolveLatestTarget", + `${target.registry}:${target.name}@${range} -> WILDCARD ${ + lookup.version + }${parentUrl ? " [" + parentUrl + "]" : ""}` + ); + return lookup; + } + if (range.isExact && range.version.tag) { + const tag = range.version.tag; + let lookup = await (cache.tags[tag] || + (cache.tags[tag] = lookupRange.call( + this, + registry, + name, + tag, + unstable, + parentUrl + ))); + // Deno wat? + if (lookup instanceof Promise) lookup = await lookup; + if (!lookup) return null; + this.log( + "jspm/resolveLatestTarget", + `${target.registry}:${target.name}@${range} -> TAG ${tag}${ + parentUrl ? " [" + parentUrl + "]" : "" + }` + ); + return lookup; + } + let stableFallback = false; + if (range.isMajor) { + const major = range.version.major; + let lookup = await (cache.majors[major] || + (cache.majors[major] = lookupRange.call( + this, + registry, + name, + major, + unstable, + parentUrl + ))); + // Deno wat? + if (lookup instanceof Promise) lookup = await lookup; + if (!lookup) return null; + // if the latest major is actually a downgrade, use the latest minor version (fallthrough) + // note this might miss later major prerelease versions, which should strictly be supported via a pkg@X@ unstable major lookup + if (range.version.gt(lookup.version)) { + stableFallback = true; + } else { + this.log( + "jspm/resolveLatestTarget", + `${target.registry}:${target.name}@${range} -> MAJOR ${lookup.version}${ + parentUrl ? " [" + parentUrl + "]" : "" + }` + ); + return lookup; + } + } + if (stableFallback || range.isStable) { + const minor = `${range.version.major}.${range.version.minor}`; + let lookup = await (cache.minors[minor] || + (cache.minors[minor] = lookupRange.call( + this, + registry, + name, + minor, + unstable, + parentUrl + ))); + // in theory a similar downgrade to the above can happen for stable prerelease ranges ~1.2.3-pre being downgraded to 1.2.2 + // this will be solved by the pkg@X.Y@ unstable minor lookup + // Deno wat? + if (lookup instanceof Promise) lookup = await lookup; + if (!lookup) return null; + this.log( + "jspm/resolveLatestTarget", + `${target.registry}:${target.name}@${range} -> MINOR ${lookup.version}${ + parentUrl ? " [" + parentUrl + "]" : "" + }` + ); + return lookup; + } + return null; + } + + async function lookupRange( + this: Resolver, + registry: string, + name: string, + range: string, + unstable: boolean, + parentUrl?: string + ): Promise { + const versions = await fetchVersions(name); + const semverRange = new SemverRange(String(range) || "*", unstable); + const version = semverRange.bestMatch(versions, unstable); + + if (version) { + return { registry, name, version: version.toString() }; + } + throw new JspmError( + `Unable to resolve ${registry}:${name}@${range} to a valid version${importedFrom( + parentUrl + )}` + ); + } + + const versionsCacheMap = new Map(); + + async function fetchVersions(name: string): Promise { + if (versionsCacheMap.has(name)) { + return versionsCacheMap.get(name); + } + console.log(`${registryHost}${encodeURI(name)}`); + const registryLookup = await ( + await fetch(`${registryHost}${encodeURI(name)}`, {}) + ).json(); + const versions = Object.keys(registryLookup.versions || {}); + versionsCacheMap.set(name, versions); + + return versions; + } + + return resolveLatestTarget; +} diff --git a/test/providers/deno.skipbrowser.test.js b/test/providers/deno.skipbrowser.test.js index 1d3fead..885464d 100644 --- a/test/providers/deno.skipbrowser.test.js +++ b/test/providers/deno.skipbrowser.test.js @@ -6,7 +6,7 @@ import assert from "assert"; defaultProvider: "deno", }); - await generator.install("denoland:fresh@1.1.5/runtime.ts"); + await generator.install("jsr:@fresh/core"); const json = generator.getMap(); assert.strictEqual(