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

fix: preserve trailing slashes in import URLs #139

Merged
merged 4 commits into from
Mar 6, 2024
Merged
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
100 changes: 49 additions & 51 deletions lib/dependency.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,62 +148,64 @@
*/
export async function resolveLatestVersion(
dependency: Dependency,
options?: { cache?: boolean },
): Promise<UpdatedDependency | undefined> {
await LatestVersionCache.lock(dependency.name);
using cache = new LatestVersionCache(dependency.name);
if (options?.cache) {
const cached = cache.get(dependency.name);
if (cached) {
return { ...cached, path: dependency.path };
}
if (cached === null) {
// The dependency is already found to be up to date or unable to resolve.
return;
}

Check warning on line 162 in lib/dependency.ts

View check run for this annotation

Codecov / codecov/patch

lib/dependency.ts#L160-L162

Added lines #L160 - L162 were not covered by tests
}
const constraint = dependency.version
? SemVer.tryParseRange(dependency.version)
: undefined;
// Do not update inequality ranges.
if (constraint && constraint.flat().length > 1) {
return;
}
const result = await _resolveLatestVersion(dependency);
LatestVersionCache.unlock(dependency.name);
if (options?.cache) {
cache.set(dependency.name, result ?? null);
}
return result;
}

class LatestVersionCache {
class LatestVersionCache implements Disposable {
static #mutex = new Map<string, Mutex>();
static #cache = new Map<string, UpdatedDependency | null>();

static lock(name: string): Promise<void> {
const mutex = this.#mutex.get(name) ??
this.#mutex.set(name, new Mutex()).get(name)!;
return mutex.acquire();
}

static unlock(name: string): void {
const mutex = this.#mutex.get(name);
assertExists(mutex);
mutex.release();
constructor(readonly name: string) {
const mutex = LatestVersionCache.#mutex.get(name) ??
LatestVersionCache.#mutex.set(name, new Mutex()).get(name)!;
mutex.acquire();
}

static get(name: string): UpdatedDependency | null | undefined {
return this.#cache.get(name);
get(name: string): UpdatedDependency | null | undefined {
return LatestVersionCache.#cache.get(name);
}

static set<T extends UpdatedDependency | null>(
set<T extends UpdatedDependency | null>(
name: string,
dependency: T,
): T {
this.#cache.set(name, dependency);
return dependency;
): void {
LatestVersionCache.#cache.set(name, dependency);
}

[Symbol.dispose]() {
const mutex = LatestVersionCache.#mutex.get(this.name);
assertExists(mutex);
mutex.release();
}
}

async function _resolveLatestVersion(
dependency: Dependency,
): Promise<UpdatedDependency | undefined> {
const cached = LatestVersionCache.get(dependency.name);
if (cached) {
return { ...cached, path: dependency.path };
}
if (cached === null) {
// The dependency is already found to be up to date or unable to resolve.
return;
}
const constraint = dependency.version
? SemVer.tryParseRange(dependency.version)
: undefined;

// Do not update inequality ranges.
if (constraint && constraint.flat().length > 1) {
return;
}

switch (dependency.protocol) {
case "npm:": {
const response = await fetch(
Expand All @@ -221,10 +223,7 @@
if (latest === dependency.version || isPreRelease(latest)) {
break;
}
return LatestVersionCache.set(
dependency.name,
{ ...dependency, version: latest },
);
return { ...dependency, version: latest };
}
// Ref: https://jsr.io/docs/api#jsr-registry-api
case "jsr:": {
Expand All @@ -248,10 +247,7 @@
if (latest === dependency.version || isPreRelease(latest)) {
break;
}
return LatestVersionCache.set(
dependency.name,
{ ...dependency, version: latest },
);
return { ...dependency, version: latest };
}
case "http:":
case "https:": {
Expand All @@ -263,17 +259,19 @@
if (!response.redirected) {
break;
}
const latest = parse(response.url);
if (!latest.version || isPreRelease(latest.version)) {
const redirected = parse(response.url);
if (!redirected.version || isPreRelease(redirected.version)) {
break;
}
return LatestVersionCache.set(
dependency.name,
latest as UpdatedDependency,
);
const latest = redirected as UpdatedDependency;
return {
...latest,
// Preserve the original path if it is the root, which is unlikely to be
// included in the redirected URL.
path: dependency.path === "/" ? "/" : latest.path,
};
}
}
LatestVersionCache.set(dependency.name, null);
}

const isNpmPackageMeta = is.ObjectOf({
Expand Down
4 changes: 3 additions & 1 deletion lib/testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,9 @@ function parseDenoLandUrl(url: URL) {
return {
name: std ? "deno.land/std" : `deno.land/x/${name}`,
version,
path,
// Remove a trailing slash if it exists to imitate the behavior of typical
// Web servers.
path: path.replace(/\/$/, ""),
};
}

Expand Down
36 changes: 29 additions & 7 deletions lib/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ class DenoGraph {
}

export interface CollectOptions {
/**
* Whether to use the cache to resolve dependencies.
* @default true
*/
cache?: boolean;
/**
* The working directory to resolve relative paths.
* If not specified, the current working directory is used.
Expand Down Expand Up @@ -167,7 +172,7 @@ export async function collect(
const update = await _createDependencyUpdate(
dependency,
mod.specifier,
{ ...options, importMap },
{ cache: true, ...options, importMap },
);
if (update) updates.push(update);
})
Expand Down Expand Up @@ -218,7 +223,7 @@ const load: NonNullable<CreateGraphOptions["load"]> = async (
async function _createDependencyUpdate(
dependencyJson: DependencyJson,
referrer: string,
options?: Pick<CollectOptions, "ignore" | "only"> & {
options?: Pick<CollectOptions, "cache" | "ignore" | "only"> & {
importMap?: ImportMap;
},
): Promise<DependencyUpdate | undefined> {
Expand All @@ -243,7 +248,9 @@ async function _createDependencyUpdate(
if (options?.only && !options.only(dependency)) {
return;
}
const latest = await resolveLatestVersion(dependency);
const latest = await resolveLatestVersion(dependency, {
cache: options?.cache,
});
if (!latest || latest.version === dependency.version) {
return;
}
Expand All @@ -256,7 +263,7 @@ async function _createDependencyUpdate(
);
}
return {
from: dependency,
from: normalizeWithUpdated(dependency, latest),
to: latest,
code: {
// We prefer to put the original specifier here.
Expand All @@ -275,7 +282,7 @@ async function _createDependencyUpdate(

async function _collectFromImportMap(
specifier: ModuleJson["specifier"],
options: Pick<CollectOptions, "ignore" | "only">,
options: Pick<CollectOptions, "cache" | "ignore" | "only">,
): Promise<DependencyUpdate[]> {
const json = await readImportMapJson(new URL(specifier));
const updates: DependencyUpdate[] = [];
Expand All @@ -292,12 +299,14 @@ async function _collectFromImportMap(
if (options.only && !options.only(dependency)) {
return;
}
const latest = await resolveLatestVersion(dependency);
const latest = await resolveLatestVersion(dependency, {
cache: options.cache,
});
if (!latest || latest.version === dependency.version) {
return;
}
updates.push({
from: dependency,
from: normalizeWithUpdated(dependency, latest),
to: latest,
code: {
specifier: value,
Expand All @@ -317,6 +326,19 @@ async function _collectFromImportMap(
return updates;
}

function normalizeWithUpdated(
dependency: Dependency,
updated: UpdatedDependency,
): Dependency {
if (dependency.version) {
return dependency;
}
return {
...updated,
version: undefined,
};
}

export type VersionChange = {
from?: string;
to: string;
Expand Down
1 change: 1 addition & 0 deletions lib/update_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ async function test(
) {
try {
const updates = await collect(new URL(path, import.meta.url), {
cache: false,
cwd: new URL(dirname(path), import.meta.url),
});
Deno.test(
Expand Down
5 changes: 3 additions & 2 deletions test/snapshots/file_test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1159,9 +1159,10 @@ snapshot[`associateByFile - unversioned.ts 1`] = `
specifier: "https://deno.land/std/assert/assert.ts",
},
from: {
name: "deno.land/std/assert/assert.ts",
path: "",
name: "deno.land/std",
path: "/assert/assert.ts",
protocol: "https:",
version: undefined,
},
map: undefined,
to: {
Expand Down
5 changes: 3 additions & 2 deletions test/snapshots/update_test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -865,9 +865,10 @@ snapshot[`collect - unversioned.ts 1`] = `
specifier: "https://deno.land/std/assert/assert.ts",
},
from: {
name: "deno.land/std/assert/assert.ts",
path: "",
name: "deno.land/std",
path: "/assert/assert.ts",
protocol: "https:",
version: undefined,
},
to: {
name: "deno.land/std",
Expand Down
Loading