Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip: jsr support #372

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 26 additions & 5 deletions src/providers/deno.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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}"?`
);
Expand Down Expand Up @@ -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);
Expand All @@ -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,
Expand All @@ -215,6 +233,9 @@ export async function resolveLatestTarget(
): Promise<ExactPackage | null> {
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 };

Expand All @@ -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}`);
Expand Down
1 change: 1 addition & 0 deletions src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export function getDefaultProviderStrings() {
export const registryProviders: Record<string, string> = {
"denoland:": "deno",
"deno:": "deno",
"jsr:": "deno",
};

export const mappableSchemes = new Set<String>(["npm", "deno", "node"]);
Expand Down
183 changes: 183 additions & 0 deletions src/providers/shared.ts
Original file line number Diff line number Diff line change
@@ -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<ExactPackage | null>;
majors: Record<string, Promise<ExactPackage | null>>;
minors: Record<string, Promise<ExactPackage | null>>;
tags: Record<string, Promise<ExactPackage | null>>;
}
> = {};

// function clearResolveCache() {
// resolveCache = {};
// }

async function resolveLatestTarget(
this: Resolver,
target: LatestPackageTarget,
layer: string,
parentUrl: string
): Promise<ExactPackage | null> {
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 [email protected]@ 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<ExactPackage | null> {
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<string, string[]>();

async function fetchVersions(name: string): Promise<string[]> {
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;
}
2 changes: 1 addition & 1 deletion test/providers/deno.skipbrowser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import assert from "assert";
defaultProvider: "deno",
});

await generator.install("denoland:[email protected]/runtime.ts");
await generator.install("jsr:@fresh/core");
const json = generator.getMap();

assert.strictEqual(
Expand Down
Loading