diff --git a/lib/dependency.ts b/lib/dependency.ts index 96fa0ac4..ff28a7eb 100644 --- a/lib/dependency.ts +++ b/lib/dependency.ts @@ -148,62 +148,64 @@ export function toUrl(dependency: Dependency): string { */ export async function resolveLatestVersion( dependency: Dependency, + options?: { cache?: boolean }, ): Promise { - 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; + } + } + 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(); static #cache = new Map(); - static lock(name: string): Promise { - 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( + set( 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 { - 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( @@ -221,10 +223,7 @@ async function _resolveLatestVersion( 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:": { @@ -248,10 +247,7 @@ async function _resolveLatestVersion( if (latest === dependency.version || isPreRelease(latest)) { break; } - return LatestVersionCache.set( - dependency.name, - { ...dependency, version: latest }, - ); + return { ...dependency, version: latest }; } case "http:": case "https:": { @@ -263,17 +259,19 @@ async function _resolveLatestVersion( 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({ diff --git a/lib/testing.ts b/lib/testing.ts index 9b708640..851a5208 100644 --- a/lib/testing.ts +++ b/lib/testing.ts @@ -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(/\/$/, ""), }; } diff --git a/lib/update.ts b/lib/update.ts index 603b0a0c..6ddc5d04 100644 --- a/lib/update.ts +++ b/lib/update.ts @@ -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. @@ -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); }) @@ -218,7 +223,7 @@ const load: NonNullable = async ( async function _createDependencyUpdate( dependencyJson: DependencyJson, referrer: string, - options?: Pick & { + options?: Pick & { importMap?: ImportMap; }, ): Promise { @@ -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; } @@ -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. @@ -275,7 +282,7 @@ async function _createDependencyUpdate( async function _collectFromImportMap( specifier: ModuleJson["specifier"], - options: Pick, + options: Pick, ): Promise { const json = await readImportMapJson(new URL(specifier)); const updates: DependencyUpdate[] = []; @@ -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, @@ -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; diff --git a/lib/update_test.ts b/lib/update_test.ts index 7580d38d..7b0d2692 100644 --- a/lib/update_test.ts +++ b/lib/update_test.ts @@ -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( diff --git a/test/snapshots/file_test.ts.snap b/test/snapshots/file_test.ts.snap index 7e358172..0c115558 100644 --- a/test/snapshots/file_test.ts.snap +++ b/test/snapshots/file_test.ts.snap @@ -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: { diff --git a/test/snapshots/update_test.ts.snap b/test/snapshots/update_test.ts.snap index dddc5fd9..3bd6094b 100644 --- a/test/snapshots/update_test.ts.snap +++ b/test/snapshots/update_test.ts.snap @@ -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",