From 4cd17799ac8d82573156e99b6a6ceeb7efe0b489 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Mon, 15 Jul 2024 20:25:59 +0530 Subject: [PATCH] chore: update script now supports bumping deps in files --- deno.lock | 89 ++-------- deps.ts | 4 +- dev_deps.ts | 22 ++- scripts/release.ts | 53 +++--- scripts/update.ts | 406 ++++++++++++++++++++++++++++++++++++++++++--- 5 files changed, 441 insertions(+), 133 deletions(-) diff --git a/deno.lock b/deno.lock index 2ffd876..1dfd100 100644 --- a/deno.lock +++ b/deno.lock @@ -6,53 +6,43 @@ "jsr:@cliffy/internal@1.0.0-rc.5": "jsr:@cliffy/internal@1.0.0-rc.5", "jsr:@cliffy/keycode@1.0.0-rc.5": "jsr:@cliffy/keycode@1.0.0-rc.5", "jsr:@cliffy/prompt@^1.0.0-rc.5": "jsr:@cliffy/prompt@1.0.0-rc.5", - "jsr:@core/unknownutil@^3.18.0": "jsr:@core/unknownutil@3.18.1", + "jsr:@core/unknownutil@^3.18.1": "jsr:@core/unknownutil@3.18.1", "jsr:@david/dax@^0.41.0": "jsr:@david/dax@0.41.0", "jsr:@david/which@^0.4.1": "jsr:@david/which@0.4.1", - "jsr:@deno/graph@^0.73.1": "jsr:@deno/graph@0.73.1", + "jsr:@deno/graph@^0.80.1": "jsr:@deno/graph@0.80.1", "jsr:@lambdalisue/async@^2.1.1": "jsr:@lambdalisue/async@2.1.1", - "jsr:@molt/core@^0.18.5": "jsr:@molt/core@0.18.5", - "jsr:@molt/lib@^0.18.0": "jsr:@molt/lib@0.18.5", "jsr:@std/assert@1.0.0-rc.2": "jsr:@std/assert@1.0.0-rc.2", "jsr:@std/assert@^0.221.0": "jsr:@std/assert@0.221.0", - "jsr:@std/assert@^0.222.1": "jsr:@std/assert@0.222.1", "jsr:@std/assert@^1.0.0": "jsr:@std/assert@1.0.0", "jsr:@std/async@^1.0.0-rc.4": "jsr:@std/async@1.0.0-rc.4", "jsr:@std/bytes@^0.221.0": "jsr:@std/bytes@0.221.0", "jsr:@std/cli@^1.0.0-rc.5": "jsr:@std/cli@1.0.0-rc.5", - "jsr:@std/collections@^0.222.1": "jsr:@std/collections@0.222.1", + "jsr:@std/collections@^1.0.4": "jsr:@std/collections@1.0.4", "jsr:@std/encoding@1.0.0-rc.2": "jsr:@std/encoding@1.0.0-rc.2", "jsr:@std/encoding@^1.0.1": "jsr:@std/encoding@1.0.1", "jsr:@std/fmt@^0.221.0": "jsr:@std/fmt@0.221.0", - "jsr:@std/fmt@^0.222.1": "jsr:@std/fmt@0.222.1", "jsr:@std/fmt@^1.0.0-rc.1": "jsr:@std/fmt@1.0.0-rc.1", "jsr:@std/fmt@~0.225.4": "jsr:@std/fmt@0.225.6", "jsr:@std/fs@0.221.0": "jsr:@std/fs@0.221.0", - "jsr:@std/fs@^0.222.1": "jsr:@std/fs@0.222.1", "jsr:@std/fs@^1.0.0-rc.5": "jsr:@std/fs@1.0.0-rc.5", "jsr:@std/http@^1.0.0-rc.4": "jsr:@std/http@1.0.0-rc.4", "jsr:@std/internal@^1.0.1": "jsr:@std/internal@1.0.1", "jsr:@std/io@0.221.0": "jsr:@std/io@0.221.0", "jsr:@std/io@^0.221.0": "jsr:@std/io@0.221.0", "jsr:@std/io@~0.224.2": "jsr:@std/io@0.224.3", - "jsr:@std/json@^0.222.1": "jsr:@std/json@0.222.1", - "jsr:@std/jsonc@^0.222.1": "jsr:@std/jsonc@0.222.1", "jsr:@std/media-types@^1.0.1": "jsr:@std/media-types@1.0.1", "jsr:@std/net@^1.0.0-rc.1": "jsr:@std/net@1.0.0-rc.1", "jsr:@std/path@0.221.0": "jsr:@std/path@0.221.0", "jsr:@std/path@1.0.0-rc.2": "jsr:@std/path@1.0.0-rc.2", "jsr:@std/path@^0.221.0": "jsr:@std/path@0.221.0", - "jsr:@std/path@^0.222.1": "jsr:@std/path@0.222.1", "jsr:@std/path@^1.0.0": "jsr:@std/path@1.0.0", "jsr:@std/path@^1.0.0-rc.4": "jsr:@std/path@1.0.0", "jsr:@std/regexp@^1.0.0": "jsr:@std/regexp@1.0.0", "jsr:@std/regexp@^1.0.0-rc.1": "jsr:@std/regexp@1.0.0", - "jsr:@std/semver@^0.222.1": "jsr:@std/semver@0.222.1", "jsr:@std/semver@^1.0.0-rc.2": "jsr:@std/semver@1.0.0-rc.2", "jsr:@std/streams@0.221.0": "jsr:@std/streams@0.221.0", "jsr:@std/streams@^1.0.0-rc.3": "jsr:@std/streams@1.0.0-rc.3", "jsr:@std/text@1.0.0-rc.1": "jsr:@std/text@1.0.0-rc.1", - "npm:@types/node": "npm:@types/node@18.16.19", "npm:chokidar@^3.6.0": "npm:chokidar@3.6.0", "npm:zod@^3.23.8": "npm:zod@3.23.8" }, @@ -99,39 +89,15 @@ "@david/which@0.4.1": { "integrity": "896a682b111f92ab866cc70c5b4afab2f5899d2f9bde31ed00203b9c250f225e" }, - "@deno/graph@0.73.1": { - "integrity": "cd69639d2709d479037d5ce191a422eabe8d71bb68b0098344f6b07411c84d41" + "@deno/graph@0.80.1": { + "integrity": "a7aca57c5f91ce41e01b788a1dad591734df7f0d2310cf072800cdc57986df23" }, "@lambdalisue/async@2.1.1": { "integrity": "1fc9bc6f4ed50215cd2f7217842b18cea80f81c25744f88f8c5eb4be5a1c9ab4" }, - "@molt/core@0.18.5": { - "integrity": "42bc81de1af0ffd728626bfd99350c0f01ddec53988a8d069561009953a55823", - "dependencies": [ - "jsr:@core/unknownutil@^3.18.0", - "jsr:@deno/graph@^0.73.1", - "jsr:@lambdalisue/async@^2.1.1", - "jsr:@molt/lib@^0.18.0", - "jsr:@std/assert@^0.222.1", - "jsr:@std/collections@^0.222.1", - "jsr:@std/fs@^0.222.1", - "jsr:@std/jsonc@^0.222.1", - "jsr:@std/path@^0.222.1", - "jsr:@std/semver@^0.222.1" - ] - }, - "@molt/lib@0.18.5": { - "integrity": "dfbcf01f9c57f57d0fc2581ec8ac087f933f17da106feaaf60feced2e8cc6eab" - }, "@std/assert@0.221.0": { "integrity": "a5f1aa6e7909dbea271754fd4ab3f4e687aeff4873b4cef9a320af813adb489a" }, - "@std/assert@0.222.1": { - "integrity": "691637161ee584a9919d1f9950ddd1272feb8e0a19e83aa5b7563cedaf73d74c", - "dependencies": [ - "jsr:@std/fmt@^0.222.1" - ] - }, "@std/assert@1.0.0": { "integrity": "0e4f6d873f7f35e2a1e6194ceee39686c996b9e5d134948e644d35d4c4df2008", "dependencies": [ @@ -150,8 +116,8 @@ "@std/cli@1.0.0-rc.5": { "integrity": "6ca4f53c10d20992aa25211c97d576f29e539160bf68368ad8e75e1c7ef76b61" }, - "@std/collections@0.222.1": { - "integrity": "234099e08eead6a87e59f4f1abdcba35df5503cfb0e852e77a19f79359ed5760" + "@std/collections@1.0.4": { + "integrity": "bcc90800e489dc6bacdf68eb5dc746d6d8a033cb4f3311f0f9cf8094de429ce7" }, "@std/encoding@1.0.0-rc.2": { "integrity": "160d7674a20ebfbccdf610b3801fee91cf6e42d1c106dd46bbaf46e395cd35ef" @@ -162,9 +128,6 @@ "@std/fmt@0.221.0": { "integrity": "379fed69bdd9731110f26b9085aeb740606b20428ce6af31ef6bd45ef8efa62a" }, - "@std/fmt@0.222.1": { - "integrity": "ec3382f9b0261c1ab1a5c804aa355d816515fa984cdd827ed32edfb187c0a722" - }, "@std/fmt@0.225.6": { "integrity": "aba6aea27f66813cecfd9484e074a9e9845782ab0685c030e453a8a70b37afc8" }, @@ -178,13 +141,6 @@ "jsr:@std/path@^0.221.0" ] }, - "@std/fs@0.222.1": { - "integrity": "337613f33e6e5970dddb263c3a3e5b8e39c97810ad6fe326cb9f65146af2503b", - "dependencies": [ - "jsr:@std/assert@^0.222.1", - "jsr:@std/path@^0.222.1" - ] - }, "@std/fs@1.0.0-rc.5": { "integrity": "fa1ea439d0bd569bf2811efb5fba393be8f860ee8ab4cd5461bf6f500d0acb08", "dependencies": [ @@ -216,16 +172,6 @@ "@std/io@0.224.3": { "integrity": "b402edeb99c6b3778d9ae3e9927bc9085b170b41e5a09bbb7064ab2ee394ae2f" }, - "@std/json@0.222.1": { - "integrity": "ce4fb420dfd818fc2569289217842a3e70f249279f038a5e266f7b6572a2829a" - }, - "@std/jsonc@0.222.1": { - "integrity": "5d82d64eeb244e88fbb0927587dea7e1feccc6d5d9f49995b9ff5756d01be493", - "dependencies": [ - "jsr:@std/assert@^0.222.1", - "jsr:@std/json@^0.222.1" - ] - }, "@std/media-types@1.0.1": { "integrity": "f2ddc3497be0bd87ac0c9b9b26bb454f76bdc45e1b9a12146af47fab3ba2828c" }, @@ -238,12 +184,6 @@ "jsr:@std/assert@^0.221.0" ] }, - "@std/path@0.222.1": { - "integrity": "aad3e9463ca53b0adb25b4d5beb330025674aaa3278da24c1c261d9289a9e48b", - "dependencies": [ - "jsr:@std/assert@^0.222.1" - ] - }, "@std/path@1.0.0": { "integrity": "77fcb858b6e38777d1154df0f02245ba0b07e2c40ca3c0eec57c9233188c2d21" }, @@ -253,9 +193,6 @@ "@std/regexp@1.0.0": { "integrity": "158628d134c49a0858afe05017c4666f5f73d3a56602c346549ca42f3fab244a" }, - "@std/semver@0.222.1": { - "integrity": "43c5b526423f48f6f75375c77226646803776fdc678b331f243b0bd667d54c18" - }, "@std/semver@1.0.0-rc.2": { "integrity": "cc7b1ce3a11b24ec29d2d975e37d96107b8dd298c1c4c3d13bda5f198755c53c" }, @@ -273,10 +210,6 @@ } }, "npm": { - "@types/node@18.16.19": { - "integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==", - "dependencies": {} - }, "anymatch@3.1.3": { "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dependencies": { @@ -369,5 +302,11 @@ } } }, - "remote": {} + "remote": { + "https://deno.land/x/dir@1.5.1/data_local_dir/mod.ts": "91eb1c4bfadfbeda30171007bac6d85aadacd43224a5ed721bbe56bc64e9eb66", + "https://deno.land/x/import_map@v0.20.0/import_map.generated.js": "a8ebe1a146053f9a336fa9cb5aa63a2dac0cbca6adb183b04270abe56698f68c", + "https://deno.land/x/import_map@v0.20.0/mod.ts": "b074650942b9cca0bd1a5a2c354338f844f93b0ae01de10a14718415d7f370f7", + "https://deno.land/x/wasmbuild@0.15.1/cache.ts": "9d01b5cb24e7f2a942bbd8d14b093751fa690a6cde8e21709ddc97667e6669ed", + "https://deno.land/x/wasmbuild@0.15.1/loader.ts": "8c2fc10e21678e42f84c5135d8ab6ab7dc92424c3f05d2354896a29ccfd02a63" + } } diff --git a/deps.ts b/deps.ts index 49c6201..30bbfe3 100644 --- a/deps.ts +++ b/deps.ts @@ -1,8 +1,8 @@ export { abortable, deadline, debounce, delay, retry } from 'jsr:@std/async@^1.0.0-rc.4' export { walk } from 'jsr:@std/fs@^1.0.0-rc.5' export { serveDir, type ServeDirOptions, STATUS_CODE, STATUS_TEXT, type StatusCode } from 'jsr:@std/http@^1.0.0-rc.4' -export { joinGlobs, toFileUrl } from 'jsr:@std/path@^1.0.0-rc.4' +export { joinGlobs, toFileUrl } from 'jsr:@std/path@^1.0.0' export { normalize as posixNormalize } from 'jsr:@std/path@^1.0.0/posix/normalize' -export { escape } from 'jsr:@std/regexp@^1.0.0-rc.1' +export { escape } from 'jsr:@std/regexp@^1.0.0' export { watch } from 'npm:chokidar@^3.6.0' export type { ZodType } from 'npm:zod@^3.23.8' diff --git a/dev_deps.ts b/dev_deps.ts index 43059a5..2babb0e 100644 --- a/dev_deps.ts +++ b/dev_deps.ts @@ -1,3 +1,6 @@ +import { $ as _$ } from 'jsr:@david/dax@^0.41.0' + +export { parseFromJson } from 'https://deno.land/x/import_map@v0.20.0/mod.ts' export { Confirm, type ConfirmOptions, @@ -6,13 +9,22 @@ export { Select, type SelectOptions, } from 'jsr:@cliffy/prompt@^1.0.0-rc.5' -export { $ } from 'jsr:@david/dax@^0.41.0' -export { parse as parseDependency, resolveLatestVersion, stringify } from 'jsr:@molt/core@^0.18.5' -export { assertEquals } from 'jsr:@std/assert@^1.0.0' +export { ensure, is } from 'jsr:@core/unknownutil@^3.18.1' +export { createGraph, load as loadGraph } from 'jsr:@deno/graph@^0.80.1' +export type { DependencyJson, ResolvedDependency } from 'jsr:@deno/graph@^0.80.1/types' +export { Mutex } from 'jsr:@lambdalisue/async@^2.1.1' +export { assertEquals, assertExists } from 'jsr:@std/assert@^1.0.0' export { parseArgs, Spinner } from 'jsr:@std/cli@^1.0.0-rc.5' +export { filterEntries } from 'jsr:@std/collections@^1.0.4' export { bold, cyan, dim, green, magenta } from 'jsr:@std/fmt@^1.0.0-rc.1/colors' export { expandGlob } from 'jsr:@std/fs@^1.0.0-rc.5' export { getAvailablePort } from 'jsr:@std/net@^1.0.0-rc.1/get-available-port' -export { dirname, relative } from 'jsr:@std/path@^1.0.0' +export { dirname, fromFileUrl, relative, resolve, toFileUrl } from 'jsr:@std/path@^1.0.0' export { escape } from 'jsr:@std/regexp@^1.0.0' -export { canParse, format, increment, parse as parseSemVer, type ReleaseType } from 'jsr:@std/semver@^1.0.0-rc.2' +export * as SemVer from 'jsr:@std/semver@^1.0.0-rc.2' + +export const $ = new Proxy(_$, { + apply(target, _, args: Parameters) { + return target.raw(...args).quiet() + }, +}) diff --git a/scripts/release.ts b/scripts/release.ts index f1a5b31..19eda7c 100644 --- a/scripts/release.ts +++ b/scripts/release.ts @@ -26,33 +26,37 @@ import { $, bold, - canParse, Confirm as _Confirm, type ConfirmOptions, cyan, dim, escape, - format, green, - increment, Input as _Input, type InputOptions, magenta, - parseSemVer, - type ReleaseType, Select as _Select, type SelectOptions, + SemVer, Spinner, } from '../dev_deps.ts' -const SEMVER_INCREMENTS: ReleaseType[] = ['patch', 'minor', 'major', 'prepatch', 'preminor', 'premajor', 'prerelease'] - -function isReleaseType(value: string): value is ReleaseType { - return SEMVER_INCREMENTS.includes(value as ReleaseType) +const SEMVER_INCREMENTS: SemVer.ReleaseType[] = [ + 'patch', + 'minor', + 'major', + 'prepatch', + 'preminor', + 'premajor', + 'prerelease', +] + +function isReleaseType(value: string): value is SemVer.ReleaseType { + return SEMVER_INCREMENTS.includes(value as SemVer.ReleaseType) } const denoJson = JSON.parse(await Deno.readTextFile('deno.json')) -const oldVersion = parseSemVer(denoJson.version) +const oldVersion = SemVer.parse(denoJson.version) // #endregion @@ -87,7 +91,7 @@ class Select extends _Select { name = name + '' if (isReleaseType(name)) { - const newVersion = increment(oldVersion, name) + const newVersion = SemVer.increment(oldVersion, name) const newMajor = newVersion.major + '' const newMinor = newVersion.minor + '' @@ -242,7 +246,7 @@ async function open(url: string): Promise { console.error('Unsupported OS. Please open the following URL manually:\n' + url) return } - await $.raw`${programAliases[Deno.build.os]} ${$.escapeArg(url)}` + await $`${programAliases[Deno.build.os]} ${$.escapeArg(url)}` } // #endregion @@ -261,40 +265,38 @@ if (version === 'other') { message: 'Enter new version', validate: (value) => { if (!value) return 'Version is required' - if (!canParse(value)) return 'Invalid semver version' + if (!SemVer.canParse(value)) return 'Invalid semver version' return true }, }) } -const newVersion = format(isReleaseType(version) ? increment(oldVersion, version) : parseSemVer(version)) +const newVersion = SemVer.format(isReleaseType(version) ? SemVer.increment(oldVersion, version) : SemVer.parse(version)) if (!(await Confirm.prompt({ message: `Bump ${dim(`(${denoJson.version} → ${newVersion})`)}?` }))) Deno.exit() await step('Updating version in deno.json...', async () => { denoJson.version = newVersion await Deno.writeTextFile('deno.json', JSON.stringify(denoJson, null, 2)) - await $.raw`deno fmt deno.json`.quiet() + await $`deno fmt deno.json` }) await step('Generating changelog...', async () => { - await $ - .raw`deno run -A --no-lock npm:conventional-changelog-cli -i CHANGELOG.md -s -p conventionalcommits -k deno.json` - .quiet() - await $.raw`deno fmt CHANGELOG.md`.quiet() + await $`deno run -A --no-lock npm:conventional-changelog-cli -i CHANGELOG.md -s -p conventionalcommits -k deno.json` + await $`deno fmt CHANGELOG.md` }) if (!(await Confirm.prompt({ message: 'Changelog generated. Does it look good?' }))) Deno.exit() await step('Committing changes...', async () => { - await $.raw`git add deno.json CHANGELOG.md`.quiet() - await $.raw`git commit -m "release: v${newVersion}"`.quiet() - await $.raw`git tag v${newVersion}`.quiet() + await $`git add deno.json CHANGELOG.md` + await $`git commit -m "release: v${newVersion}"` + await $`git tag v${newVersion}` }) await step('Pushing to GitHub...', async () => { - await $.raw`git push origin refs/tags/v${newVersion}`.quiet() - await $.raw`git push`.quiet() + await $`git push origin refs/tags/v${newVersion}` + await $`git push` }) await step('Creating a new release...', async () => { @@ -304,7 +306,7 @@ await step('Creating a new release...', async () => { const url = newGithubReleaseUrl({ body: `${match?.[2]?.trim() ?? ''}\n\n**Full Changelog**: ${match?.[1]?.trim() ?? ''}`, - isPrerelease: (parseSemVer(newVersion).prerelease?.length ?? 0) > 0, + isPrerelease: (SemVer.parse(newVersion).prerelease?.length ?? 0) > 0, repoUrl: 'https://github.com/globalbrain/hado', tag: `v${newVersion}`, }) @@ -317,5 +319,4 @@ await step('Creating a new release...', async () => { /** * TODO: * - publish stuff from this script as standalone modules - * - remove version from @cliffy/prompt when it's stable */ diff --git a/scripts/update.ts b/scripts/update.ts index 5ba396f..5e91bea 100644 --- a/scripts/update.ts +++ b/scripts/update.ts @@ -1,36 +1,113 @@ -import { $, expandGlob, parseArgs, parseDependency, relative, resolveLatestVersion, stringify } from '../dev_deps.ts' +/** + * Credits: + * + * - @molt/core - MIT License + * Copyright (c) 2023 Chiezo + * https://github.com/hasundue/molt/blob/main/LICENSE + */ -const args = parseArgs(Deno.args, { collect: ['x'] }) -const excludes = (args.x ?? []) as string[] +import { + $, + assertExists, + createGraph, + type DependencyJson, + ensure, + filterEntries, + fromFileUrl, + is, + loadGraph, + Mutex, + parseFromJson, + resolve, + type ResolvedDependency, + SemVer, + toFileUrl, +} from '../dev_deps.ts' -const denoJson = JSON.parse(await Deno.readTextFile('deno.json')) as { imports: Record | undefined } -const newImports = { ...denoJson.imports } +// #region Vendored constants -for (const [key, value] of Object.entries(denoJson.imports ?? {})) { - if (!value.includes(':') || excludes.includes(key)) continue +const supportedProtocols = ['npm:', 'jsr:', 'http:', 'https:'] as const - const parsed = parseDependency(value) - let rangeSpecifier: string | undefined = undefined +class LatestVersionCache implements Disposable { + static #mutex = new Map() + static #cache = new Map() - if (parsed.version && /^[~^]/.test(parsed.version)) { - rangeSpecifier = parsed.version[0] - parsed.version = parsed.version.slice(1) + constructor(readonly name: string) { + const mutex = LatestVersionCache.#mutex.get(name) ?? LatestVersionCache.#mutex.set(name, new Mutex()).get(name)! + mutex.acquire() } - const resolved = await resolveLatestVersion(parsed) - if (!resolved) { - console.log(`Could not resolve latest version for ${key} (${value})`) - continue + get(name: string): UpdatedDependency | null | undefined { + return LatestVersionCache.#cache.get(name) } - resolved.version = `${rangeSpecifier || ''}${resolved.version}` - newImports[key] = stringify(resolved) + set(name: string, dependency: T): void { + LatestVersionCache.#cache.set(name, dependency) + } + + [Symbol.dispose]() { + const mutex = LatestVersionCache.#mutex.get(this.name) + assertExists(mutex) + mutex.release() + } +} + +const isNpmPackageMeta = is.ObjectOf({ 'dist-tags': is.ObjectOf({ latest: is.String }) }) + +const isJsrPackageMeta = is.ObjectOf({ + versions: is.RecordOf(is.ObjectOf({ yanked: is.OptionalOf(is.Boolean) }), is.String), +}) + +// #endregion + +// update dependencies in files + +const files = (await $`git ls-files -- '*.ts'`.text()) + .split('\n') + .filter(Boolean) + .map((file) => toFileUrl(resolve(file)).href) + +const importMap = await Deno.readTextFile('deno.json') +const resolvedImportMap = await parseFromJson(toFileUrl(resolve('deno.json')), importMap, { expandImports: true }) + +const graph = await createGraph(files, { + resolve: resolvedImportMap.resolve.bind(resolvedImportMap), + // deno-lint-ignore require-await + load: async (specifier) => { + return files.includes(specifier) ? loadGraph(specifier) : undefined + }, +}) + +const modules = graph.modules + .filter(({ specifier }) => files.includes(specifier)) + .map((mod) => ({ + specifier: mod.specifier, + dependencies: (mod.dependencies ?? []).filter((dep) => + // deno-lint-ignore no-explicit-any + supportedProtocols.includes(URL.parse(dep.specifier)?.protocol as any) + ), + })) + .filter((mod) => mod.dependencies.length) + +for (const mod of modules) { + updateDepsInFile(fromFileUrl(mod.specifier), mod.dependencies) +} + +// update deps in deno.json + +const denoJson = JSON.parse(importMap) as { imports: Record | undefined } +const newImports = { ...denoJson.imports } + +for (const [key, value] of Object.entries(denoJson.imports ?? {})) { + newImports[key] = await updateSpecifier(value) } denoJson.imports = newImports await Deno.writeTextFile('deno.json', JSON.stringify(denoJson, null, 2)) -await $.raw`deno fmt deno.json`.quiet() +await $`deno fmt deno.json` + +// regenerate lock file try { await Deno.remove('deno.lock') @@ -38,17 +115,296 @@ try { if (e instanceof Deno.errors.NotFound) { // ignore } else { - throw e + console.error(e) + } +} + +await $`deno cache --reload --lock=deno.lock ${files.join(' ')}` + +// #region Update logic + +async function updateDepsInFile(path: string, deps: DependencyJson[]) { + const file = await Deno.readFile(path) + const text = new TextDecoder().decode(file) + const lines = text.split('\n') + + async function updateLine(dep?: ResolvedDependency) { + if (!dep?.specifier) return + + const start = dep.span.start.line + const startChar = dep.span.start.character + 1 + const end = dep.span.end.line + const endChar = dep.span.end.character - 1 + + if (start !== end) { + console.log(`Span is multiline, skipping update for ${dep.specifier} in ${path}`) + } else { + const newSpecifier = await updateSpecifier(dep.specifier) + const newLine = lines[start]!.slice(0, startChar) + newSpecifier + lines[start]!.slice(endChar) + lines[start] = newLine + } + } + + for (const dep of deps) { + await updateLine(dep.code) + await updateLine(dep.type) } + + await Deno.writeTextFile(path, lines.join('\n')) } -const files = - (await Array.fromAsync(expandGlob('**/*.ts', { root: Deno.cwd(), includeDirs: false, exclude: ['**/_*', '**/.*'] }))) - .map((x) => relative(Deno.cwd(), x.path)).join(' ') +async function updateSpecifier(specifier: string) { + const parsed = parseDependency(specifier) + let rangeSpecifier: string | undefined = undefined + + if (parsed.version && /^[~^]/.test(parsed.version)) { + rangeSpecifier = parsed.version[0] + parsed.version = parsed.version.slice(1) + } + + const resolved = await resolveLatestVersion(parsed, { cache: true, allowPreRelease: isPreRelease(parsed.version!) }) + if (!resolved) { + console.log(`Could not resolve latest version for ${specifier}`) + return specifier + } + + resolved.version = `${rangeSpecifier || ''}${resolved.version}` + return stringifyDependency(resolved) +} + +// #endregion + +// #region Vendored functions + +/** + * Properties of a dependency parsed from an import specifier. + */ +interface Dependency { + /** + * The URL protocol of the dependency. + * @example + * ```ts + * const { protocol } = Dependency.parse( + * new URL("https://deno.land/std/fs/mod.ts") + * ); + * // -> "https:" + */ + protocol: string + /** + * The name of the dependency. + * @example + * ```ts + * const { name } = Dependency.parse( + * new URL("https://deno.land/std@0.205.0/fs/mod.ts") + * ); + * // -> "deno.land/std" + * ``` + */ + name: string + /** + * The version string of the dependency. + * @example + * ```ts + * const { version } = Dependency.parse( + * new URL("https://deno.land/std@0.205.0/fs/mod.ts") + * ); + * // -> "0.205.0" + * ``` + */ + version?: string + /** + * The subpath of the dependency. + * @example + * ```ts + * const { path } = Dependency.parse( + * new URL("https://deno.land/std@0.205.0/fs/mod.ts") + * ); + * // -> "/fs/mod.ts" + * + * const { path } = Dependency.parse( + * new URL("npm:node-emoji@2.0.0") + * ); + * // -> "" + * ``` + */ + path: string +} + +/** + * Properties of a dependency parsed from an updated import specifier. + * The `version` property is guaranteed to be present. + */ +interface UpdatedDependency extends Dependency { + version: string +} + +/** + * Parse properties of a dependency from the given URL. + * @example + * ```ts + * const { name, version, path } = Dependency.parse( + * new URL("https://deno.land/std@0.200.0/fs/mod.ts") + * ); + * // -> { name: "deno.land/std", version: "0.200.0", path: "/fs/mod.ts" } + * ``` + */ +function parseDependency(url: string | URL): Dependency { + url = new URL(url) + const protocol = url.protocol + const body = url.hostname + url.pathname + + const matched = body.match(/^(?.+)@(?[^/]+)(?\/.*)?$/) + + if (matched) { + assertExists(matched.groups) + const { name, version } = matched.groups + const path = matched.groups.path ?? '' + return { protocol, name: name!, version, path } + } + + return { protocol, name: body, path: '' } +} + +/** + * Convert the given protocol to a URL scheme. + */ +function addSeparator(protocol: string): string { + switch (protocol) { + case 'file:': + case 'http:': + case 'https:': + return protocol + '//' + default: + return protocol + } +} + +/** + * Convert the given dependency to a URL string. + * @example + * ```ts + * const uri = toURL({ + * protocol: "https:", + * name: "deno.land/std", + * version: "1.0.0", + * path: "/fs/mod.ts", + * }); + * // -> "https://deno.land/std@1.0.0/fs/mod.ts" + * ``` + */ +function stringifyDependency( + dependency: Dependency, + include: { protocol?: boolean; version?: boolean; path?: boolean } = {}, +): string { + include = { protocol: true, version: true, path: true, ...include } + + const header = include.protocol ? addSeparator(dependency.protocol) : '' + const version = include.version ? (dependency.version ? '@' + dependency.version : '') : '' + const path = include.path ? dependency.path : '' + + return `${header}${dependency.name}${version}` + path +} + +/** + * Resolve the latest version of the given dependency. + * + * @returns The latest version of the given dependency, or `undefined` if the + * latest version of dependency is unable to resolve. + * + * @throws An error if the dependency is not found in the registry. + * + * @example + * ```ts + * await resolveLatestVersion( + * Dependency.parse(new URL("https://deno.land/std@0.200.0/fs/mod.ts")) + * ); + * // -> { name: "deno.land/std", version: "0.207.0", path: "/fs/mod.ts" } + * ``` + */ +async function resolveLatestVersion( + dependency: Dependency, + options?: { cache?: boolean; allowPreRelease?: boolean }, +): Promise { + const constraint = dependency.version ? SemVer.tryParseRange(dependency.version) : undefined + if (constraint && constraint.flat().length > 1) return + using cache = options?.cache ? new LatestVersionCache(dependency.name) : undefined + const cached = cache?.get(dependency.name) + if (cached) { + dependency.version === undefined + ? { name: cached.name, path: dependency.name.slice(cached.name.length) } + : { ...cached, path: dependency.path } + } + if (cached === null) return + const result = await _resolveLatestVersion(dependency, options) + cache?.set(dependency.name, result ?? null) + return result +} + +async function _resolveLatestVersion( + dependency: Dependency, + options?: { allowPreRelease?: boolean }, +): Promise { + function _isPreRelease(version: string): boolean { + if (options?.allowPreRelease) return false + return isPreRelease(version) + } + switch (dependency.protocol) { + case 'npm:': { + const response = await fetch(`https://registry.npmjs.org/${dependency.name}`) + if (!response.ok) break + const pkg = ensure(await response.json(), isNpmPackageMeta, { + message: `Invalid response from NPM registry: ${response.url}`, + }) + const latest = pkg['dist-tags'].latest + if (_isPreRelease(latest)) break + return { ...dependency, version: latest } + } + case 'jsr:': { + const response = await fetch(`https://jsr.io/${dependency.name}/meta.json`) + if (!response.ok) break + const meta = ensure(await response.json(), isJsrPackageMeta, { + message: `Invalid response from JSR registry: ${response.url}`, + }) + const candidates = filterEntries(meta.versions, ([version, { yanked }]) => !yanked && !_isPreRelease(version)) + const semvers = Object.keys(candidates).map(SemVer.parse) + if (!semvers.length) break + const latest = SemVer.format(semvers.sort(SemVer.compare).reverse()[0]!) + if (_isPreRelease(latest)) break + return { ...dependency, version: latest } + } + case 'http:': + case 'https:': { + const response = await fetch(addSeparator(dependency.protocol) + dependency.name + dependency.path, { + method: 'HEAD', + }) + await response.arrayBuffer() + if (!response.redirected) break + const redirected = parseDependency(response.url) + if (!redirected.version || _isPreRelease(redirected.version)) break + const latest = redirected as UpdatedDependency + return { ...latest, path: dependency.path === '/' ? '/' : latest.path } + } + } + return +} + +/** + * Check if the given version string represents a pre-release. + * + * @example + * ```ts + * isPreRelease("0.1.0"); // -> false + * isPreRelease("0.1.0-alpha.1"); // -> true + * ``` + */ +function isPreRelease(version: string): boolean { + const parsed = SemVer.tryParse(version) + return parsed !== undefined && parsed.prerelease !== undefined && parsed.prerelease.length > 0 +} -await $.raw`deno cache --reload --lock=deno.lock ${files}` +// #endregion /** * TODO: - * - simplify when https://github.com/dsherret/dax/issues/251 is implemented + * - removed vendored code when https://github.com/hasundue/molt/issues/194 and https://github.com/hasundue/molt/issues/195 are resolved */