diff --git a/cli.ts b/cli.ts index 9f5a3f4f..e0fc9640 100644 --- a/cli.ts +++ b/cli.ts @@ -5,14 +5,16 @@ import { colors, Command } from "./lib/x/cliffy.ts"; import { $ } from "./lib/x/dax.ts"; import { ensure, is } from "./lib/x/unknownutil.ts"; import { findFileUp } from "./lib/path.ts"; -import { parse, resolveLatestVersion } from "./lib/dependency.ts"; import { collect, - type CommitSequence, + CollectResult, + CommitSequence, createCommitSequence, - type DependencyUpdate, - exec, - writeAll, + DependencyUpdate, + execute, + parse, + resolveLatestVersion, + write, } from "./mod.ts"; const { gray, yellow, bold, cyan } = colors; @@ -62,14 +64,14 @@ const main = new Command() } } ensureFiles(files); - const updates = await collectUpdates(files, options); - printUpdates(files, updates); + const result = await collectUpdates(files, options); + printResult(files, result); if (options.write) { - return writeUpdates(updates, options); + return writeResult(result, options); } if (options.commit) { const tasks = await getTasks(); - return commitUpdates(updates, { + return commitResult(result, { ...options, preCommit: filterKeys( tasks, @@ -100,27 +102,23 @@ async function collectUpdates( importMap?: string; only?: string[]; }, -): Promise { - return await $.progress("Checking for updates").with(async () => { - const updates = await Promise.all( - entrypoints.map(async (entrypoint) => - await collect(entrypoint, { - ignore: options.ignore - ? (dep) => options.ignore!.some((it) => dep.name.includes(it)) - : undefined, - importMap: options.importMap, - only: options.only - ? (dep) => options.only!.some((it) => dep.name.includes(it)) - : undefined, - }) - ), - ).then((results) => results.flat()); - if (!updates.length) { - console.log("🍵 No updates found"); - Deno.exit(0); - } - return updates; - }); +): Promise { + const result = await $.progress("Checking for updates").with(() => + collect(entrypoints, { + importMap: options.importMap, + ignore: options.ignore + ? (dep) => options.ignore!.some((it) => dep.name.includes(it)) + : undefined, + only: options.only + ? (dep) => options.only!.some((it) => dep.name.includes(it)) + : undefined, + }) + ); + if (!result.updates.length) { + console.log("🍵 No updates found"); + Deno.exit(0); + } + return result; } type TaskRecord = Record; @@ -151,20 +149,20 @@ async function getTasks() { const toRelativePath = (path: string) => relative(Deno.cwd(), path); -function printUpdates( +function printResult( files: string[], - updates: DependencyUpdate[], + result: CollectResult, ) { const dependencies = new Map(); - for (const u of updates) { + for (const u of result.updates) { const list = dependencies.get(u.to.name) ?? []; list.push(u); dependencies.set(u.to.name, list); } let count = 0; - const nWrites = distinct(updates.map((u) => u.referrer)).length; + const nWrites = distinct(result.updates.map((u) => u.referrer)).length; for (const [name, list] of dependencies.entries()) { - const froms = distinct(list.map((u) => u.from.version)).join(", "); + const froms = distinct(list.map((u) => u.from?.version)).join(", "); console.log( `📦 ${bold(name)} ${yellow(froms)} => ${yellow(list[0].to.version)}`, ); @@ -172,7 +170,7 @@ function printUpdates( distinct( list.map((u) => { const source = toRelativePath(u.map?.source ?? u.referrer); - return ` ${source} ` + gray(u.from.version ?? ""); + return ` ${source} ` + gray(u.from?.version ?? ""); }), ).forEach((line) => console.log(line)); if (++count < dependencies.size) { @@ -182,15 +180,15 @@ function printUpdates( } } -async function writeUpdates( - updates: DependencyUpdate[], +async function writeResult( + result: CollectResult, options?: { summary?: string; report?: string; }, ) { console.log(); - await writeAll(updates, { + await write(result, { onWrite: (file) => console.log(`💾 ${toRelativePath(file.path)}`), }); if (options?.summary || options?.report) { @@ -202,15 +200,17 @@ async function writeUpdates( } if (options?.report) { const content = distinct( - updates.map((u) => `- ${u.to.name} ${u.from.version} => ${u.to.version}`), + result.updates.map((u) => + `- ${u.to.name} ${u.from?.version} => ${u.to.version}` + ), ).join("\n"); await Deno.writeTextFile(options.report, content); console.log(`📄 ${options.report}`); } } -async function commitUpdates( - updates: DependencyUpdate[], +async function commitResult( + result: CollectResult, options: { preCommit?: TaskRecord; postCommit?: TaskRecord; @@ -226,8 +226,7 @@ async function commitUpdates( const hasTask = preCommitTasks.length > 0 || postCommitTasks.length > 0; let count = 0; - - const commits = createCommitSequence(updates, { + const commits = createCommitSequence(result, { groupBy: (dependency) => dependency.to.name, composeCommitMessage: ({ group, version }) => formatPrefix(options.prefix) + `bump ${group}` + @@ -251,8 +250,7 @@ async function commitUpdates( } }, }); - - await exec(commits); + await execute(commits); if (options?.summary || options?.report) { console.log(); diff --git a/deno.json b/deno.json index 5e5059eb..79751d8e 100644 --- a/deno.json +++ b/deno.json @@ -3,9 +3,9 @@ "cache": "deno cache ./lib/*/*.ts --lock", "lock": "deno task -q cache --lock-write && git add deno.lock", "check": "deno check ./*.ts ./lib/*.ts ./test/integration/*.ts", - "test": "NO_COLOR=1 deno test -A --no-check ./lib", + "test": "NO_COLOR=1 deno test -A --no-check", "pre-commit": "deno fmt && deno lint && deno task -q check && deno task lock && deno task -q test", - "integration": "NO_COLOR=1 deno test --no-lock -A ./test/integration/*.ts", + "integration": "deno task -q test --no-lock ./test/integration/*.ts", "run": "deno run --allow-env --allow-read --allow-net --allow-write=. --allow-run=git,deno cli.ts", "update": "deno run --allow-env --allow-read --allow-write --allow-net=deno.land,registry.npmjs.org --allow-run=git,deno ./cli.ts ./lib/*/*.ts", "update:commit": "deno task -q update --commit --pre-commit=fmt,lock --prefix 'build(deps):'" diff --git a/deno.lock b/deno.lock index 63a4fdea..e421cbf0 100644 --- a/deno.lock +++ b/deno.lock @@ -133,11 +133,17 @@ "https://deno.land/std@0.218.2/assert/assert_throws.ts": "31f3c061338aec2c2c33731973d58ccd4f14e42f355501541409ee958d2eb8e5", "https://deno.land/std@0.218.2/assert/assertion_error.ts": "9f689a101ee586c4ce92f52fa7ddd362e86434ffdf1f848e45987dc7689976b8", "https://deno.land/std@0.218.2/assert/equal.ts": "fae5e8a52a11d3ac694bbe1a53e13a7969e3f60791262312e91a3e741ae519e2", + "https://deno.land/std@0.218.2/collections/_utils.ts": "fc59674548a4dd9f5c3cc4f99f9c264aa521a52c1a4ff56b0263713e57e79206", + "https://deno.land/std@0.218.2/collections/deep_merge.ts": "04f8d2a6cfa15c7580e788689bcb5e162512b9ccb18bab1241824b432a78551e", "https://deno.land/std@0.218.2/collections/distinct.ts": "42d81633e4ccd5ea89118cb08f875bbfc683ac6620df2fa60f3d07270b177f4d", + "https://deno.land/std@0.218.2/collections/distinct_by.ts": "e895705decb0ce88b31c6679fd4e2bd08ac6d47cde7d00bf2016e3c2bac565a7", "https://deno.land/std@0.218.2/collections/filter_entries.ts": "12202e99001695fcf541ec0191e9762fe7290ba4597df762d3a0c6e611a1aafe", "https://deno.land/std@0.218.2/collections/filter_keys.ts": "bf8eaae2769e6ddd34f23ba1912f7feda2b6f374253c07a6630e9a2e384ba611", "https://deno.land/std@0.218.2/collections/map_entries.ts": "45b8bf3f197ee24abe2e078213a4cf2f4e187ad130649fff34e13b1a03ede478", + "https://deno.land/std@0.218.2/collections/map_not_nullish.ts": "f519f74db550afcf6c6e2486382cb4dc6b0465e4234b6acca26910d118e7dd55", + "https://deno.land/std@0.218.2/collections/map_values.ts": "91d6ece4b4dc4b94abc378f51e0e309e7f7f18b2ce4c335dab673a31ebd17c75", "https://deno.land/std@0.218.2/collections/max_by.ts": "a601c296d54349097d5b424bc86c84010ab22733b622b480e2ad65927063d7bb", + "https://deno.land/std@0.218.2/collections/omit.ts": "91563393913b36b2af1156cb94d36610e513a60a87eccbdbf2264cc2218a3f32", "https://deno.land/std@0.218.2/collections/partition.ts": "22689dc4834bbcaa06f9ac204ac5502ab0f7087d7fca26bad50319c12434d099", "https://deno.land/std@0.218.2/fmt/colors.ts": "d239d84620b921ea520125d778947881f62c50e78deef2657073840b8af9559a", "https://deno.land/std@0.218.2/fs/_get_file_info_type.ts": "da7bec18a7661dba360a1db475b826b18977582ce6fc9b25f3d4ee0403fe8cbd", @@ -295,18 +301,18 @@ "https://deno.land/x/dir@1.5.1/data_local_dir/mod.ts": "91eb1c4bfadfbeda30171007bac6d85aadacd43224a5ed721bbe56bc64e9eb66", "https://deno.land/x/import_map@v0.19.0/import_map.generated.js": "e47e88116350f98d35a773330d47539286b6c9e2ea4d2644f82f07e2882e6ae2", "https://deno.land/x/import_map@v0.19.0/mod.ts": "dd4c4e20639dcfd720a6d41f47bea250db86137901a08988102094678c6b7859", - "https://deno.land/x/unknownutil@v3.16.3/_typeutil.ts": "b0d6b3501f9ac491dcd18402a5d2a26d5036ab3ca1954b4f0ec6af60a0c45a86", - "https://deno.land/x/unknownutil@v3.16.3/inspect.ts": "33e61bdfed94cd586d66600813b528fa93046a2802d8144277b92f0fa5e5f10e", - "https://deno.land/x/unknownutil@v3.16.3/is.ts": "7080444f66ca46e44bb4ec094caad2ef52654397d72861618cbf4c84a8986df5", - "https://deno.land/x/unknownutil@v3.16.3/is/_deprecated.ts": "d1422e65e8a87d1ab156b236d2bdbbeb17a19c88943e6d9c7453662e773c7e7a", - "https://deno.land/x/unknownutil@v3.16.3/is/annotation.ts": "2df8bb0fb948f8db120ec3a812abeac689fe77f7d977786331e03fe18bc0cebc", - "https://deno.land/x/unknownutil@v3.16.3/is/core.ts": "850bd5859f0f3c9a66c0118a2666caa901e28c6030e9de6450dcd0a7e617a5dd", - "https://deno.land/x/unknownutil@v3.16.3/is/factory.ts": "9b5c56c0dbfc7b977f05d2510ed3daf53525245430fd9f9469bd17ca03e9ca5f", - "https://deno.land/x/unknownutil@v3.16.3/is/type.ts": "54e0232318e075d683a1afd18bb126c2b8f52fd58257da78b722d790991af017", - "https://deno.land/x/unknownutil@v3.16.3/is/utility.ts": "e1ba67073d3e737fa11a8b08e9c1fec383ea70a654dddf19834626a56d50420f", - "https://deno.land/x/unknownutil@v3.16.3/metadata.ts": "db770390cfc1a7743230da20e87ea1e7cbba313de2612b63735cc290ad179512", - "https://deno.land/x/unknownutil@v3.16.3/mod.ts": "175beb74e096cd52db58f895b208e4a0fb0bad4819f27fc4e13dd4181d03188b", - "https://deno.land/x/unknownutil@v3.16.3/util.ts": "4eb75d6f31d4973d79f588be4fce1944c4dc43a73df4c48342d85c86810ba83f", + "https://deno.land/x/unknownutil@v3.17.0/_typeutil.ts": "b0d6b3501f9ac491dcd18402a5d2a26d5036ab3ca1954b4f0ec6af60a0c45a86", + "https://deno.land/x/unknownutil@v3.17.0/inspect.ts": "33e61bdfed94cd586d66600813b528fa93046a2802d8144277b92f0fa5e5f10e", + "https://deno.land/x/unknownutil@v3.17.0/is.ts": "7080444f66ca46e44bb4ec094caad2ef52654397d72861618cbf4c84a8986df5", + "https://deno.land/x/unknownutil@v3.17.0/is/_deprecated.ts": "d1422e65e8a87d1ab156b236d2bdbbeb17a19c88943e6d9c7453662e773c7e7a", + "https://deno.land/x/unknownutil@v3.17.0/is/annotation.ts": "2df8bb0fb948f8db120ec3a812abeac689fe77f7d977786331e03fe18bc0cebc", + "https://deno.land/x/unknownutil@v3.17.0/is/core.ts": "4f378b19138862373096cd11e82980af4b22bfcc7f9079be60a204820199da07", + "https://deno.land/x/unknownutil@v3.17.0/is/factory.ts": "7dfcc86e113d41963d62cea926599f6980ede701a7dc97b4b63346f279757b38", + "https://deno.land/x/unknownutil@v3.17.0/is/type.ts": "54e0232318e075d683a1afd18bb126c2b8f52fd58257da78b722d790991af017", + "https://deno.land/x/unknownutil@v3.17.0/is/utility.ts": "e1ba67073d3e737fa11a8b08e9c1fec383ea70a654dddf19834626a56d50420f", + "https://deno.land/x/unknownutil@v3.17.0/metadata.ts": "db770390cfc1a7743230da20e87ea1e7cbba313de2612b63735cc290ad179512", + "https://deno.land/x/unknownutil@v3.17.0/mod.ts": "175beb74e096cd52db58f895b208e4a0fb0bad4819f27fc4e13dd4181d03188b", + "https://deno.land/x/unknownutil@v3.17.0/util.ts": "4eb75d6f31d4973d79f588be4fce1944c4dc43a73df4c48342d85c86810ba83f", "https://deno.land/x/wasmbuild@0.15.1/cache.ts": "9d01b5cb24e7f2a942bbd8d14b093751fa690a6cde8e21709ddc97667e6669ed", "https://deno.land/x/wasmbuild@0.15.1/loader.ts": "8c2fc10e21678e42f84c5135d8ab6ab7dc92424c3f05d2354896a29ccfd02a63", "https://deno.land/x/which@0.3.0/mod.ts": "3e10d07953c14e4ddc809742a3447cef14202cdfe9be6678a1dfc8769c4487e6" diff --git a/lib/dependency.ts b/lib/dependency.ts index ba73be1e..ded6c8bf 100644 --- a/lib/dependency.ts +++ b/lib/dependency.ts @@ -123,11 +123,20 @@ function addSeparator(protocol: string): string { * // -> "https://deno.land/std@1.0.0/fs/mod.ts" * ``` */ -export function toUrl(dependency: Dependency): string { +export function stringify(dependency: Dependency, includePath = true): string { const header = addSeparator(dependency.protocol); const version = dependency.version ? "@" + dependency.version : ""; const path = dependency.path; - return `${header}${dependency.name}${version}${path}`; + return `${header}${dependency.name}${version}` + (includePath ? path : ""); +} + +export function hasVersionRange( + dependency: Dependency, +): boolean { + const constraint = dependency.version + ? SemVer.tryParseRange(dependency.version) + : undefined; + return !!constraint && constraint.flat().length > 1; } /** @@ -140,7 +149,7 @@ export function toUrl(dependency: Dependency): string { * * @example * ```ts - * await Dependency.resolveLatestVersion( + * 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" } @@ -218,7 +227,7 @@ async function _resolveLatestVersion( { message: `Invalid response from NPM registry: ${response.url}` }, ); const latest = pkg["dist-tags"].latest; - if (latest === dependency.version || isPreRelease(latest)) { + if (isPreRelease(latest)) { break; } return { ...dependency, version: latest }; @@ -242,7 +251,7 @@ async function _resolveLatestVersion( ); const semvers = Object.keys(candidates).map(SemVer.parse); const latest = SemVer.format(semvers.sort(SemVer.compare).reverse()[0]); - if (latest === dependency.version || isPreRelease(latest)) { + if (isPreRelease(latest)) { break; } return { ...dependency, version: latest }; diff --git a/lib/dependency_test.ts b/lib/dependency_test.ts index 49dc5b58..a05fce34 100644 --- a/lib/dependency_test.ts +++ b/lib/dependency_test.ts @@ -1,7 +1,7 @@ import { afterAll, beforeAll, describe, it } from "./std/testing.ts"; import { assertEquals, assertExists, assertObjectMatch } from "./std/assert.ts"; import { isPreRelease, parse, resolveLatestVersion } from "./dependency.ts"; -import { LatestSemVerStub } from "./testing.ts"; +import { LatestVersionStub } from "./testing.ts"; describe("parse", () => { it("deno.land/std", () => @@ -90,10 +90,10 @@ Deno.test("isPreRelease", () => { describe("resolveLatestVersion", () => { const LATEST = "123.456.789"; - let stub: LatestSemVerStub; + let stub: LatestVersionStub; beforeAll(() => { - stub = LatestSemVerStub.create(LATEST); + stub = LatestVersionStub.create(LATEST); }); afterAll(() => { @@ -200,10 +200,10 @@ describe("resolveLatestVersion", () => { }); describe("resolveLatestVersion - pre-release", () => { - let stub: LatestSemVerStub; + let stub: LatestVersionStub; beforeAll(() => { - stub = LatestSemVerStub.create("123.456.789-alpha.1"); + stub = LatestVersionStub.create("123.456.789-alpha.1"); }); afterAll(() => { diff --git a/lib/file.ts b/lib/file.ts index 2a97ccb8..d2190cec 100644 --- a/lib/file.ts +++ b/lib/file.ts @@ -1,45 +1,52 @@ +import { assertEquals } from "./std/assert.ts"; +import { deepMerge, omit, partition } from "./std/collections.ts"; import { detectEOL, EOL } from "./std/fs.ts"; -import { toUrl } from "./dependency.ts"; -import { type DependencyUpdate } from "./update.ts"; +import { stringify } from "./dependency.ts"; +import { createLockPart, LockPart, parseLockFileJson } from "./lockfile.ts"; +import { + CollectResult, + DependencyUpdate, + SourceType, + sourceTypeOf, +} from "./update.ts"; /** - * Write the given array of DependencyUpdate to files. - * + * A collection of updates to dependencies associated with a file. + */ +export interface FileUpdate { + /** The full path to the file being updated. + * @example "/path/to/mod.ts" */ + path: string; + /** The type of the file being updated. */ + kind: T; + /** The updates to dependencies associated with the file. */ + dependencies: DependencyUpdate[]; +} + +export interface WriteOptions { + onWrite?: (file: FileUpdate) => void | Promise; +} + +/** + * Write the given `CollectResult` to file system. * @returns A promise that resolves when all updates are written. - * * @example * ```ts - * await writeAll(updates, { + * await write(updates, { * onWrite: (file) => { * console.log(`Updated ${file.specifier}`); * }, * }); * ``` */ -export function writeAll( - updates: DependencyUpdate[], - options?: { - onWrite?: (file: FileUpdate) => void | Promise; - }, +export function write( + result: CollectResult, + options: WriteOptions = {}, ) { - return write(associateByFile(updates), options); -} - -type FileKind = "module" | "import_map"; - -/** - * A collection of updates to dependencies associated with a file. - */ -export interface FileUpdate< - Kind extends FileKind = FileKind, -> { - /** The full path to the file being updated. - * @example "/path/to/mod.ts" */ - path: string; - /** The type of the file being updated. */ - kind: Kind; - /** The updates to dependencies associated with the file. */ - dependencies: DependencyUpdate[]; + return writeFileUpdate(associateByFile(result.updates), { + ...options, + locks: result.locks, + }); } /** @@ -59,36 +66,41 @@ export function associateByFile( } return Array.from(fileToDepsMap.entries()).map(( [referrer, dependencies], - ) => ({ - path: referrer, - kind: dependencies[0].map ? "import_map" : "module", - dependencies, - })).sort((a, b) => a.path.localeCompare(b.path)) as FileUpdate[]; + ) => { + const kind = sourceTypeOf(dependencies[0]); + return { + path: referrer, + kind, + dependencies, + }; + }).sort((a, b) => a.path.localeCompare(b.path)) as FileUpdate[]; +} + +interface WriteFileUpdateOptions extends WriteOptions { + locks?: LockPart[]; } /** * Write the given (array of) FileUpdate to file system. */ -export async function write( +export async function writeFileUpdate( updates: FileUpdate | FileUpdate[], - options?: { - onWrite?: (result: FileUpdate) => void | Promise; - }, + options: WriteFileUpdateOptions = {}, ) { for (const update of [updates].flat()) { - await _write(update); - await options?.onWrite?.(update); + await writeTo(update, options.locks ?? []); + await options.onWrite?.(update); } } -function _write( - update: FileUpdate, -) { +function writeTo(update: FileUpdate, locks: LockPart[]) { switch (update.kind) { case "module": return writeToModule(update as FileUpdate<"module">); case "import_map": return writeToImportMap(update as FileUpdate<"import_map">); + case "lockfile": + return writeToLockfile(update as FileUpdate<"lockfile">, locks); } } @@ -114,7 +126,7 @@ async function writeToModule( dependency.code.span.start.character + 1, dependency.code.span.end.character - 1, ), - toUrl(dependency.to), + stringify(dependency.to), ) : line; }) @@ -123,12 +135,112 @@ async function writeToModule( } async function writeToImportMap( - /** The dependency update to apply. */ update: FileUpdate<"import_map">, ) { let content = await Deno.readTextFile(update.path); for (const dependency of update.dependencies) { - content = content.replaceAll(toUrl(dependency.from), toUrl(dependency.to)); + content = content.replaceAll( + stringify(dependency.from), + stringify(dependency.to), + ); } await Deno.writeTextFile(update.path, content); } + +async function writeToLockfile( + update: FileUpdate<"lockfile">, + locks: LockPart[], +) { + const original = await parseLockFileJson(update.path); + + for await (const dependency of update.dependencies) { + const specifier = dependency.code.specifier; + + // An updated partial lockfile for the dependency. + const { data: patch } = await createLockPart( + specifier, + null, + dependency.from?.protocol.startsWith("http") + ? specifier.replace( + stringify(dependency.from), + stringify(dependency.to), + ) + : undefined, + ); + + // Specifiers that are only depended by the current dependency. + const omitter = createLockFileOmitKeys(specifier, locks); + + if (original.packages && patch.packages) { + original.packages.specifiers = deepMerge( + original.packages.specifiers, + patch.packages.specifiers, + ); + if (patch.packages.jsr) { + original.packages.jsr = deepMerge( + omit(original.packages.jsr ?? {}, omitter.jsr), + patch.packages.jsr, + { arrays: "replace" }, + ); + } + if (patch.packages.npm) { + original.packages.npm = deepMerge( + omit(original.packages.npm ?? {}, omitter.npm), + patch.packages.npm, + ); + } + } + if (patch.remote) { + original.remote = deepMerge( + omit(original.remote ?? {}, omitter.remote), + patch.remote, + ); + } + } + await Deno.writeTextFile(update.path, JSON.stringify(original, replacer, 2)); +} + +function replacer( + key: string, + value: unknown, +) { + return ["specifiers", "jsr", "npm", "remote"].includes(key) && value + ? Object.fromEntries(Object.entries(value).sort()) + : value; +} + +interface LockFileOmitKeys { + jsr: string[]; + npm: string[]; + remote: string[]; +} + +/** Create a list of keys to omit from the original lockfile. */ +function createLockFileOmitKeys( + specifier: string, + locks: LockPart[], +): LockFileOmitKeys { + const [relevant, others] = partition( + locks, + (it) => it.specifier === specifier, + ); + assertEquals(relevant.length, 1); + const { data: patch } = relevant[0]; + return { + jsr: Object.keys(patch.packages?.jsr ?? {}).filter((key) => + !others.some((part) => + Object.keys(part.data.packages?.jsr ?? {}).some((it) => it === key) + ) + ), + npm: Object.keys(patch.packages?.npm ?? {}).filter((key) => + !others.some((part) => + Object.keys(part.data.packages?.npm ?? {}).some((it) => it === key) + ) + ), + remote: Object.keys(patch.remote ?? {}).filter((key) => + !others.some((part) => + Object.keys(part.data.remote ?? {}).some((it) => it === key) + ) + ), + }; +} diff --git a/lib/file_test.ts b/lib/file_test.ts index d4267b04..40bedf6c 100644 --- a/lib/file_test.ts +++ b/lib/file_test.ts @@ -7,12 +7,9 @@ import { } from "./testing.ts"; import { assertInstanceOf } from "./std/assert.ts"; import { assertSnapshot } from "./testing.ts"; -import * as DependencyUpdate from "./update.ts"; -import { associateByFile, type FileUpdate, write } from "./file.ts"; -import { LatestSemVerStub } from "./testing.ts"; - -const LATEST = "123.456.789"; -LatestSemVerStub.create(LATEST); +import { collect, CollectResult } from "./update.ts"; +import { associateByFile, type FileUpdate, writeFileUpdate } from "./file.ts"; +import { LatestVersionStub } from "./testing.ts"; function toName(path: string) { const base = basename(path); @@ -58,27 +55,40 @@ const fs = new FileSystemFake(); ReadTextFileStub.create(fs, { readThrough: true }); WriteTextFileStub.create(fs); -async function test(path: string, name = toName(path)) { - try { - const updates = await DependencyUpdate.collect( - new URL(path, import.meta.url), - { cwd: new URL(dirname(path), import.meta.url) }, - ); - const results = associateByFile(updates); +LatestVersionStub.create({ "deno.land/std": "0.218.0", _: "123.456.789" }); - Deno.test("associateByFile - " + name, async (t) => { - await assertFileUpdateSnapshot(t, results); +function test(path: string, name = toName(path)) { + Deno.test("write - " + name, async (t) => { + let result: CollectResult | undefined = undefined; + let updates: FileUpdate[] = []; + await t.step("associateByFile", async () => { + try { + result = await collect(new URL(path, import.meta.url), { + cwd: new URL(dirname(path), import.meta.url), + lock: path.includes("lockfile"), + }); + updates = associateByFile(result.updates); + await assertFileUpdateSnapshot(t, updates); + } catch (error) { + if (path.includes("import_map_referred/deno.json")) { + // deno.json just reffers to another import_map.json + assertInstanceOf(error, SyntaxError, error); + } else if (path.includes("lockfile_not_importable/deno.json")) { + // can't lock an import specifier that is not importable as is + assertInstanceOf(error, Deno.errors.NotSupported); + } else { + throw error; + } + } }); - - Deno.test("write - " + name, async (t) => { + await t.step("writeFileUpdate", async () => { fs.clear(); - await write(results); - await assertFileSystemSnapshot(t, fs); + if (result) { + await writeFileUpdate(updates, { locks: result.locks }); + await assertFileSystemSnapshot(t, fs); + } }); - } catch (error) { - // import_map_reffered/deno.json just reffers to another import_map.json - assertInstanceOf(error, SyntaxError); - } + }); } // Test the all cases in test/data @@ -86,7 +96,7 @@ for await ( const testCase of Deno.readDir(new URL("../test/data", import.meta.url)) ) { if (testCase.isFile && testCase.name.endsWith(".ts")) { - await test(`../test/data/${testCase.name}`); + test(`../test/data/${testCase.name}`); } if (testCase.isDirectory) { for await ( @@ -98,7 +108,7 @@ for await ( entry.isFile && entry.name === "mod.ts" || entry.name.endsWith(".json") || entry.name.endsWith(".jsonc") ) { - await test(`../test/data/${testCase.name}/${entry.name}`); + test(`../test/data/${testCase.name}/${entry.name}`); } } } diff --git a/lib/git.ts b/lib/git.ts index f2a8d298..b3979845 100644 --- a/lib/git.ts +++ b/lib/git.ts @@ -1,10 +1,7 @@ +import { distinct, mapN } from "./std/collections.ts"; import { relative } from "./std/path.ts"; -import { - type DependencyUpdate, - getVersionChange, - VersionChange, -} from "./update.ts"; -import { associateByFile, type FileUpdate, write } from "./file.ts"; +import { CollectResult, DependencyUpdate } from "./update.ts"; +import { associateByFile, type FileUpdate, writeFileUpdate } from "./file.ts"; export interface CommitProps { /** The name of the module group */ @@ -12,6 +9,16 @@ export interface CommitProps { version?: VersionChange; } +export interface GitCommit extends CommitProps { + message: string; + updates: DependencyUpdate[]; +} + +export interface CommitSequence { + commits: GitCommit[]; + options: CommitOptions; +} + export interface CommitOptions { groupBy?: (dependency: DependencyUpdate) => string; composeCommitMessage?: (props: CommitProps) => string; @@ -37,33 +44,30 @@ const defaultCommitOptions = { gitCommitOptions: [], } satisfies CommitOptions; -export interface GitCommit extends CommitProps { - message: string; - updates: DependencyUpdate[]; -} - -export interface CommitSequence { - commits: GitCommit[]; - options: CommitOptions; -} - -export function commitAll( - updates: DependencyUpdate[], +/** + * Write the given `CollectResult` to file system. + * @returns A promise that resolves when all updates are committed. + */ +export function commit( + result: CollectResult, options?: CommitOptions, ) { - return exec(createCommitSequence(updates, { + return execute(createCommitSequence(result, { ...defaultCommitOptions, ...options, })); } +/** + * Create a sequence of commits from the given `CollectResult`. + */ export function createCommitSequence( - updates: DependencyUpdate[], + result: CollectResult, options?: Partial, ): CommitSequence { const _options = { ...defaultCommitOptions, ...options }; const groups = new Map(); - for (const u of updates) { + for (const u of result.updates) { const key = _options.groupBy(u); if (!groups.has(key)) { groups.set(key, []); @@ -84,10 +88,13 @@ export function createCommitSequence( return { commits, options: _options }; } -export function exec(commit: GitCommit, options?: CommitOptions): Promise; -export function exec(sequence: CommitSequence): Promise; +export function execute( + commit: GitCommit, + options?: CommitOptions, +): Promise; +export function execute(sequence: CommitSequence): Promise; -export function exec( +export function execute( commit: GitCommit | CommitSequence, options?: CommitOptions, ): Promise { @@ -110,7 +117,7 @@ async function execCommit( options?: CommitOptions, ) { const results = associateByFile(commit.updates); - await write(results); + await writeFileUpdate(results); await options?.preCommit?.(commit); await addCommand(results, options?.gitAddOptions ?? []); await commitCommand(commit.message, options?.gitCommitOptions ?? []); @@ -141,3 +148,29 @@ async function commitCommand( throw new Error(new TextDecoder().decode(stderr)); } } + +export type VersionChange = { + from?: string; + to: string; +}; + +export function getVersionChange( + dependencies: DependencyUpdate[], +): VersionChange | undefined { + const modules = distinct(dependencies.map((d) => d.to.name)); + if (modules.length > 1) { + // Cannot provide a well-defined version prop + return; + } + const tos = distinct(dependencies.map((d) => d.to.version)); + if (tos.length > 1) { + throw new Error( + "Multiple target versions are specified for a single module", + ); + } + const froms = distinct(mapN(dependencies, (d) => d.from?.version)); + return { + from: froms.length === 1 ? froms[0] : undefined, + to: tos[0], + }; +} diff --git a/lib/git_test.ts b/lib/git_test.ts index e1557b55..ff308bad 100644 --- a/lib/git_test.ts +++ b/lib/git_test.ts @@ -1,44 +1,184 @@ -import { assertArrayIncludes } from "./std/assert.ts"; -import { assertSpyCall } from "./std/testing.ts"; +import { + assertArrayIncludes, + assertEquals, + assertThrows, +} from "./std/assert.ts"; +import { assertSpyCall, describe, it } from "./std/testing.ts"; import { basename, relative } from "./std/path.ts"; import { CommandStub, FileSystemFake, - LatestSemVerStub, + LatestVersionStub, ReadTextFileStub, WriteTextFileStub, } from "./testing.ts"; import { collect } from "./update.ts"; -import { commitAll } from "./git.ts"; +import { commit, getVersionChange } from "./git.ts"; + +//-------------------------------------------------------------------- +// Unit tests +//-------------------------------------------------------------------- + +describe("getVersionChange", () => { + it("single version", () => { + assertEquals( + getVersionChange([ + { + from: { + name: "deno_graph", + version: "0.0.1", + }, + to: { + name: "deno_graph", + version: "0.1.0", + }, + }, + // deno-lint-ignore no-explicit-any + ] as any), + { + from: "0.0.1", + to: "0.1.0", + }, + ); + }); + it("multiple versions with different names", () => { + assertEquals( + getVersionChange([ + { + from: { + name: "deno_graph", + version: "0.0.1", + }, + to: { + name: "deno_graph", + version: "0.1.0", + }, + }, + { + from: { + name: "node-emoji", + version: "0.0.1", + }, + to: { + name: "node-emoji", + version: "0.1.0", + }, + }, + // deno-lint-ignore no-explicit-any + ] as any), + undefined, + ); + }); + it("multiple versions with different `from`s and a common `to`", () => { + assertEquals( + getVersionChange([ + { + from: { + name: "deno_graph", + version: "0.0.1", + }, + to: { + name: "deno_graph", + version: "0.1.0", + }, + }, + { + from: { + name: "deno_graph", + version: "0.0.2", + }, + to: { + name: "deno_graph", + version: "0.1.0", + }, + }, + // deno-lint-ignore no-explicit-any + ] as any), + { + from: undefined, + to: "0.1.0", + }, + ); + }); + it("multiple versions with a common `from` and `to`", () => { + assertEquals( + getVersionChange([ + { + from: { + name: "deno_graph", + version: "0.0.1", + }, + to: { + name: "deno_graph", + version: "0.2.0", + }, + }, + { + from: { + name: "deno_graph", + version: "0.0.1", + }, + to: { + name: "deno_graph", + version: "0.2.0", + }, + }, + // deno-lint-ignore no-explicit-any + ] as any), + { + from: "0.0.1", + to: "0.2.0", + }, + ); + }); + it("getVersionChange - multiple versions with a common `from` and different `to`s", () => { + assertThrows(() => + getVersionChange([ + { + from: { + name: "deno_graph", + version: "0.0.1", + }, + to: { + name: "deno_graph", + version: "0.1.0", + }, + }, + { + from: { + name: "deno_graph", + version: "0.0.1", + }, + to: { + name: "deno_graph", + version: "0.2.0", + }, + }, + // deno-lint-ignore no-explicit-any + ] as any) + ); + }); +}); -const normalizePath = (path: string) => - Deno.build.os === "windows" ? path.replaceAll("/", "\\") : path; +Deno.test("commit", async (t) => { + const DIR = "test/data/multiple_modules"; -const LATEST = "123.456.789"; + const LATEST = "123.456.789"; + LatestVersionStub.create(LATEST); -const EXPECTED = [ - `import { assert } from "https://deno.land/std@${LATEST}/assert/assert.ts"; + const EXPECTED = [ + `import { assert } from "https://deno.land/std@${LATEST}/assert/assert.ts"; import { createGraph } from "https://deno.land/x/deno_graph@${LATEST}/mod.ts"; import emoji from "npm:node-emoji@${LATEST}"; import { noop } from "./lib.ts"; `, - `import { assertEquals } from "https://deno.land/std@${LATEST}/assert/assert_equals.ts"; + `import { assertEquals } from "https://deno.land/std@${LATEST}/assert/assert_equals.ts"; export const noop = () => {}; `, -]; - -const assertFileSystem = ( - fs: FileSystemFake, -) => assertArrayIncludes(Array.from(fs.values()), EXPECTED); - -const DIR = "test/data/multiple_modules"; - -Deno.test("commitAll", async (t) => { - LatestSemVerStub.create(LATEST); - - const updates = await collect( - new URL(`../${DIR}/mod.ts`, import.meta.url), - ); + ]; + const assertFileSystem = ( + fs: FileSystemFake, + ) => assertArrayIncludes(Array.from(fs.values()), EXPECTED); const cs = CommandStub.create(); Deno.Command = cs; @@ -47,8 +187,10 @@ Deno.test("commitAll", async (t) => { ReadTextFileStub.create(fs, { readThrough: true }); WriteTextFileStub.create(fs); - let calls = 0; + const normalizePath = (path: string) => + Deno.build.os === "windows" ? path.replaceAll("/", "\\") : path; + let calls = 0; const assertGitAdd = ( ...paths: string[] ) => @@ -58,7 +200,6 @@ Deno.test("commitAll", async (t) => { { args: ["add", ...paths.map(normalizePath)] }, ], }); - const assertGitCommit = ( message: string, ) => @@ -69,8 +210,12 @@ Deno.test("commitAll", async (t) => { ], }); + const result = await collect( + new URL(`../${DIR}/mod.ts`, import.meta.url), + ); + await t.step("no grouping", async () => { - await commitAll(updates); + await commit(result); assertGitAdd(`${DIR}/lib.ts`, `${DIR}/mod.ts`); assertGitCommit("build(deps): update dependencies"); assertFileSystem(fs); @@ -79,7 +224,7 @@ Deno.test("commitAll", async (t) => { fs.clear(); await t.step("group by dependency name", async () => { - await commitAll(updates, { + await commit(result, { groupBy: (update) => update.to.name, composeCommitMessage: ({ group }) => `build(deps): update ${group}`, }); @@ -95,7 +240,7 @@ Deno.test("commitAll", async (t) => { fs.clear(); await t.step("group by module (file) name", async () => { - await commitAll(updates, { + await commit(result, { groupBy: (update) => basename(update.referrer), composeCommitMessage: ({ group }) => { const path = relative(Deno.cwd(), group); diff --git a/lib/graph.ts b/lib/graph.ts new file mode 100644 index 00000000..e76a9014 --- /dev/null +++ b/lib/graph.ts @@ -0,0 +1,50 @@ +import { + createGraph, + CreateGraphOptions, + init as initDenoGraph, + load as defaultLoad, +} from "./x/deno_graph.ts"; + +class DenoGraph { + static #initialized = false; + + static async ensureInit() { + if (this.#initialized) { + return; + } + await initDenoGraph(); + this.#initialized = true; + } +} + +export async function createGraphLocally( + specifiers: string[], + options?: CreateGraphOptions, +) { + await DenoGraph.ensureInit(); + return createGraph(specifiers, { + load: async (specifier) => { + const url = new URL(specifier); // should not throw + switch (url.protocol) { + case "node:": + case "npm:": + case "jsr:": + return { + kind: "external", + specifier, + }; + case "http:": + case "https:": + return { + kind: "external", + specifier, + }; + case "file:": + return await defaultLoad(specifier); + default: + throw new Error(`Unsupported protocol: ${url.protocol}`); + } + }, + ...options, + }); +} diff --git a/lib/import_map_test.ts b/lib/import_map_test.ts index 757dd36d..9ccafe85 100644 --- a/lib/import_map_test.ts +++ b/lib/import_map_test.ts @@ -59,9 +59,9 @@ describe("resolve()", () => { assertEquals( importMap.resolve("node-emoji", referrer), { - resolved: "npm:node-emoji@1.0.0", + resolved: "npm:node-emoji@2.0.0", key: "node-emoji", - value: "npm:node-emoji@1.0.0", + value: "npm:node-emoji@2.0.0", }, ); assertEquals( diff --git a/lib/lockfile.ts b/lib/lockfile.ts new file mode 100644 index 00000000..e416fe12 --- /dev/null +++ b/lib/lockfile.ts @@ -0,0 +1,209 @@ +import { parse, UpdatedDependency } from "./dependency.ts"; +import { toPath } from "./path.ts"; +import { distinctBy, mapN } from "./std/collections.ts"; +import { DependencyUpdate } from "./update.ts"; +import { ensure, is, PredicateType } from "./x/unknownutil.ts"; + +const isLockFileJson = is.ObjectOf({ + version: is.String, + packages: is.OptionalOf(is.ObjectOf({ + specifiers: is.RecordOf(is.String, is.String), + jsr: is.OptionalOf(is.RecordOf( + is.ObjectOf({ + dependencies: is.OptionalOf(is.ArrayOf(is.String)), + }), + is.String, + )), + npm: is.OptionalOf(is.RecordOf( + is.ObjectOf({ + integrity: is.String, + dependencies: is.RecordOf(is.String, is.String), + }), + is.String, + )), + })), + remote: is.OptionalOf(is.RecordOf(is.String, is.String)), + workspace: is.OptionalOf(is.ObjectOf({ + dependencies: is.ArrayOf(is.String), + })), +}); + +/** + * A parsed lockfile JSON object. + * @example + * ```ts + * { + * version: "3", + * packages: { + * specifiers: { + * "jsr:@core/match@0.1.x": "jsr:@core/match@0.1.9", + * "npm:node-emoji@^2": "npm:node-emoji@2.1.3", + * "npm:ts-toolbelt@9.6.0": "npm:ts-toolbelt@9.6.0" + * }, + * }, + * } + */ +export type LockFileJson = PredicateType; + +/** An object representing a lockfile. */ +export interface LockFile { + /** The path to the lockfile. */ + path: string; + /** The parsed lockfile JSON object. */ + data: LockFileJson; +} + +/** A partial lock for a specific dependency */ +export interface LockPart { + /** The import specifier of the dependency. */ + specifier: string; + /** The parsed lockfile JSON object. */ + data: LockFileJson; +} + +/** + * Read, parse, and validate a lockfile. + * + * @param specifier - The URL or path to the lockfile. + * @returns The parsed JSON object of the lockfile. + */ +export async function parseLockFileJson( + specifier: URL | string, +): Promise { + try { + return ensure( + JSON.parse(await Deno.readTextFile(specifier)), + isLockFileJson, + ); + } catch (cause) { + throw new Error(`Failed to parse lockfile: ${specifier}`, { cause }); + } +} + +/** + * Read, parse, and validate a lockfile. + * + * @param specifier - The URL or path to the lockfile. + * @returns The parsed `LockFile` object. + */ +export async function parseLockFile( + specifier: URL | string, +): Promise { + return { + path: toPath(specifier), + data: await parseLockFileJson(specifier), + }; +} + +// +// Temporary file management +// +interface TempFile { + path: string; + [Symbol.asyncDispose](): Promise; +} + +async function createTempFile(): Promise { + const path = await Deno.makeTempFile(); + return { + [Symbol.asyncDispose]() { + return Deno.remove(path); + }, + path, + }; +} + +export class CommandError extends Error {} + +/** + * Create a partial lockfile for the given dependency as a temporary file and returns + * the parsed LockFile object. + * + * The implementation here is quite inefficient. We should rather add a JS interface to + * the `deno_lockfile` crate. + * + * @param dependency - The import specifier of dependency to create a lockfile for. + * @param locked - If given, the resulting lock has the same version as this. + * @param lockTo - If given, the resulting lock has the same version as this. + * @returns A promise to the updated lockfile. + */ +export async function createLockPart( + dependency: string, + locked?: LockFile | null, + lockTo?: string, +): Promise { + // Create a dummy module that only includes the given dependencies. + const specifier = lockTo ?? locked?.data.packages?.specifiers[dependency] ?? + dependency; + await using mod = await createTempFile(); + await Deno.writeTextFile(mod.path, `import "${specifier}";\n`); + + // Create a lockfile for the dummy module. + await using lock = await createTempFile(); + const { code, stderr } = await new Deno.Command("deno", { + args: ["cache", "--lock-write", "--lock", lock.path, mod.path], + }).output(); + if (code !== 0) { + throw new CommandError(new TextDecoder().decode(stderr)); + } + return { + specifier: dependency, + data: await parseLockFileJson(lock.path), + }; +} + +/** + * Create a new lockfile for each dependency and returns a list of them. + * + * @param lockfile - The path to the lockfile. + * @returns A Promise for the LockFile objects of updated lockfiles. + */ +export async function createLockPartForEach( + lockfile: LockFile, + update = true, +): Promise { + return await Promise.all( + Object.entries(lockfile.data.packages?.specifiers ?? {}).map( + ([specifier, locked]): Promise => + createLockPart(update ? specifier : locked), + ), + ); +} + +/** + * Collect updates to dependencies in the given lockfile. + * + * @param original - The LockFile object for the original lockfile. + * @param patches - The LockFile objects for dependencies being updated. + * @returns The collected updates to dependencies. + */ +export async function collectUpdateFromLockFile( + original: LockFile, + ...patches: LockPart[] +): Promise[]> { + patches = patches.length ? patches : await createLockPartForEach(original); + return distinctBy( + patches.flatMap( + (patch: LockPart) => + mapN( + Object.entries(patch.data.packages?.specifiers ?? {}), + ([specifier, locking]): DependencyUpdate | undefined => { + const locked = original.data.packages?.specifiers[specifier]; + if (locked !== locking) { + return { + from: locked ? parse(locked) : undefined, + to: parse(locking) as UpdatedDependency, + code: { + specifier, + span: undefined, + }, + referrer: original.path, + map: undefined, + }; + } + }, + ), + ), + (update) => update.to.name, + ).sort((a, b) => a.to.name.localeCompare(b.to.name)); +} diff --git a/lib/lockfile_test.ts b/lib/lockfile_test.ts new file mode 100644 index 00000000..805bf7b1 --- /dev/null +++ b/lib/lockfile_test.ts @@ -0,0 +1,222 @@ +import { + collectUpdateFromLockFile, + createLockPart, + createLockPartForEach, + parseLockFile, + parseLockFileJson, +} from "./lockfile.ts"; +import { assertEquals, assertObjectMatch } from "./std/assert.ts"; + +Deno.test("parseLockFileJson", async () => + assertObjectMatch( + await parseLockFileJson( + new URL("../test/data/lockfile/deno.updated.lock", import.meta.url), + ), + { + version: "3", + packages: { + specifiers: { + "jsr:@core/match@0.1.x": "jsr:@core/match@0.1.9", + "npm:hono@^3": "npm:hono@3.12.12", + "npm:ts-toolbelt@9.6.0": "npm:ts-toolbelt@9.6.0", + }, + }, + }, + )); + +Deno.test("createLockPart - jsr:@core/match", async () => { + const lock = await createLockPart("jsr:@core/match@0.1.x"); + assertObjectMatch( + lock.data, + { + packages: { + specifiers: { + "jsr:@core/match@0.1.x": "jsr:@core/match@0.1.9", + "npm:ts-toolbelt@9.6.0": "npm:ts-toolbelt@9.6.0", + }, + }, + }, + ); +}); + +Deno.test("createLockPart - npm:hono", async () => { + const lock = await createLockPart("npm:hono@^3"); + assertObjectMatch( + lock.data, + { + packages: { + specifiers: { + "npm:hono@^3": "npm:hono@3.12.12", + }, + }, + }, + ); +}); + +Deno.test("createLockPartForEach", async () => { + const updated = await createLockPartForEach( + await parseLockFile( + new URL("../test/data/lockfile/deno.lock", import.meta.url), + ), + ); + assertEquals(updated.length, 3); + assertObjectMatch( + updated[0].data, + { + packages: { + specifiers: { + "jsr:@core/match@0.1.x": "jsr:@core/match@0.1.9", + "npm:ts-toolbelt@9.6.0": "npm:ts-toolbelt@9.6.0", + }, + }, + }, + ); + assertObjectMatch( + updated[1].data, + { + packages: { + specifiers: { + "npm:hono@^3": "npm:hono@3.12.12", + }, + }, + }, + ); + assertObjectMatch( + updated[2].data, + { + packages: { + specifiers: { + "npm:ts-toolbelt@9.6.0": "npm:ts-toolbelt@9.6.0", + }, + }, + }, + ); +}); + +Deno.test("createLockPartForEach - no updates", async () => { + const updated = await createLockPartForEach( + await parseLockFile( + new URL("../test/data/lockfile/deno.lock", import.meta.url), + ), + false, + ); + assertEquals(updated.length, 3); + assertObjectMatch( + updated[0].data, + { + version: "3", + packages: { + specifiers: { + "jsr:@core/match@0.1.0": "jsr:@core/match@0.1.0", + "npm:ts-toolbelt@9.6.0": "npm:ts-toolbelt@9.6.0", + }, + jsr: { "@core/match@0.1.0": {/* won't check */} }, + npm: { "ts-toolbelt@9.6.0": {/* won't check */} }, + }, + remote: { + "https://jsr.io/@core/match/0.1.0/mod.ts": + "15840318ac06cca93d260e9e4dd271d07749f6f463ab04bb0d6afdd72709eaf5", + }, + }, + ); + assertObjectMatch( + updated[1].data, + { + version: "3", + packages: { + specifiers: { "npm:hono@3.0.0": "npm:hono@3.0.0" }, + npm: { "hono@3.0.0": {/* won't check */} }, + }, + remote: {}, + }, + ); + assertObjectMatch( + updated[2].data, + { + version: "3", + packages: { + specifiers: { "npm:ts-toolbelt@9.6.0": "npm:ts-toolbelt@9.6.0" }, + npm: { + "ts-toolbelt@9.6.0": {/* won't check */}, + }, + }, + remote: {}, + }, + ); +}); + +Deno.test("collectUpdateFromLockFile", async () => { + const updates = await collectUpdateFromLockFile( + await parseLockFile( + new URL("../test/data/lockfile/deno.lock", import.meta.url), + ), + ); + assertEquals(updates.length, 2); + assertObjectMatch( + updates[0], + { + from: { + protocol: "jsr:", + name: "@core/match", + version: "0.1.0", + path: "", + }, + to: { + protocol: "jsr:", + name: "@core/match", + version: "0.1.9", + path: "", + }, + code: { specifier: "jsr:@core/match@0.1.x", span: undefined }, + map: undefined, + }, + ); + assertObjectMatch( + updates[1], + { + from: { + protocol: "npm:", + name: "hono", + version: "3.0.0", + path: "", + }, + to: { + protocol: "npm:", + name: "hono", + version: "3.12.12", + path: "", + }, + code: { specifier: "npm:hono@^3", span: undefined }, + map: undefined, + }, + ); +}); + +Deno.test("collectUpdateFromLockFile - with a patch", async () => { + const updates = await collectUpdateFromLockFile( + await parseLockFile( + new URL("../test/data/lockfile/deno.lock", import.meta.url), + ), + await createLockPart("npm:hono@^3"), + ); + assertEquals(updates.length, 1); + assertObjectMatch( + updates[0], + { + from: { + protocol: "npm:", + name: "hono", + version: "3.0.0", + path: "", + }, + to: { + protocol: "npm:", + name: "hono", + version: "3.12.12", + path: "", + }, + code: { specifier: "npm:hono@^3", span: undefined }, + map: undefined, + }, + ); +}); diff --git a/lib/std/collections.ts b/lib/std/collections.ts index a7116a78..1f49f89a 100644 --- a/lib/std/collections.ts +++ b/lib/std/collections.ts @@ -1,6 +1,11 @@ +export { deepMerge } from "https://deno.land/std@0.218.2/collections/deep_merge.ts"; export { distinct } from "https://deno.land/std@0.218.2/collections/distinct.ts"; -export { maxBy } from "https://deno.land/std@0.218.2/collections/max_by.ts"; -export { filterKeys } from "https://deno.land/std@0.218.2/collections/filter_keys.ts"; +export { distinctBy } from "https://deno.land/std@0.218.2/collections/distinct_by.ts"; export { filterEntries } from "https://deno.land/std@0.218.2/collections/filter_entries.ts"; +export { filterKeys } from "https://deno.land/std@0.218.2/collections/filter_keys.ts"; +export { mapValues } from "https://deno.land/std@0.218.2/collections/map_values.ts"; export { mapEntries } from "https://deno.land/std@0.218.2/collections/map_entries.ts"; +export { mapNotNullish as mapN } from "https://deno.land/std@0.218.2/collections/map_not_nullish.ts"; +export { maxBy } from "https://deno.land/std@0.218.2/collections/max_by.ts"; +export { omit } from "https://deno.land/std@0.218.2/collections/omit.ts"; export { partition } from "https://deno.land/std@0.218.2/collections/partition.ts"; diff --git a/lib/std/fs.ts b/lib/std/fs.ts index 09000b18..ee55cf39 100644 --- a/lib/std/fs.ts +++ b/lib/std/fs.ts @@ -1,4 +1,4 @@ -export { existsSync } from "https://deno.land/std@0.218.2/fs/exists.ts"; +export { exists, existsSync } from "https://deno.land/std@0.218.2/fs/exists.ts"; export { detect as detectEOL, EOL, diff --git a/lib/testing.ts b/lib/testing.ts index 851a5208..7bebe502 100644 --- a/lib/testing.ts +++ b/lib/testing.ts @@ -1,6 +1,6 @@ import { createAssertSnapshot, spy, Stub, stub } from "./std/testing.ts"; import { formatEOL, LF } from "./std/fs.ts"; -import { fromFileUrl } from "./std/path.ts"; +import { dirname, fromFileUrl } from "./std/path.ts"; export const assertSnapshot = createAssertSnapshot({ dir: fromFileUrl(new URL("../test/snapshots/", import.meta.url)), @@ -48,7 +48,7 @@ export const ReadTextFileStub = { "readTextFile", async (path) => { return fs.get(path.toString()) ?? - (options?.readThrough + (path.toString().startsWith("/tmp") || options?.readThrough ? await original(path) : _throw(new Deno.errors.NotFound(`File not found: ${path}`))); }, @@ -61,18 +61,31 @@ export const WriteTextFileStub = { create( fs: FileSystemFake, ) { + const original = Deno.writeTextFile; + const tmp = getTempDir(); return stub( Deno, "writeTextFile", (path, data) => { - fs.set(path.toString(), formatEOL(data.toString(), LF)); - return Promise.resolve(); + if (path.toString().startsWith(tmp)) { + return original(path, data); + } else { + fs.set(path.toString(), formatEOL(data.toString(), LF)); + return Promise.resolve(); + } }, ); }, }; export type WriteTextFileStub = ReturnType; +function getTempDir() { + const temp = Deno.makeTempFileSync(); + const tempDir = dirname(temp); + Deno.removeSync(temp); + return tempDir; +} + export const FetchStub = { create( createResponse: ( @@ -91,13 +104,23 @@ export const FetchStub = { }; export type FetchStub = ReturnType; -export const LatestSemVerStub = { - create(latest: string): FetchStub { +export const LatestVersionStub = { + create( + replacer: string | Record, + ): FetchStub { return FetchStub.create((request, init) => { request = (request instanceof Request) ? request : new Request(request, init); const url = new URL(request.url); + const latest = typeof replacer === "string" + ? replacer + : Object.entries(replacer) + .find(([pattern]) => url.href.includes(pattern))?.[1] ?? + replacer["_"]; + if (!latest) { + return init.original(request, init); + } switch (url.hostname) { case "registry.npmjs.org": return new Response( @@ -136,7 +159,7 @@ export const LatestSemVerStub = { }); }, }; -export type LatestSemVerStub = ReturnType; +export type LatestVersionStub = ReturnType; function parseDenoLandUrl(url: URL) { const std = url.pathname.startsWith("/std"); @@ -167,7 +190,7 @@ export function enableTestMode() { const fs = new FileSystemFake(); ReadTextFileStub.create(fs, { readThrough: true }); WriteTextFileStub.create(fs); - LatestSemVerStub.create("123.456.789"); + LatestVersionStub.create("123.456.789"); Deno.Command = CommandStub.create(); } diff --git a/lib/update.ts b/lib/update.ts index 6ddc5d04..feaaae9b 100644 --- a/lib/update.ts +++ b/lib/update.ts @@ -1,12 +1,8 @@ -import { distinct, partition } from "./std/collections.ts"; -import { fromFileUrl } from "./std/path.ts"; -import { - createGraph, - type CreateGraphOptions, - init as initDenoGraph, - load as defaultLoad, - type ModuleJson, -} from "./x/deno_graph.ts"; +import { assertExists } from "./std/assert.ts"; +import { partition } from "./std/collections.ts"; +import { exists } from "./std/fs.ts"; +import { ModuleJson } from "./x/deno_graph.ts"; +import { createGraphLocally } from "./graph.ts"; import { findFileUp, toPath, toUrl } from "./path.ts"; import { ImportMap, @@ -15,24 +11,38 @@ import { tryReadFromJson, } from "./import_map.ts"; import { - type Dependency, + Dependency, + hasVersionRange, parse, resolveLatestVersion, - toUrl as dependencyToUrl, - type UpdatedDependency, + stringify, + UpdatedDependency, } from "./dependency.ts"; +import { + collectUpdateFromLockFile, + CommandError, + createLockPart, + LockFile, + LockPart, + parseLockFile, +} from "./lockfile.ts"; -type DependencyJson = NonNullable[number]; - -type If = T extends true ? A : B; +export type SourceType = "import_map" | "module" | "lockfile"; /** * Representation of an update to a dependency. */ -export interface DependencyUpdate { - /** Properties of the dependency being updated. */ - from: Dependency; - /** Properties of the updated dependency. */ +export interface DependencyUpdate< + T extends SourceType = SourceType, +> { + /** + * Properties of the dependency being updated. + * Undefined if the dependency is added. + */ + from: T extends "lockfile" ? Dependency | undefined : Dependency; + /* + * Properties of the updated dependency. + */ to: UpdatedDependency; /** * The code of the dependency. Note that `type` in the DependencyJSON @@ -41,32 +51,30 @@ export interface DependencyUpdate { code: { /** The original specifier of the dependency appeared in the code. */ specifier: string; - span: If["span"]>; + span: T extends "module" ? NonNullable["span"] + : undefined; }; - /** The full path to the module that imports the dependency. - * @example "/path/to/mod.ts" */ - referrer: string; - /** Information about the import map used to resolve the dependency. */ - map: If< - IsMapped, - { + /** + * Information about the import map used to resolve the dependency. + */ + map: T extends "import_map" ? { /** The full path to the import map used to resolve the dependency. * @example "/path/to/import_map.json" */ source: string; - } & ImportMapResolveResult, - undefined - >; + } & ImportMapResolveResult + : undefined; + /** The full path to the module that imports the dependency. + * @example "/path/to/mod.ts" */ + referrer: string; } -class DenoGraph { - static #initialized = false; - - static async ensureInit() { - if (this.#initialized) { - return; - } - await initDenoGraph(); - this.#initialized = true; +export function sourceTypeOf(update: DependencyUpdate): SourceType { + if (update.map) { + return "import_map"; + } else if (update.code.span) { + return "module"; + } else { + return "lockfile"; } } @@ -79,8 +87,6 @@ export interface CollectOptions { /** * The working directory to resolve relative paths. * If not specified, the current working directory is used. - * At present, this option is only used to find the import map. - * * @example "/path/to/project" */ cwd?: string | URL; @@ -88,7 +94,6 @@ export interface CollectOptions { * The path to the import map used to resolve dependencies. * If not specified, molt will automatically find deno.json or deno.jsonc * in the current working directory or parent directories. - * * @example * ```ts * const updates = await DependencyUpdate.collect("mod.ts", { @@ -98,9 +103,20 @@ export interface CollectOptions { * ``` */ importMap?: string | URL; + /** + * Whether to collect updates from the lockfile and update it. + * @default true + */ + lock?: boolean; + /** + * The path to the lockfile being updated. + * If not specified, molt will try to find `deno.lock` in the current directory + * or parent directories. + * @example "./deno/lock.json" + */ + lockFile?: string | URL; /** * A function to filter out dependencies. - * * @example * ```ts * const updates = await DependencyUpdate.collect("mod.ts", { @@ -112,7 +128,6 @@ export interface CollectOptions { ignore?: (dependency: Dependency) => boolean; /** * A function to pick dependencies. - * * @example * ```ts * const updates = await DependencyUpdate.collect("mod.ts", { @@ -124,10 +139,20 @@ export interface CollectOptions { only?: (dependency: Dependency) => boolean; } +/** + * The result of collecting dependencies. + */ +export interface CollectResult { + /** Partial lockfiles for all dependencies found, which will be used to update the lockfile. */ + locks: LockPart[]; + /** The updates to dependencies. */ + updates: DependencyUpdate[]; +} + /** * Collect dependencies from the given module(s) or Deno configuration file(s). * Local submodules are also checked recursively. - * + * @param from - The path(s) to the file(s) to collect dependencies from. * @param options - Options to customize the behavior. * @returns The list of dependencies. @@ -146,184 +171,245 @@ export interface CollectOptions { export async function collect( from: string | URL | (string | URL)[], options: CollectOptions = {}, -): Promise { - const froms = [from].flat(); - const urls = froms.map((path) => toUrl(path)); +): Promise { + const cwd = options.cwd ?? Deno.cwd(); const importMapPath = options.importMap ?? - await findFileUp(options.cwd ?? Deno.cwd(), "deno.json", "deno.jsonc"); - + await findFileUp(cwd, "deno.json", "deno.jsonc"); const importMap = importMapPath ? await tryReadFromJson(toUrl(importMapPath)) : undefined; + const lockFilePath = options.lock + ? (options.lockFile ?? + (importMapPath + ? await maybeFile(new URL("deno.lock", toUrl(importMapPath))) + : undefined)) + : undefined; + const lockFile = lockFilePath ? await parseLockFile(lockFilePath) : undefined; + + const urls = [from].flat().map((path) => toUrl(path)); const [jsons, esms] = partition(urls, isJsonPath); - await DenoGraph.ensureInit(); - const graph = await createGraph(esms, { - load, + const graph = await createGraphLocally(esms, { resolve: importMap?.resolveInner, }); - const updates: DependencyUpdate[] = []; - await Promise.all([ - ...graph.modules.flatMap((mod) => - mod.dependencies?.map(async (dependency) => { - const update = await _createDependencyUpdate( - dependency, - mod.specifier, - { cache: true, ...options, importMap }, - ); - if (update) updates.push(update); - }) - ), - ...jsons.map(async (url) => { - const results = await _collectFromImportMap(url, options); - updates.push(...results); - }), - ]); - return updates.sort((a, b) => a.to.name.localeCompare(b.to.name)); + const _options: CollectInnerOptions = { + cache: true, + ...options, + importMap, + lockFile, + }; + const result = reduceCollectResult( + await Promise.all([ + ...graph.modules.flatMap((mod) => + (mod.dependencies ?? []).map((dependency) => + collectFromDependency(dependency, mod.specifier, _options) + ) + ), + ...jsons.map((url) => collectFromImportMap(url, _options)), + ]), + ); + return { + locks: result.locks, + updates: result.updates.sort((a, b) => a.to.name.localeCompare(b.to.name)), + }; } -const isJsonPath = (path: string) => - path.endsWith(".json") || path.endsWith(".jsonc"); +//---------------------------------- +// +// Inner functions and types +// +//---------------------------------- -const load: NonNullable = async ( - specifier, -) => { - const url = new URL(specifier); // should not throw - switch (url.protocol) { - case "node:": - case "npm:": - case "jsr:": - return { - kind: "external", - specifier, - }; - case "http:": - case "https:": - return { - kind: "external", - specifier, - }; - case "file:": - return await defaultLoad(specifier); - default: - throw new Error(`Unsupported protocol: ${url.protocol}`); - } -}; +type DependencyJson = NonNullable[number]; -/** - * Create a DependencyUpdate from the given dependency. - * @param dependencyJson - The dependency to create an update from. - * @param referrer - The URL of the module that imports the dependency. - * @param options - Options to customize the behavior. - * @returns The created DependencyUpdate. - */ -async function _createDependencyUpdate( +interface CollectInnerOptions + extends Omit { + importMap?: ImportMap; + lockFile?: LockFile; +} + +function reduceCollectResult( + results: CollectResult[], +): CollectResult { + return results.reduce( + (acc, { locks, updates }) => ({ + locks: acc.locks.concat(locks), + updates: acc.updates.concat(updates), + }), + { locks: [], updates: [] }, + ); +} + +/** Create a DependencyUpdate from the given dependency. */ +async function collectFromDependency( dependencyJson: DependencyJson, referrer: string, - options?: Pick & { - importMap?: ImportMap; - }, -): Promise { - const specifier = dependencyJson.code?.specifier ?? + options: CollectInnerOptions, +): Promise { + const resolved = dependencyJson.code?.specifier ?? dependencyJson.type?.specifier; - if (!specifier) { - throw new Error( - `The dependency ${dependencyJson.specifier} in ${ - fromFileUrl(referrer) - } has no resolved specifier.`, - { cause: dependencyJson }, - ); - } - const mapped = options?.importMap?.resolve( + assertExists(resolved); + const lock = options.lockFile + ? await createLockPart(resolved, options.lockFile) + : undefined; + const mapped = options.importMap?.resolve( dependencyJson.specifier, referrer, ) as ImportMapResolveResult | undefined; - const dependency = parse(new URL(mapped?.value ?? specifier)); - if (options?.ignore?.(dependency)) { - return; + const dependency = parse(new URL(mapped?.value ?? resolved)); + const none = { locks: lock ? [lock] : [], updates: [] }; + if (options.ignore?.(dependency) || options.only?.(dependency) === false) { + return none; } - if (options?.only && !options.only(dependency)) { - return; + if (options.lockFile && hasVersionRange(dependency)) { + return { + ...none, + updates: await collectUpdateFromLockFile( + options.lockFile, + await createLockPart(resolved), + ), + }; } const latest = await resolveLatestVersion(dependency, { - cache: options?.cache, + cache: options.cache, }); if (!latest || latest.version === dependency.version) { - return; + return none; } const span = dependencyJson.code?.span ?? dependencyJson.type?.span; - if (!span) { - throw new Error( - `The dependency ${dependencyJson.specifier} in ${ - fromFileUrl(referrer) - } has no span.`, - ); - } - return { + assertExists(span); + const update = { from: normalizeWithUpdated(dependency, latest), to: latest, + }; + const updates: DependencyUpdate[] = [{ + ...update, code: { // We prefer to put the original specifier here. specifier: dependencyJson.specifier, span, }, + map: mapped ? { source: options.importMap!.path, ...mapped } : undefined, referrer: toPath(referrer), - map: mapped - ? { - source: options!.importMap!.path, - ...mapped, - } - : undefined, - }; + }]; + if (options.lockFile) { + updates.push({ + ...update, + code: { specifier: resolved, span: undefined }, + map: undefined, + referrer: options.lockFile.path, + }); + } + return { ...none, updates }; } -async function _collectFromImportMap( - specifier: ModuleJson["specifier"], - options: Pick, -): Promise { - const json = await readImportMapJson(new URL(specifier)); - const updates: DependencyUpdate[] = []; - await Promise.all( - Object.entries(json.imports).map( - async ([key, value]): Promise => { - if (!URL.canParse(value)) { - return; - } - const dependency = parse(new URL(value)); - if (options.ignore?.(dependency)) { - return; - } - if (options.only && !options.only(dependency)) { - return; - } - const latest = await resolveLatestVersion(dependency, { - cache: options.cache, - }); - if (!latest || latest.version === dependency.version) { - return; - } - updates.push({ - from: normalizeWithUpdated(dependency, latest), - to: latest, - code: { - specifier: value, - span: undefined, - }, - referrer: toPath(specifier), - map: { - source: toPath(specifier), - resolved: value, - key, - value: dependencyToUrl(latest), - }, - }); - }, +async function collectFromImportMap( + path: string, + options: CollectInnerOptions, +): Promise { + const json = await readImportMapJson(new URL(path)); + return reduceCollectResult( + await Promise.all( + Object.entries(json.imports).map((entry) => + collectFromImportMapEntry(path, entry, options) + ), ), ); - return updates; +} + +async function collectFromImportMapEntry( + path: string, + entry: [string, string], + options: CollectInnerOptions, +): Promise { + const [mapFrom, mapTo] = entry; + if (!URL.canParse(mapTo)) { // map to a local file + return { locks: [], updates: [] }; + } + const lock = options.lockFile + ? await _createLockPart(mapTo, options.lockFile) + : undefined; + const dependency = parse(new URL(mapTo)); + const none = { locks: lock ? [lock] : [], updates: [] }; + if (options.ignore?.(dependency) || options.only?.(dependency) === false) { + return none; + } + if (options.lockFile && hasVersionRange(dependency)) { + return { + ...none, + updates: await collectUpdateFromLockFile( + options.lockFile, + await createLockPart(mapTo), + ), + }; + } + const latest = await resolveLatestVersion(dependency, { + cache: options.cache, + }); + if (!latest || latest.version === dependency.version) { + return none; + } + const update = { + from: normalizeWithUpdated(dependency, latest), + to: latest, + }; + const updates: DependencyUpdate[] = [{ + ...update, + code: { + specifier: mapTo, + span: undefined, + }, + map: { + source: toPath(path), + resolved: mapTo, + key: mapFrom, + value: stringify(latest), + }, + referrer: toPath(path), + }]; + if (options.lockFile) { + updates.push({ + ...update, + code: { specifier: mapTo, span: undefined }, + map: undefined, + referrer: options.lockFile.path, + }); + } + return { ...none, updates }; +} + +async function _createLockPart( + specifier: string, + locked?: LockFile, +): Promise { + try { + return await createLockPart(specifier, locked); + } catch (err) { + if (err instanceof CommandError) { + throw new Deno.errors.NotSupported( + `Can't update a lockfile for an import map including any URL that can'be imported as is.`, + { cause: specifier }, + ); + } + throw err; + } +} + +//---------------------------------- +// +// Utility functions +// +//---------------------------------- + +async function maybeFile(path: string | URL) { + return await exists(path) ? path : undefined; +} + +function isJsonPath(path: string) { + return path.endsWith(".json") || path.endsWith(".jsonc"); } function normalizeWithUpdated( @@ -338,29 +424,3 @@ function normalizeWithUpdated( version: undefined, }; } - -export type VersionChange = { - from?: string; - to: string; -}; - -export function getVersionChange( - dependencies: DependencyUpdate[], -): VersionChange | undefined { - const modules = distinct(dependencies.map((d) => d.to.name)); - if (modules.length > 1) { - // Cannot provide a well-defined version prop - return; - } - const tos = distinct(dependencies.map((d) => d.to.version)); - if (tos.length > 1) { - throw new Error( - "Multiple target versions are specified for a single module", - ); - } - const froms = distinct(dependencies.map((d) => d.from.version)); - return { - from: froms.length === 1 ? froms[0] : undefined, - to: tos[0], - }; -} diff --git a/lib/update_test.ts b/lib/update_test.ts index 7b0d2692..33419002 100644 --- a/lib/update_test.ts +++ b/lib/update_test.ts @@ -1,58 +1,73 @@ import { dirname } from "./std/path.ts"; -import { assertEquals, assertInstanceOf, assertThrows } from "./std/assert.ts"; +import { assertEquals, assertInstanceOf } from "./std/assert.ts"; import { filterKeys } from "./std/collections.ts"; import { basename } from "./std/path.ts"; import { assertSnapshot } from "./testing.ts"; -import { LatestSemVerStub } from "./testing.ts"; -import { collect, DependencyUpdate, getVersionChange } from "./update.ts"; +import { LatestVersionStub } from "./testing.ts"; +import { collect, DependencyUpdate } from "./update.ts"; -async function test( +function test( path: string, name = basename(path), variation?: string, ) { - try { - const updates = await collect(new URL(path, import.meta.url), { - cache: false, - cwd: new URL(dirname(path), import.meta.url), - }); - Deno.test( - "collect - " + (variation ? `${name} - ${variation}` : name), - async (t) => { - for (const update of updates) { - await assertUpdateSnapshot(t, update); + Deno.test( + "collect - " + (variation ? `${name} - ${variation}` : name), + async (t) => { + try { + const actual = await collect(new URL(path, import.meta.url), { + cwd: new URL(dirname(path), import.meta.url), + lock: path.includes("lockfile"), + }); + const no_cache = await collect(new URL(path, import.meta.url), { + cache: false, + cwd: new URL(dirname(path), import.meta.url), + lock: path.includes("lockfile"), + }); + assertEquals(actual, no_cache, "cache should not affect results"); + await assertUpdateSnapshot(t, actual.updates); + if (actual.locks.length) { + await assertSnapshot(t, actual.locks); } - }, - ); - } catch (error) { - // import_map_reffered/deno.json just reffers to another import_map.json - assertInstanceOf(error, SyntaxError); - } + } catch (error) { + if (path.includes("import_map_referred/deno.json")) { + // deno.json just reffers to another import_map.json + assertInstanceOf(error, SyntaxError, error); + } else if (path.includes("lockfile_not_importable/deno.json")) { + // can't lock an import specifier that is not importable as is + assertInstanceOf(error, Deno.errors.NotSupported); + } else { + throw error; + } + } + }, + ); } async function assertUpdateSnapshot( t: Deno.TestContext, - update: DependencyUpdate, + actual: DependencyUpdate[], ) { await assertSnapshot( t, - filterKeys( - // deno-lint-ignore no-explicit-any - update as Readonly>, - (key) => ["from", "to", "code"].includes(key), + actual.map((update) => + filterKeys( + // deno-lint-ignore no-explicit-any + update as Readonly>, + (key) => ["from", "to", "code"].includes(key), + ) ), ); } -const LATEST = "123.456.789"; -LatestSemVerStub.create(LATEST); +LatestVersionStub.create({ "deno.land/std": "0.218.0", _: "123.456.789" }); // Test collect() for all cases in test/data for await ( const testCase of Deno.readDir(new URL("../test/data", import.meta.url)) ) { if (testCase.isFile && testCase.name.endsWith(".ts")) { - await test(`../test/data/${testCase.name}`); + test(`../test/data/${testCase.name}`); } if (testCase.isDirectory) { for await ( @@ -62,9 +77,9 @@ for await ( ) { if ( entry.isFile && entry.name === "mod.ts" || - entry.name.endsWith(".json") || entry.name.endsWith("jsonc") + entry.name.endsWith(".json") || entry.name.endsWith(".jsonc") ) { - await test( + test( `../test/data/${testCase.name}/${entry.name}`, testCase.name, entry.name, @@ -73,146 +88,3 @@ for await ( } } } - -Deno.test("getVersionChange - single version", () => { - assertEquals( - getVersionChange([ - { - from: { - name: "deno_graph", - version: "0.0.1", - }, - to: { - name: "deno_graph", - version: "0.1.0", - }, - }, - // deno-lint-ignore no-explicit-any - ] as any), - { - from: "0.0.1", - to: "0.1.0", - }, - ); -}); - -Deno.test("getVersionChange - multiple versions with different names", () => { - assertEquals( - getVersionChange([ - { - from: { - name: "deno_graph", - version: "0.0.1", - }, - to: { - name: "deno_graph", - version: "0.1.0", - }, - }, - { - from: { - name: "node-emoji", - version: "0.0.1", - }, - to: { - name: "node-emoji", - version: "0.1.0", - }, - }, - // deno-lint-ignore no-explicit-any - ] as any), - undefined, - ); -}); - -Deno.test("getVersionChange - multiple versions with different `from`s and a common `to`", () => { - assertEquals( - getVersionChange([ - { - from: { - name: "deno_graph", - version: "0.0.1", - }, - to: { - name: "deno_graph", - version: "0.1.0", - }, - }, - { - from: { - name: "deno_graph", - version: "0.0.2", - }, - to: { - name: "deno_graph", - version: "0.1.0", - }, - }, - // deno-lint-ignore no-explicit-any - ] as any), - { - from: undefined, - to: "0.1.0", - }, - ); -}); - -Deno.test("getVersionChange - multiple versions with a common `from` and `to`", () => { - assertEquals( - getVersionChange([ - { - from: { - name: "deno_graph", - version: "0.0.1", - }, - to: { - name: "deno_graph", - version: "0.2.0", - }, - }, - { - from: { - name: "deno_graph", - version: "0.0.1", - }, - to: { - name: "deno_graph", - version: "0.2.0", - }, - }, - // deno-lint-ignore no-explicit-any - ] as any), - { - from: "0.0.1", - to: "0.2.0", - }, - ); -}); - -Deno.test("getVersionChange - multiple versions with a common `from` and different `to`s", () => { - assertThrows(() => - getVersionChange([ - { - from: { - name: "deno_graph", - version: "0.0.1", - }, - to: { - name: "deno_graph", - version: "0.1.0", - }, - }, - { - from: { - name: "deno_graph", - version: "0.0.1", - }, - to: { - name: "deno_graph", - version: "0.2.0", - }, - }, - // deno-lint-ignore no-explicit-any - ] as any) - ); -}); diff --git a/lib/x/unknownutil.ts b/lib/x/unknownutil.ts index 1f452fb6..07597a37 100644 --- a/lib/x/unknownutil.ts +++ b/lib/x/unknownutil.ts @@ -2,4 +2,5 @@ export { assert, ensure, is, -} from "https://deno.land/x/unknownutil@v3.16.3/mod.ts"; + type PredicateType, +} from "https://deno.land/x/unknownutil@v3.17.0/mod.ts"; diff --git a/mod.ts b/mod.ts index e1ddc981..3679ddac 100644 --- a/mod.ts +++ b/mod.ts @@ -8,20 +8,20 @@ * To update all dependencies in a module and write the changes to files: * * ```ts - * import { collect, writeAll } from "https://deno.land/x/molt@{VERSION}/mod.ts"; + * import { collect, write } from "https://deno.land/x/molt@{VERSION}/mod.ts"; * - * const updates = await collect("./mod.ts"); - * await writeAll(updates); + * const result = await collect("./mod.ts"); + * await write(result); * ``` * * To update all dependencies in a module and commit the changes to local git repository: * * ```ts - * import { collect, commitAll } from "https://deno.land/x/molt@{VERSION}/mod.ts"; + * import { collect, commit } from "https://deno.land/x/molt@{VERSION}/mod.ts"; * - * const updates = await collect("./mod.ts"); + * const result = await collect("./mod.ts"); * - * await commitAll(updates, { + * await commitAll(result, { * groupBy: (dependency) => dependency.name, * composeCommitMessage: ({ group, version }) => * `build(deps): bump ${group} to ${version!.to}`, @@ -31,7 +31,28 @@ * @module */ -export * from "./lib/update.ts"; -export * from "./lib/file.ts"; -export * from "./lib/git.ts"; -export * from "./lib/dependency.ts"; +export { + type Dependency, + parse, + resolveLatestVersion, + stringify, + type UpdatedDependency, +} from "./lib/dependency.ts"; + +export { type FileUpdate, write, type WriteOptions } from "./lib/file.ts"; + +export { + commit, + type CommitSequence, + createCommitSequence, + execute, + type GitCommit, +} from "./lib/git.ts"; + +export { + collect, + type CollectOptions, + type CollectResult, + type DependencyUpdate, + type SourceType, +} from "./lib/update.ts"; diff --git a/test/data/import_map/deno.json b/test/data/import_map/deno.json index 6a32c256..dc8c0c50 100644 --- a/test/data/import_map/deno.json +++ b/test/data/import_map/deno.json @@ -2,7 +2,7 @@ "imports": { "std/": "https://deno.land/std@0.200.0/", "deno_graph": "https://deno.land/x/deno_graph@0.50.0/mod.ts", - "node-emoji": "npm:node-emoji@1.0.0", + "node-emoji": "npm:node-emoji@2.0.0", "flag": "jsr:@luca/flag@1.0.0", "/": "./" } diff --git a/test/data/jsonc/deno.jsonc b/test/data/jsonc/deno.jsonc index dee039a8..6fa31e3b 100644 --- a/test/data/jsonc/deno.jsonc +++ b/test/data/jsonc/deno.jsonc @@ -2,7 +2,7 @@ "imports": { "std/": "https://deno.land/std@0.200.0/", "deno_graph": "https://deno.land/x/deno_graph@0.50.0/mod.ts", - "node-emoji": "npm:node-emoji@1.0.0", + "node-emoji": "npm:node-emoji@2.0.0", "flag": "jsr:@luca/flag@1.0.0", // map root to the project root "/": "./" diff --git a/test/data/lockfile/deno.json b/test/data/lockfile/deno.json new file mode 100644 index 00000000..5a1ccddc --- /dev/null +++ b/test/data/lockfile/deno.json @@ -0,0 +1,7 @@ +{ + "imports": { + "std/bytes": "https://deno.land/std@0.200.0/bytes/copy.ts", + "hono": "npm:hono@^3", + "@core/match": "jsr:@core/match@0.1.x" + } +} diff --git a/test/data/lockfile/deno.lock b/test/data/lockfile/deno.lock new file mode 100644 index 00000000..8f9a9c23 --- /dev/null +++ b/test/data/lockfile/deno.lock @@ -0,0 +1,37 @@ +{ + "version": "3", + "packages": { + "specifiers": { + "jsr:@core/match@0.1.x": "jsr:@core/match@0.1.0", + "npm:hono@^3": "npm:hono@3.0.0", + "npm:ts-toolbelt@9.6.0": "npm:ts-toolbelt@9.6.0" + }, + "jsr": { + "@core/match@0.1.0": { + "dependencies": [ + "npm:ts-toolbelt@9.6.0" + ] + } + }, + "npm": { + "hono@3.0.0": { + "integrity": "sha512-OBWEvcRydCOLdOiazqOmKy8PXciR8uIyUM9HpmXot7UmwNBGOdEJvTrVRIgjV603w+go6cTXBOvFiQClSABT6Q==", + "dependencies": {} + }, + "ts-toolbelt@9.6.0": { + "integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==", + "dependencies": {} + } + } + }, + "remote": { + "https://deno.land/std@0.200.0/bytes/copy.ts": "939d89e302a9761dcf1d9c937c7711174ed74c59eef40a1e4569a05c9de88219", + "https://jsr.io/@core/match/0.1.0/mod.ts": "15840318ac06cca93d260e9e4dd271d07749f6f463ab04bb0d6afdd72709eaf5" + }, + "workspace": { + "dependencies": [ + "jsr:@core/match@0.1.x", + "npm:hono@^3" + ] + } +} diff --git a/test/data/lockfile/deno.updated.lock b/test/data/lockfile/deno.updated.lock new file mode 100644 index 00000000..afeb801e --- /dev/null +++ b/test/data/lockfile/deno.updated.lock @@ -0,0 +1,37 @@ +{ + "version": "3", + "packages": { + "specifiers": { + "jsr:@core/match@0.1.x": "jsr:@core/match@0.1.9", + "npm:hono@^3": "npm:hono@3.12.12", + "npm:ts-toolbelt@9.6.0": "npm:ts-toolbelt@9.6.0" + }, + "jsr": { + "@core/match@0.1.9": { + "dependencies": [ + "npm:ts-toolbelt@9.6.0" + ] + } + }, + "npm": { + "hono@3.12.12": { + "integrity": "sha512-5IAMJOXfpA5nT+K0MNjClchzz0IhBHs2Szl7WFAhrFOsbtQsYmNynFyJRg/a3IPsmCfxcrf8txUGiNShXpK5Rg==", + "dependencies": {} + }, + "ts-toolbelt@9.6.0": { + "integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==", + "dependencies": {} + } + } + }, + "remote": { + "https://deno.land/std@123.456.789/bytes/copy.ts": "939d89e302a9761dcf1d9c937c7711174ed74c59eef40a1e4569a05c9de88219", + "https://jsr.io/@core/match/0.1.9/mod.ts": "5b9781f50b239d6cdc5cf99208418e0c16f732fbb2b7237454336559ca73537f" + }, + "workspace": { + "dependencies": [ + "jsr:@core/match@0.1.x", + "npm:hono@^3" + ] + } +} diff --git a/test/data/lockfile/mod.ts b/test/data/lockfile/mod.ts new file mode 100644 index 00000000..1ae6fa40 --- /dev/null +++ b/test/data/lockfile/mod.ts @@ -0,0 +1,3 @@ +import { copy } from "std/bytes"; +import { App } from "hono"; +import { match } from "@core/match"; diff --git a/test/data/lockfile_not_importable/deno.json b/test/data/lockfile_not_importable/deno.json new file mode 100644 index 00000000..4fe88450 --- /dev/null +++ b/test/data/lockfile_not_importable/deno.json @@ -0,0 +1,5 @@ +{ + "imports": { + "std/": "https://deno.land/std@0.200.0/" + } +} diff --git a/test/data/lockfile_not_importable/deno.lock b/test/data/lockfile_not_importable/deno.lock new file mode 100644 index 00000000..ae412824 --- /dev/null +++ b/test/data/lockfile_not_importable/deno.lock @@ -0,0 +1,6 @@ +{ + "version": "3", + "remote": { + "https://deno.land/std@0.200.0/version.ts": "ae3fc7003d763c0d78f76fad04df179529d0aef813a247b4a3909e22adf6bfd1" + } +} diff --git a/test/data/lockfile_not_importable/mod.ts b/test/data/lockfile_not_importable/mod.ts new file mode 100644 index 00000000..e3f8f1e8 --- /dev/null +++ b/test/data/lockfile_not_importable/mod.ts @@ -0,0 +1 @@ +import "std/version.ts"; diff --git a/test/data/updated_and_outdated.ts b/test/data/updated_and_outdated.ts index 410def46..0d20caf4 100644 --- a/test/data/updated_and_outdated.ts +++ b/test/data/updated_and_outdated.ts @@ -1,2 +1,2 @@ -import { assert } from "https://deno.land/std@123.456.789/assert/assert.ts"; +import { assert } from "https://deno.land/std@0.218.0/assert/assert.ts"; import { assertEquals } from "https://deno.land/std@0.200.0/assert/assert_equals.ts"; diff --git a/test/data/updated_import_and_outdated_export.ts b/test/data/updated_import_and_outdated_export.ts index 5fe3ae96..c2992fb7 100644 --- a/test/data/updated_import_and_outdated_export.ts +++ b/test/data/updated_import_and_outdated_export.ts @@ -1,2 +1,2 @@ -import { assert } from "https://deno.land/std@123.456.789/assert/assert.ts"; +import { assert } from "https://deno.land/std@0.218.0/assert/assert.ts"; export { assertEquals } from "https://deno.land/std@0.200.0/assert/assert_equals.ts"; diff --git a/test/snapshots/cli.ts.snap b/test/snapshots/cli.ts.snap index 97c95efb..8d719ee8 100644 --- a/test/snapshots/cli.ts.snap +++ b/test/snapshots/cli.ts.snap @@ -121,7 +121,7 @@ snapshot[`cli - import_map - "molt mod.ts" 1`] = ` "📦 @luca/flag 1.0.0 => 123.456.789 📦 deno.land/std 0.200.0 => 123.456.789 📦 deno.land/x/deno_graph 0.50.0 => 123.456.789 -📦 node-emoji 1.0.0 => 123.456.789 +📦 node-emoji 2.0.0 => 123.456.789 " `; @@ -134,7 +134,7 @@ snapshot[`cli - import_map - "molt mod.ts --import-map deno.json" 1`] = ` "📦 @luca/flag 1.0.0 => 123.456.789 📦 deno.land/std 0.200.0 => 123.456.789 📦 deno.land/x/deno_graph 0.50.0 => 123.456.789 -📦 node-emoji 1.0.0 => 123.456.789 +📦 node-emoji 2.0.0 => 123.456.789 " `; @@ -380,7 +380,7 @@ snapshot[`cli - import_map - "molt deno.json" 1`] = ` "📦 @luca/flag 1.0.0 => 123.456.789 📦 deno.land/std 0.200.0 => 123.456.789 📦 deno.land/x/deno_graph 0.50.0 => 123.456.789 -📦 node-emoji 1.0.0 => 123.456.789 +📦 node-emoji 2.0.0 => 123.456.789 " `; @@ -393,7 +393,7 @@ snapshot[`cli - import_map - "molt deno.json --write" 1`] = ` "📦 @luca/flag 1.0.0 => 123.456.789 📦 deno.land/std 0.200.0 => 123.456.789 📦 deno.land/x/deno_graph 0.50.0 => 123.456.789 -📦 node-emoji 1.0.0 => 123.456.789 +📦 node-emoji 2.0.0 => 123.456.789 💾 deno.json " @@ -408,12 +408,12 @@ snapshot[`cli - import_map - "molt deno.json --commit --prefix :package:" 1`] = "📦 @luca/flag 1.0.0 => 123.456.789 📦 deno.land/std 0.200.0 => 123.456.789 📦 deno.land/x/deno_graph 0.50.0 => 123.456.789 -📦 node-emoji 1.0.0 => 123.456.789 +📦 node-emoji 2.0.0 => 123.456.789 📝 :package: bump @luca/flag from 1.0.0 to 123.456.789 📝 :package: bump deno.land/std from 0.200.0 to 123.456.789 📝 :package: bump deno.land/x/deno_graph from 0.50.0 to 123.456.789 -📝 :package: bump node-emoji from 1.0.0 to 123.456.789 +📝 :package: bump node-emoji from 2.0.0 to 123.456.789 " `; @@ -426,7 +426,7 @@ snapshot[`cli - jsonc - "molt deno.jsonc" 1`] = ` "📦 @luca/flag 1.0.0 => 123.456.789 📦 deno.land/std 0.200.0 => 123.456.789 📦 deno.land/x/deno_graph 0.50.0 => 123.456.789 -📦 node-emoji 1.0.0 => 123.456.789 +📦 node-emoji 2.0.0 => 123.456.789 " `; @@ -439,7 +439,7 @@ snapshot[`cli - jsonc - "molt deno.jsonc --write" 1`] = ` "📦 @luca/flag 1.0.0 => 123.456.789 📦 deno.land/std 0.200.0 => 123.456.789 📦 deno.land/x/deno_graph 0.50.0 => 123.456.789 -📦 node-emoji 1.0.0 => 123.456.789 +📦 node-emoji 2.0.0 => 123.456.789 💾 deno.jsonc " @@ -454,12 +454,12 @@ snapshot[`cli - jsonc - "molt deno.jsonc --commit --prefix :package:" 1`] = ` "📦 @luca/flag 1.0.0 => 123.456.789 📦 deno.land/std 0.200.0 => 123.456.789 📦 deno.land/x/deno_graph 0.50.0 => 123.456.789 -📦 node-emoji 1.0.0 => 123.456.789 +📦 node-emoji 2.0.0 => 123.456.789 📝 :package: bump @luca/flag from 1.0.0 to 123.456.789 📝 :package: bump deno.land/std from 0.200.0 to 123.456.789 📝 :package: bump deno.land/x/deno_graph from 0.50.0 to 123.456.789 -📝 :package: bump node-emoji from 1.0.0 to 123.456.789 +📝 :package: bump node-emoji from 2.0.0 to 123.456.789 " `; diff --git a/test/snapshots/file_test.ts.snap b/test/snapshots/file_test.ts.snap index 0c115558..f23156b7 100644 --- a/test/snapshots/file_test.ts.snap +++ b/test/snapshots/file_test.ts.snap @@ -1,6 +1,6 @@ export const snapshot = {}; -snapshot[`associateByFile - export.ts 1`] = ` +snapshot[`write - export.ts 1`] = ` [ { dependencies: [ @@ -29,7 +29,7 @@ snapshot[`associateByFile - export.ts 1`] = ` name: "deno.land/std", path: "/assert/assert.ts", protocol: "https:", - version: "123.456.789", + version: "0.218.0", }, }, ], @@ -38,14 +38,96 @@ snapshot[`associateByFile - export.ts 1`] = ` ] `; -snapshot[`write - export.ts 1`] = ` +snapshot[`write - export.ts 2`] = ` [ - 'export { assert } from "https://deno.land/std@123.456.789/assert/assert.ts"; + 'export { assert } from "https://deno.land/std@0.218.0/assert/assert.ts"; ', ] `; -snapshot[`associateByFile - multiple_imports.ts 1`] = ` +snapshot[`write - lockfile_not_importable/mod.ts 1`] = ` +[ + { + dependencies: [ + { + code: { + span: { + end: { + character: 23, + line: 0, + }, + start: { + character: 7, + line: 0, + }, + }, + specifier: "std/version.ts", + }, + from: { + name: "deno.land/std", + path: "/", + protocol: "https:", + version: "0.200.0", + }, + map: { + key: "std/", + resolved: "https://deno.land/std@0.200.0/version.ts", + }, + to: { + name: "deno.land/std", + path: "/", + protocol: "https:", + version: "0.218.0", + }, + }, + ], + kind: "import_map", + }, + { + dependencies: [ + { + code: { + span: undefined, + specifier: "https://deno.land/std@0.200.0/version.ts", + }, + from: { + name: "deno.land/std", + path: "/", + protocol: "https:", + version: "0.200.0", + }, + map: undefined, + to: { + name: "deno.land/std", + path: "/", + protocol: "https:", + version: "0.218.0", + }, + }, + ], + kind: "lockfile", + }, +] +`; + +snapshot[`write - lockfile_not_importable/mod.ts 2`] = ` +[ + '{ + "imports": { + "std/": "https://deno.land/std@0.218.0/" + } +} +', + '{ + "version": "3", + "remote": { + "https://deno.land/std@0.218.0/version.ts": "cfb73e2f2b628978b2b70585b941c5ef5ccbf1dc06e76967206115c74ecfce49" + } +}', +] +`; + +snapshot[`write - multiple_imports.ts 1`] = ` [ { dependencies: [ @@ -102,7 +184,7 @@ snapshot[`associateByFile - multiple_imports.ts 1`] = ` name: "deno.land/std", path: "/assert/assert.ts", protocol: "https:", - version: "123.456.789", + version: "0.218.0", }, }, { @@ -167,9 +249,9 @@ snapshot[`associateByFile - multiple_imports.ts 1`] = ` ] `; -snapshot[`write - multiple_imports.ts 1`] = ` +snapshot[`write - multiple_imports.ts 2`] = ` [ - 'import { assert } from "https://deno.land/std@123.456.789/assert/assert.ts"; + 'import { assert } from "https://deno.land/std@0.218.0/assert/assert.ts"; import { createGraph } from "https://deno.land/x/deno_graph@123.456.789/mod.ts"; import emoji from "npm:node-emoji@123.456.789"; import { printProgress } from "jsr:@luca/flag@123.456.789"; @@ -177,7 +259,7 @@ import { printProgress } from "jsr:@luca/flag@123.456.789"; ] `; -snapshot[`associateByFile - import_map_referred/import_map.json 1`] = ` +snapshot[`write - import_map_referred/import_map.json 1`] = ` [ { dependencies: [ @@ -209,7 +291,7 @@ snapshot[`associateByFile - import_map_referred/import_map.json 1`] = ` ] `; -snapshot[`write - import_map_referred/import_map.json 1`] = ` +snapshot[`write - import_map_referred/import_map.json 2`] = ` [ '{ "imports": { @@ -220,7 +302,7 @@ snapshot[`write - import_map_referred/import_map.json 1`] = ` ] `; -snapshot[`associateByFile - import_map_referred/mod.ts 1`] = ` +snapshot[`write - import_map_referred/mod.ts 1`] = ` [ { dependencies: [ @@ -261,7 +343,7 @@ snapshot[`associateByFile - import_map_referred/mod.ts 1`] = ` ] `; -snapshot[`write - import_map_referred/mod.ts 1`] = ` +snapshot[`write - import_map_referred/mod.ts 2`] = ` [ '{ "imports": { @@ -272,7 +354,7 @@ snapshot[`write - import_map_referred/mod.ts 1`] = ` ] `; -snapshot[`associateByFile - updated_import_and_outdated_export.ts 1`] = ` +snapshot[`write - updated_import_and_outdated_export.ts 1`] = ` [ { dependencies: [ @@ -301,7 +383,7 @@ snapshot[`associateByFile - updated_import_and_outdated_export.ts 1`] = ` name: "deno.land/std", path: "/assert/assert_equals.ts", protocol: "https:", - version: "123.456.789", + version: "0.218.0", }, }, ], @@ -310,15 +392,15 @@ snapshot[`associateByFile - updated_import_and_outdated_export.ts 1`] = ` ] `; -snapshot[`write - updated_import_and_outdated_export.ts 1`] = ` +snapshot[`write - updated_import_and_outdated_export.ts 2`] = ` [ - 'import { assert } from "https://deno.land/std@123.456.789/assert/assert.ts"; -export { assertEquals } from "https://deno.land/std@123.456.789/assert/assert_equals.ts"; + 'import { assert } from "https://deno.land/std@0.218.0/assert/assert.ts"; +export { assertEquals } from "https://deno.land/std@0.218.0/assert/assert_equals.ts"; ', ] `; -snapshot[`associateByFile - import_map/deno.json 1`] = ` +snapshot[`write - import_map/deno.json 1`] = ` [ { dependencies: [ @@ -363,7 +445,7 @@ snapshot[`associateByFile - import_map/deno.json 1`] = ` name: "deno.land/std", path: "/", protocol: "https:", - version: "123.456.789", + version: "0.218.0", }, }, { @@ -391,17 +473,17 @@ snapshot[`associateByFile - import_map/deno.json 1`] = ` { code: { span: undefined, - specifier: "npm:node-emoji@1.0.0", + specifier: "npm:node-emoji@2.0.0", }, from: { name: "node-emoji", path: "", protocol: "npm:", - version: "1.0.0", + version: "2.0.0", }, map: { key: "node-emoji", - resolved: "npm:node-emoji@1.0.0", + resolved: "npm:node-emoji@2.0.0", }, to: { name: "node-emoji", @@ -416,11 +498,11 @@ snapshot[`associateByFile - import_map/deno.json 1`] = ` ] `; -snapshot[`write - import_map/deno.json 1`] = ` +snapshot[`write - import_map/deno.json 2`] = ` [ '{ "imports": { - "std/": "https://deno.land/std@123.456.789/", + "std/": "https://deno.land/std@0.218.0/", "deno_graph": "https://deno.land/x/deno_graph@123.456.789/mod.ts", "node-emoji": "npm:node-emoji@123.456.789", "flag": "jsr:@luca/flag@123.456.789", @@ -431,7 +513,7 @@ snapshot[`write - import_map/deno.json 1`] = ` ] `; -snapshot[`associateByFile - import_map/mod.ts 1`] = ` +snapshot[`write - import_map/mod.ts 1`] = ` [ { dependencies: [ @@ -494,7 +576,7 @@ snapshot[`associateByFile - import_map/mod.ts 1`] = ` name: "deno.land/std", path: "/", protocol: "https:", - version: "123.456.789", + version: "0.218.0", }, }, { @@ -546,11 +628,11 @@ snapshot[`associateByFile - import_map/mod.ts 1`] = ` name: "node-emoji", path: "", protocol: "npm:", - version: "1.0.0", + version: "2.0.0", }, map: { key: "node-emoji", - resolved: "npm:node-emoji@1.0.0", + resolved: "npm:node-emoji@2.0.0", }, to: { name: "node-emoji", @@ -565,11 +647,11 @@ snapshot[`associateByFile - import_map/mod.ts 1`] = ` ] `; -snapshot[`write - import_map/mod.ts 1`] = ` +snapshot[`write - import_map/mod.ts 2`] = ` [ '{ "imports": { - "std/": "https://deno.land/std@123.456.789/", + "std/": "https://deno.land/std@0.218.0/", "deno_graph": "https://deno.land/x/deno_graph@123.456.789/mod.ts", "node-emoji": "npm:node-emoji@123.456.789", "flag": "jsr:@luca/flag@123.456.789", @@ -580,7 +662,7 @@ snapshot[`write - import_map/mod.ts 1`] = ` ] `; -snapshot[`associateByFile - npm.ts 1`] = ` +snapshot[`write - npm.ts 1`] = ` [ { dependencies: [ @@ -618,7 +700,7 @@ snapshot[`associateByFile - npm.ts 1`] = ` ] `; -snapshot[`write - npm.ts 1`] = ` +snapshot[`write - npm.ts 2`] = ` [ 'import emoji from "npm:node-emoji@123.456.789"; import chalk from "npm:chalk@5"; @@ -627,7 +709,7 @@ import express from "npm:express@^4.17"; ] `; -snapshot[`associateByFile - import_and_export.ts 1`] = ` +snapshot[`write - import_and_export.ts 1`] = ` [ { dependencies: [ @@ -656,7 +738,7 @@ snapshot[`associateByFile - import_and_export.ts 1`] = ` name: "deno.land/std", path: "/assert/assert.ts", protocol: "https:", - version: "123.456.789", + version: "0.218.0", }, }, { @@ -693,19 +775,19 @@ snapshot[`associateByFile - import_and_export.ts 1`] = ` ] `; -snapshot[`write - import_and_export.ts 1`] = ` +snapshot[`write - import_and_export.ts 2`] = ` [ - 'import { assert } from "https://deno.land/std@123.456.789/assert/assert.ts"; + 'import { assert } from "https://deno.land/std@0.218.0/assert/assert.ts"; export { createGraph } from "https://deno.land/x/deno_graph@123.456.789/mod.ts"; ', ] `; -snapshot[`associateByFile - import_map_no_resolve/deno.json 1`] = `[]`; - snapshot[`write - import_map_no_resolve/deno.json 1`] = `[]`; -snapshot[`associateByFile - import_map_no_resolve/mod.ts 1`] = ` +snapshot[`write - import_map_no_resolve/deno.json 2`] = `[]`; + +snapshot[`write - import_map_no_resolve/mod.ts 1`] = ` [ { dependencies: [ @@ -734,7 +816,7 @@ snapshot[`associateByFile - import_map_no_resolve/mod.ts 1`] = ` name: "deno.land/std", path: "/assert/assert.ts", protocol: "https:", - version: "123.456.789", + version: "0.218.0", }, }, ], @@ -743,14 +825,14 @@ snapshot[`associateByFile - import_map_no_resolve/mod.ts 1`] = ` ] `; -snapshot[`write - import_map_no_resolve/mod.ts 1`] = ` +snapshot[`write - import_map_no_resolve/mod.ts 2`] = ` [ - 'import { assert } from "https://deno.land/std@123.456.789/assert/assert.ts"; + 'import { assert } from "https://deno.land/std@0.218.0/assert/assert.ts"; ', ] `; -snapshot[`associateByFile - relative_import/mod.ts 1`] = ` +snapshot[`write - relative_import/mod.ts 1`] = ` [ { dependencies: [ @@ -779,7 +861,7 @@ snapshot[`associateByFile - relative_import/mod.ts 1`] = ` name: "deno.land/std", path: "/assert/assert.ts", protocol: "https:", - version: "123.456.789", + version: "0.218.0", }, }, ], @@ -788,14 +870,311 @@ snapshot[`associateByFile - relative_import/mod.ts 1`] = ` ] `; -snapshot[`write - relative_import/mod.ts 1`] = ` +snapshot[`write - relative_import/mod.ts 2`] = ` [ - 'export { assert } from "https://deno.land/std@123.456.789/assert/assert.ts"; + 'export { assert } from "https://deno.land/std@0.218.0/assert/assert.ts"; ', ] `; -snapshot[`associateByFile - jsonc/deno.jsonc 1`] = ` +snapshot[`write - lockfile/deno.json 1`] = ` +[ + { + dependencies: [ + { + code: { + span: undefined, + specifier: "https://deno.land/std@0.200.0/bytes/copy.ts", + }, + from: { + name: "deno.land/std", + path: "/bytes/copy.ts", + protocol: "https:", + version: "0.200.0", + }, + map: { + key: "std/bytes", + resolved: "https://deno.land/std@0.200.0/bytes/copy.ts", + }, + to: { + name: "deno.land/std", + path: "/bytes/copy.ts", + protocol: "https:", + version: "0.218.0", + }, + }, + ], + kind: "import_map", + }, + { + dependencies: [ + { + code: { + span: undefined, + specifier: "jsr:@core/match@0.1.x", + }, + from: { + name: "@core/match", + path: "", + protocol: "jsr:", + version: "0.1.0", + }, + map: undefined, + to: { + name: "@core/match", + path: "", + protocol: "jsr:", + version: "0.1.9", + }, + }, + { + code: { + span: undefined, + specifier: "https://deno.land/std@0.200.0/bytes/copy.ts", + }, + from: { + name: "deno.land/std", + path: "/bytes/copy.ts", + protocol: "https:", + version: "0.200.0", + }, + map: undefined, + to: { + name: "deno.land/std", + path: "/bytes/copy.ts", + protocol: "https:", + version: "0.218.0", + }, + }, + { + code: { + span: undefined, + specifier: "npm:hono@^3", + }, + from: { + name: "hono", + path: "", + protocol: "npm:", + version: "3.0.0", + }, + map: undefined, + to: { + name: "hono", + path: "", + protocol: "npm:", + version: "3.12.12", + }, + }, + ], + kind: "lockfile", + }, +] +`; + +snapshot[`write - lockfile/deno.json 2`] = ` +[ + '{ + "imports": { + "std/bytes": "https://deno.land/std@0.218.0/bytes/copy.ts", + "hono": "npm:hono@^3", + "@core/match": "jsr:@core/match@0.1.x" + } +} +', + '{ + "version": "3", + "packages": { + "specifiers": { + "jsr:@core/match@0.1.x": "jsr:@core/match@0.1.9", + "npm:hono@^3": "npm:hono@3.12.12", + "npm:ts-toolbelt@9.6.0": "npm:ts-toolbelt@9.6.0" + }, + "jsr": { + "@core/match@0.1.9": { + "dependencies": [ + "npm:ts-toolbelt@9.6.0" + ] + } + }, + "npm": { + "hono@3.12.12": { + "integrity": "sha512-5IAMJOXfpA5nT+K0MNjClchzz0IhBHs2Szl7WFAhrFOsbtQsYmNynFyJRg/a3IPsmCfxcrf8txUGiNShXpK5Rg==", + "dependencies": {} + }, + "ts-toolbelt@9.6.0": { + "integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==", + "dependencies": {} + } + } + }, + "remote": { + "https://deno.land/std@0.218.0/bytes/copy.ts": "f29c03168853720dfe82eaa57793d0b9e3543ebfe5306684182f0f1e3bfd422a", + "https://jsr.io/@core/match/0.1.9/mod.ts": "5b9781f50b239d6cdc5cf99208418e0c16f732fbb2b7237454336559ca73537f" + }, + "workspace": { + "dependencies": [ + "jsr:@core/match@0.1.x", + "npm:hono@^3" + ] + } +}', +] +`; + +snapshot[`write - lockfile/mod.ts 1`] = ` +[ + { + dependencies: [ + { + code: { + span: { + end: { + character: 32, + line: 0, + }, + start: { + character: 21, + line: 0, + }, + }, + specifier: "std/bytes", + }, + from: { + name: "deno.land/std", + path: "/bytes/copy.ts", + protocol: "https:", + version: "0.200.0", + }, + map: { + key: "std/bytes", + resolved: "https://deno.land/std@0.200.0/bytes/copy.ts", + }, + to: { + name: "deno.land/std", + path: "/bytes/copy.ts", + protocol: "https:", + version: "0.218.0", + }, + }, + ], + kind: "import_map", + }, + { + dependencies: [ + { + code: { + span: undefined, + specifier: "jsr:@core/match@0.1.x", + }, + from: { + name: "@core/match", + path: "", + protocol: "jsr:", + version: "0.1.0", + }, + map: undefined, + to: { + name: "@core/match", + path: "", + protocol: "jsr:", + version: "0.1.9", + }, + }, + { + code: { + span: undefined, + specifier: "https://deno.land/std@0.200.0/bytes/copy.ts", + }, + from: { + name: "deno.land/std", + path: "/bytes/copy.ts", + protocol: "https:", + version: "0.200.0", + }, + map: undefined, + to: { + name: "deno.land/std", + path: "/bytes/copy.ts", + protocol: "https:", + version: "0.218.0", + }, + }, + { + code: { + span: undefined, + specifier: "npm:hono@^3", + }, + from: { + name: "hono", + path: "", + protocol: "npm:", + version: "3.0.0", + }, + map: undefined, + to: { + name: "hono", + path: "", + protocol: "npm:", + version: "3.12.12", + }, + }, + ], + kind: "lockfile", + }, +] +`; + +snapshot[`write - lockfile/mod.ts 2`] = ` +[ + '{ + "imports": { + "std/bytes": "https://deno.land/std@0.218.0/bytes/copy.ts", + "hono": "npm:hono@^3", + "@core/match": "jsr:@core/match@0.1.x" + } +} +', + '{ + "version": "3", + "packages": { + "specifiers": { + "jsr:@core/match@0.1.x": "jsr:@core/match@0.1.9", + "npm:hono@^3": "npm:hono@3.12.12", + "npm:ts-toolbelt@9.6.0": "npm:ts-toolbelt@9.6.0" + }, + "jsr": { + "@core/match@0.1.9": { + "dependencies": [ + "npm:ts-toolbelt@9.6.0" + ] + } + }, + "npm": { + "hono@3.12.12": { + "integrity": "sha512-5IAMJOXfpA5nT+K0MNjClchzz0IhBHs2Szl7WFAhrFOsbtQsYmNynFyJRg/a3IPsmCfxcrf8txUGiNShXpK5Rg==", + "dependencies": {} + }, + "ts-toolbelt@9.6.0": { + "integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==", + "dependencies": {} + } + } + }, + "remote": { + "https://deno.land/std@0.218.0/bytes/copy.ts": "f29c03168853720dfe82eaa57793d0b9e3543ebfe5306684182f0f1e3bfd422a", + "https://jsr.io/@core/match/0.1.9/mod.ts": "5b9781f50b239d6cdc5cf99208418e0c16f732fbb2b7237454336559ca73537f" + }, + "workspace": { + "dependencies": [ + "jsr:@core/match@0.1.x", + "npm:hono@^3" + ] + } +}', +] +`; + +snapshot[`write - jsonc/deno.jsonc 1`] = ` [ { dependencies: [ @@ -840,7 +1219,7 @@ snapshot[`associateByFile - jsonc/deno.jsonc 1`] = ` name: "deno.land/std", path: "/", protocol: "https:", - version: "123.456.789", + version: "0.218.0", }, }, { @@ -868,17 +1247,17 @@ snapshot[`associateByFile - jsonc/deno.jsonc 1`] = ` { code: { span: undefined, - specifier: "npm:node-emoji@1.0.0", + specifier: "npm:node-emoji@2.0.0", }, from: { name: "node-emoji", path: "", protocol: "npm:", - version: "1.0.0", + version: "2.0.0", }, map: { key: "node-emoji", - resolved: "npm:node-emoji@1.0.0", + resolved: "npm:node-emoji@2.0.0", }, to: { name: "node-emoji", @@ -893,11 +1272,11 @@ snapshot[`associateByFile - jsonc/deno.jsonc 1`] = ` ] `; -snapshot[`write - jsonc/deno.jsonc 1`] = ` +snapshot[`write - jsonc/deno.jsonc 2`] = ` [ '{ "imports": { - "std/": "https://deno.land/std@123.456.789/", + "std/": "https://deno.land/std@0.218.0/", "deno_graph": "https://deno.land/x/deno_graph@123.456.789/mod.ts", "node-emoji": "npm:node-emoji@123.456.789", "flag": "jsr:@luca/flag@123.456.789", @@ -909,7 +1288,7 @@ snapshot[`write - jsonc/deno.jsonc 1`] = ` ] `; -snapshot[`associateByFile - updated_and_outdated.ts 1`] = ` +snapshot[`write - updated_and_outdated.ts 1`] = ` [ { dependencies: [ @@ -938,7 +1317,7 @@ snapshot[`associateByFile - updated_and_outdated.ts 1`] = ` name: "deno.land/std", path: "/assert/assert_equals.ts", protocol: "https:", - version: "123.456.789", + version: "0.218.0", }, }, ], @@ -947,15 +1326,15 @@ snapshot[`associateByFile - updated_and_outdated.ts 1`] = ` ] `; -snapshot[`write - updated_and_outdated.ts 1`] = ` +snapshot[`write - updated_and_outdated.ts 2`] = ` [ - 'import { assert } from "https://deno.land/std@123.456.789/assert/assert.ts"; -import { assertEquals } from "https://deno.land/std@123.456.789/assert/assert_equals.ts"; + 'import { assert } from "https://deno.land/std@0.218.0/assert/assert.ts"; +import { assertEquals } from "https://deno.land/std@0.218.0/assert/assert_equals.ts"; ', ] `; -snapshot[`associateByFile - multiple_modules/mod.ts 1`] = ` +snapshot[`write - multiple_modules/mod.ts 1`] = ` [ { dependencies: [ @@ -984,7 +1363,7 @@ snapshot[`associateByFile - multiple_modules/mod.ts 1`] = ` name: "deno.land/std", path: "/assert/assert_equals.ts", protocol: "https:", - version: "123.456.789", + version: "0.218.0", }, }, ], @@ -1017,7 +1396,7 @@ snapshot[`associateByFile - multiple_modules/mod.ts 1`] = ` name: "deno.land/std", path: "/assert/assert.ts", protocol: "https:", - version: "123.456.789", + version: "0.218.0", }, }, { @@ -1082,12 +1461,12 @@ snapshot[`associateByFile - multiple_modules/mod.ts 1`] = ` ] `; -snapshot[`write - multiple_modules/mod.ts 1`] = ` +snapshot[`write - multiple_modules/mod.ts 2`] = ` [ - 'import { assertEquals } from "https://deno.land/std@123.456.789/assert/assert_equals.ts"; + 'import { assertEquals } from "https://deno.land/std@0.218.0/assert/assert_equals.ts"; export const noop = () => {}; ', - 'import { assert } from "https://deno.land/std@123.456.789/assert/assert.ts"; + 'import { assert } from "https://deno.land/std@0.218.0/assert/assert.ts"; import { createGraph } from "https://deno.land/x/deno_graph@123.456.789/mod.ts"; import emoji from "npm:node-emoji@123.456.789"; import { noop } from "./lib.ts"; @@ -1095,7 +1474,7 @@ import { noop } from "./lib.ts"; ] `; -snapshot[`associateByFile - import.ts 1`] = ` +snapshot[`write - import.ts 1`] = ` [ { dependencies: [ @@ -1124,7 +1503,7 @@ snapshot[`associateByFile - import.ts 1`] = ` name: "deno.land/std", path: "/assert/assert.ts", protocol: "https:", - version: "123.456.789", + version: "0.218.0", }, }, ], @@ -1133,14 +1512,14 @@ snapshot[`associateByFile - import.ts 1`] = ` ] `; -snapshot[`write - import.ts 1`] = ` +snapshot[`write - import.ts 2`] = ` [ - 'import { assert } from "https://deno.land/std@123.456.789/assert/assert.ts"; + 'import { assert } from "https://deno.land/std@0.218.0/assert/assert.ts"; ', ] `; -snapshot[`associateByFile - unversioned.ts 1`] = ` +snapshot[`write - unversioned.ts 1`] = ` [ { dependencies: [ @@ -1169,7 +1548,7 @@ snapshot[`associateByFile - unversioned.ts 1`] = ` name: "deno.land/std", path: "/assert/assert.ts", protocol: "https:", - version: "123.456.789", + version: "0.218.0", }, }, ], @@ -1178,14 +1557,14 @@ snapshot[`associateByFile - unversioned.ts 1`] = ` ] `; -snapshot[`write - unversioned.ts 1`] = ` +snapshot[`write - unversioned.ts 2`] = ` [ - 'import { assert } from "https://deno.land/std@123.456.789/assert/assert.ts"; + 'import { assert } from "https://deno.land/std@0.218.0/assert/assert.ts"; ', ] `; -snapshot[`associateByFile - jsr.ts 1`] = ` +snapshot[`write - jsr.ts 1`] = ` [ { dependencies: [ @@ -1223,7 +1602,7 @@ snapshot[`associateByFile - jsr.ts 1`] = ` ] `; -snapshot[`write - jsr.ts 1`] = ` +snapshot[`write - jsr.ts 2`] = ` [ 'import { assert } from "jsr:@std/assert@123.456.789"; import { parse } from "jsr:@std/semver@^0.210.0"; diff --git a/test/snapshots/update_test.ts.snap b/test/snapshots/update_test.ts.snap index 3bd6094b..6e229103 100644 --- a/test/snapshots/update_test.ts.snap +++ b/test/snapshots/update_test.ts.snap @@ -1,910 +1,1245 @@ export const snapshot = {}; snapshot[`collect - export.ts 1`] = ` -{ - code: { - span: { - end: { - character: 71, - line: 0, +[ + { + code: { + span: { + end: { + character: 71, + line: 0, + }, + start: { + character: 23, + line: 0, + }, }, - start: { - character: 23, - line: 0, + specifier: "https://deno.land/std@0.200.0/assert/assert.ts", + }, + from: { + name: "deno.land/std", + path: "/assert/assert.ts", + protocol: "https:", + version: "0.200.0", + }, + to: { + name: "deno.land/std", + path: "/assert/assert.ts", + protocol: "https:", + version: "0.218.0", + }, + }, +] +`; + +snapshot[`collect - lockfile_not_importable - mod.ts 1`] = ` +[ + { + code: { + span: { + end: { + character: 23, + line: 0, + }, + start: { + character: 7, + line: 0, + }, }, + specifier: "std/version.ts", + }, + from: { + name: "deno.land/std", + path: "/", + protocol: "https:", + version: "0.200.0", + }, + to: { + name: "deno.land/std", + path: "/", + protocol: "https:", + version: "0.218.0", }, - specifier: "https://deno.land/std@0.200.0/assert/assert.ts", }, - from: { - name: "deno.land/std", - path: "/assert/assert.ts", - protocol: "https:", - version: "0.200.0", + { + code: { + span: undefined, + specifier: "https://deno.land/std@0.200.0/version.ts", + }, + from: { + name: "deno.land/std", + path: "/", + protocol: "https:", + version: "0.200.0", + }, + to: { + name: "deno.land/std", + path: "/", + protocol: "https:", + version: "0.218.0", + }, }, - to: { - name: "deno.land/std", - path: "/assert/assert.ts", - protocol: "https:", - version: "123.456.789", +] +`; + +snapshot[`collect - lockfile_not_importable - mod.ts 2`] = ` +[ + { + data: { + remote: { + "https://deno.land/std@0.200.0/version.ts": "ae3fc7003d763c0d78f76fad04df179529d0aef813a247b4a3909e22adf6bfd1", + }, + version: "3", + }, + specifier: "https://deno.land/std@0.200.0/version.ts", }, -} +] `; snapshot[`collect - multiple_imports.ts 1`] = ` -{ - code: { - span: { - end: { - character: 52, - line: 3, +[ + { + code: { + span: { + end: { + character: 52, + line: 3, + }, + start: { + character: 30, + line: 3, + }, }, - start: { - character: 30, - line: 3, + specifier: "jsr:@luca/flag@1.0.0", + }, + from: { + name: "@luca/flag", + path: "", + protocol: "jsr:", + version: "1.0.0", + }, + to: { + name: "@luca/flag", + path: "", + protocol: "jsr:", + version: "123.456.789", + }, + }, + { + code: { + span: { + end: { + character: 71, + line: 0, + }, + start: { + character: 23, + line: 0, + }, }, + specifier: "https://deno.land/std@0.200.0/assert/assert.ts", + }, + from: { + name: "deno.land/std", + path: "/assert/assert.ts", + protocol: "https:", + version: "0.200.0", + }, + to: { + name: "deno.land/std", + path: "/assert/assert.ts", + protocol: "https:", + version: "0.218.0", }, - specifier: "jsr:@luca/flag@1.0.0", }, - from: { - name: "@luca/flag", - path: "", - protocol: "jsr:", - version: "1.0.0", + { + code: { + span: { + end: { + character: 74, + line: 1, + }, + start: { + character: 28, + line: 1, + }, + }, + specifier: "https://deno.land/x/deno_graph@0.50.0/mod.ts", + }, + from: { + name: "deno.land/x/deno_graph", + path: "/mod.ts", + protocol: "https:", + version: "0.50.0", + }, + to: { + name: "deno.land/x/deno_graph", + path: "/mod.ts", + protocol: "https:", + version: "123.456.789", + }, }, - to: { - name: "@luca/flag", - path: "", - protocol: "jsr:", - version: "123.456.789", + { + code: { + span: { + end: { + character: 40, + line: 2, + }, + start: { + character: 18, + line: 2, + }, + }, + specifier: "npm:node-emoji@2.0.0", + }, + from: { + name: "node-emoji", + path: "", + protocol: "npm:", + version: "2.0.0", + }, + to: { + name: "node-emoji", + path: "", + protocol: "npm:", + version: "123.456.789", + }, }, -} -`; - -snapshot[`collect - multiple_imports.ts 2`] = ` -{ - code: { - span: { - end: { - character: 71, - line: 0, - }, - start: { - character: 23, - line: 0, - }, - }, - specifier: "https://deno.land/std@0.200.0/assert/assert.ts", - }, - from: { - name: "deno.land/std", - path: "/assert/assert.ts", - protocol: "https:", - version: "0.200.0", - }, - to: { - name: "deno.land/std", - path: "/assert/assert.ts", - protocol: "https:", - version: "123.456.789", - }, -} -`; - -snapshot[`collect - multiple_imports.ts 3`] = ` -{ - code: { - span: { - end: { - character: 74, - line: 1, - }, - start: { - character: 28, - line: 1, - }, - }, - specifier: "https://deno.land/x/deno_graph@0.50.0/mod.ts", - }, - from: { - name: "deno.land/x/deno_graph", - path: "/mod.ts", - protocol: "https:", - version: "0.50.0", - }, - to: { - name: "deno.land/x/deno_graph", - path: "/mod.ts", - protocol: "https:", - version: "123.456.789", - }, -} -`; - -snapshot[`collect - multiple_imports.ts 4`] = ` -{ - code: { - span: { - end: { - character: 40, - line: 2, - }, - start: { - character: 18, - line: 2, - }, - }, - specifier: "npm:node-emoji@2.0.0", - }, - from: { - name: "node-emoji", - path: "", - protocol: "npm:", - version: "2.0.0", - }, - to: { - name: "node-emoji", - path: "", - protocol: "npm:", - version: "123.456.789", - }, -} +] `; snapshot[`collect - import_map_referred - import_map.json 1`] = ` -{ - code: { - span: undefined, - specifier: "https://deno.land/x/dax@0.17.0/mod.ts", - }, - from: { - name: "deno.land/x/dax", - path: "/mod.ts", - protocol: "https:", - version: "0.17.0", - }, - to: { - name: "deno.land/x/dax", - path: "/mod.ts", - protocol: "https:", - version: "123.456.789", - }, -} +[ + { + code: { + span: undefined, + specifier: "https://deno.land/x/dax@0.17.0/mod.ts", + }, + from: { + name: "deno.land/x/dax", + path: "/mod.ts", + protocol: "https:", + version: "0.17.0", + }, + to: { + name: "deno.land/x/dax", + path: "/mod.ts", + protocol: "https:", + version: "123.456.789", + }, + }, +] `; snapshot[`collect - import_map_referred - mod.ts 1`] = ` -{ - code: { - span: { - end: { - character: 19, - line: 0, - }, - start: { - character: 14, - line: 0, +[ + { + code: { + span: { + end: { + character: 19, + line: 0, + }, + start: { + character: 14, + line: 0, + }, }, + specifier: "dax", + }, + from: { + name: "deno.land/x/dax", + path: "/mod.ts", + protocol: "https:", + version: "0.17.0", + }, + to: { + name: "deno.land/x/dax", + path: "/mod.ts", + protocol: "https:", + version: "123.456.789", }, - specifier: "dax", - }, - from: { - name: "deno.land/x/dax", - path: "/mod.ts", - protocol: "https:", - version: "0.17.0", - }, - to: { - name: "deno.land/x/dax", - path: "/mod.ts", - protocol: "https:", - version: "123.456.789", }, -} +] `; snapshot[`collect - updated_import_and_outdated_export.ts 1`] = ` -{ - code: { - span: { - end: { - character: 84, - line: 1, - }, - start: { - character: 29, - line: 1, +[ + { + code: { + span: { + end: { + character: 84, + line: 1, + }, + start: { + character: 29, + line: 1, + }, }, + specifier: "https://deno.land/std@0.200.0/assert/assert_equals.ts", + }, + from: { + name: "deno.land/std", + path: "/assert/assert_equals.ts", + protocol: "https:", + version: "0.200.0", + }, + to: { + name: "deno.land/std", + path: "/assert/assert_equals.ts", + protocol: "https:", + version: "0.218.0", }, - specifier: "https://deno.land/std@0.200.0/assert/assert_equals.ts", - }, - from: { - name: "deno.land/std", - path: "/assert/assert_equals.ts", - protocol: "https:", - version: "0.200.0", - }, - to: { - name: "deno.land/std", - path: "/assert/assert_equals.ts", - protocol: "https:", - version: "123.456.789", }, -} +] `; snapshot[`collect - import_map - deno.json 1`] = ` -{ - code: { - span: undefined, - specifier: "jsr:@luca/flag@1.0.0", - }, - from: { - name: "@luca/flag", - path: "", - protocol: "jsr:", - version: "1.0.0", - }, - to: { - name: "@luca/flag", - path: "", - protocol: "jsr:", - version: "123.456.789", - }, -} -`; - -snapshot[`collect - import_map - deno.json 2`] = ` -{ - code: { - span: undefined, - specifier: "https://deno.land/std@0.200.0/", - }, - from: { - name: "deno.land/std", - path: "/", - protocol: "https:", - version: "0.200.0", - }, - to: { - name: "deno.land/std", - path: "/", - protocol: "https:", - version: "123.456.789", - }, -} -`; - -snapshot[`collect - import_map - deno.json 3`] = ` -{ - code: { - span: undefined, - specifier: "https://deno.land/x/deno_graph@0.50.0/mod.ts", - }, - from: { - name: "deno.land/x/deno_graph", - path: "/mod.ts", - protocol: "https:", - version: "0.50.0", - }, - to: { - name: "deno.land/x/deno_graph", - path: "/mod.ts", - protocol: "https:", - version: "123.456.789", - }, -} -`; - -snapshot[`collect - import_map - deno.json 4`] = ` -{ - code: { - span: undefined, - specifier: "npm:node-emoji@1.0.0", - }, - from: { - name: "node-emoji", - path: "", - protocol: "npm:", - version: "1.0.0", - }, - to: { - name: "node-emoji", - path: "", - protocol: "npm:", - version: "123.456.789", - }, -} +[ + { + code: { + span: undefined, + specifier: "jsr:@luca/flag@1.0.0", + }, + from: { + name: "@luca/flag", + path: "", + protocol: "jsr:", + version: "1.0.0", + }, + to: { + name: "@luca/flag", + path: "", + protocol: "jsr:", + version: "123.456.789", + }, + }, + { + code: { + span: undefined, + specifier: "https://deno.land/std@0.200.0/", + }, + from: { + name: "deno.land/std", + path: "/", + protocol: "https:", + version: "0.200.0", + }, + to: { + name: "deno.land/std", + path: "/", + protocol: "https:", + version: "0.218.0", + }, + }, + { + code: { + span: undefined, + specifier: "https://deno.land/x/deno_graph@0.50.0/mod.ts", + }, + from: { + name: "deno.land/x/deno_graph", + path: "/mod.ts", + protocol: "https:", + version: "0.50.0", + }, + to: { + name: "deno.land/x/deno_graph", + path: "/mod.ts", + protocol: "https:", + version: "123.456.789", + }, + }, + { + code: { + span: undefined, + specifier: "npm:node-emoji@2.0.0", + }, + from: { + name: "node-emoji", + path: "", + protocol: "npm:", + version: "2.0.0", + }, + to: { + name: "node-emoji", + path: "", + protocol: "npm:", + version: "123.456.789", + }, + }, +] `; snapshot[`collect - import_map - mod.ts 1`] = ` -{ - code: { - span: { - end: { - character: 36, - line: 3, +[ + { + code: { + span: { + end: { + character: 36, + line: 3, + }, + start: { + character: 30, + line: 3, + }, }, - start: { - character: 30, - line: 3, + specifier: "flag", + }, + from: { + name: "@luca/flag", + path: "", + protocol: "jsr:", + version: "1.0.0", + }, + to: { + name: "@luca/flag", + path: "", + protocol: "jsr:", + version: "123.456.789", + }, + }, + { + code: { + span: { + end: { + character: 38, + line: 0, + }, + start: { + character: 23, + line: 0, + }, }, + specifier: "std/assert.ts", + }, + from: { + name: "deno.land/std", + path: "/", + protocol: "https:", + version: "0.200.0", + }, + to: { + name: "deno.land/std", + path: "/", + protocol: "https:", + version: "0.218.0", }, - specifier: "flag", }, - from: { - name: "@luca/flag", - path: "", - protocol: "jsr:", - version: "1.0.0", + { + code: { + span: { + end: { + character: 40, + line: 1, + }, + start: { + character: 28, + line: 1, + }, + }, + specifier: "deno_graph", + }, + from: { + name: "deno.land/x/deno_graph", + path: "/mod.ts", + protocol: "https:", + version: "0.50.0", + }, + to: { + name: "deno.land/x/deno_graph", + path: "/mod.ts", + protocol: "https:", + version: "123.456.789", + }, }, - to: { - name: "@luca/flag", - path: "", - protocol: "jsr:", - version: "123.456.789", + { + code: { + span: { + end: { + character: 36, + line: 2, + }, + start: { + character: 24, + line: 2, + }, + }, + specifier: "node-emoji", + }, + from: { + name: "node-emoji", + path: "", + protocol: "npm:", + version: "2.0.0", + }, + to: { + name: "node-emoji", + path: "", + protocol: "npm:", + version: "123.456.789", + }, }, -} -`; - -snapshot[`collect - import_map - mod.ts 2`] = ` -{ - code: { - span: { - end: { - character: 38, - line: 0, - }, - start: { - character: 23, - line: 0, - }, - }, - specifier: "std/assert.ts", - }, - from: { - name: "deno.land/std", - path: "/", - protocol: "https:", - version: "0.200.0", - }, - to: { - name: "deno.land/std", - path: "/", - protocol: "https:", - version: "123.456.789", - }, -} -`; - -snapshot[`collect - import_map - mod.ts 3`] = ` -{ - code: { - span: { - end: { - character: 40, - line: 1, - }, - start: { - character: 28, - line: 1, - }, - }, - specifier: "deno_graph", - }, - from: { - name: "deno.land/x/deno_graph", - path: "/mod.ts", - protocol: "https:", - version: "0.50.0", - }, - to: { - name: "deno.land/x/deno_graph", - path: "/mod.ts", - protocol: "https:", - version: "123.456.789", - }, -} -`; - -snapshot[`collect - import_map - mod.ts 4`] = ` -{ - code: { - span: { - end: { - character: 36, - line: 2, - }, - start: { - character: 24, - line: 2, - }, - }, - specifier: "node-emoji", - }, - from: { - name: "node-emoji", - path: "", - protocol: "npm:", - version: "1.0.0", - }, - to: { - name: "node-emoji", - path: "", - protocol: "npm:", - version: "123.456.789", - }, -} +] `; snapshot[`collect - npm.ts 1`] = ` -{ - code: { - span: { - end: { - character: 40, - line: 0, - }, - start: { - character: 18, - line: 0, +[ + { + code: { + span: { + end: { + character: 40, + line: 0, + }, + start: { + character: 18, + line: 0, + }, }, + specifier: "npm:node-emoji@2.0.0", + }, + from: { + name: "node-emoji", + path: "", + protocol: "npm:", + version: "2.0.0", + }, + to: { + name: "node-emoji", + path: "", + protocol: "npm:", + version: "123.456.789", }, - specifier: "npm:node-emoji@2.0.0", - }, - from: { - name: "node-emoji", - path: "", - protocol: "npm:", - version: "2.0.0", - }, - to: { - name: "node-emoji", - path: "", - protocol: "npm:", - version: "123.456.789", }, -} +] `; snapshot[`collect - import_and_export.ts 1`] = ` -{ - code: { - span: { - end: { - character: 71, - line: 0, - }, - start: { - character: 23, - line: 0, +[ + { + code: { + span: { + end: { + character: 71, + line: 0, + }, + start: { + character: 23, + line: 0, + }, }, + specifier: "https://deno.land/std@0.200.0/assert/assert.ts", + }, + from: { + name: "deno.land/std", + path: "/assert/assert.ts", + protocol: "https:", + version: "0.200.0", + }, + to: { + name: "deno.land/std", + path: "/assert/assert.ts", + protocol: "https:", + version: "0.218.0", }, - specifier: "https://deno.land/std@0.200.0/assert/assert.ts", - }, - from: { - name: "deno.land/std", - path: "/assert/assert.ts", - protocol: "https:", - version: "0.200.0", }, - to: { - name: "deno.land/std", - path: "/assert/assert.ts", - protocol: "https:", - version: "123.456.789", + { + code: { + span: { + end: { + character: 74, + line: 1, + }, + start: { + character: 28, + line: 1, + }, + }, + specifier: "https://deno.land/x/deno_graph@0.50.0/mod.ts", + }, + from: { + name: "deno.land/x/deno_graph", + path: "/mod.ts", + protocol: "https:", + version: "0.50.0", + }, + to: { + name: "deno.land/x/deno_graph", + path: "/mod.ts", + protocol: "https:", + version: "123.456.789", + }, }, -} +] `; -snapshot[`collect - import_and_export.ts 2`] = ` -{ - code: { - span: { - end: { - character: 74, - line: 1, - }, - start: { - character: 28, - line: 1, - }, - }, - specifier: "https://deno.land/x/deno_graph@0.50.0/mod.ts", - }, - from: { - name: "deno.land/x/deno_graph", - path: "/mod.ts", - protocol: "https:", - version: "0.50.0", - }, - to: { - name: "deno.land/x/deno_graph", - path: "/mod.ts", - protocol: "https:", - version: "123.456.789", - }, -} -`; +snapshot[`collect - import_map_no_resolve - deno.json 1`] = `[]`; snapshot[`collect - import_map_no_resolve - mod.ts 1`] = ` -{ - code: { - span: { - end: { - character: 71, - line: 0, - }, - start: { - character: 23, - line: 0, +[ + { + code: { + span: { + end: { + character: 71, + line: 0, + }, + start: { + character: 23, + line: 0, + }, }, + specifier: "https://deno.land/std@0.200.0/assert/assert.ts", + }, + from: { + name: "deno.land/std", + path: "/assert/assert.ts", + protocol: "https:", + version: "0.200.0", + }, + to: { + name: "deno.land/std", + path: "/assert/assert.ts", + protocol: "https:", + version: "0.218.0", }, - specifier: "https://deno.land/std@0.200.0/assert/assert.ts", - }, - from: { - name: "deno.land/std", - path: "/assert/assert.ts", - protocol: "https:", - version: "0.200.0", - }, - to: { - name: "deno.land/std", - path: "/assert/assert.ts", - protocol: "https:", - version: "123.456.789", }, -} +] `; snapshot[`collect - relative_import - mod.ts 1`] = ` -{ - code: { - span: { - end: { - character: 71, - line: 0, - }, - start: { - character: 23, - line: 0, +[ + { + code: { + span: { + end: { + character: 71, + line: 0, + }, + start: { + character: 23, + line: 0, + }, }, + specifier: "https://deno.land/std@0.200.0/assert/assert.ts", + }, + from: { + name: "deno.land/std", + path: "/assert/assert.ts", + protocol: "https:", + version: "0.200.0", + }, + to: { + name: "deno.land/std", + path: "/assert/assert.ts", + protocol: "https:", + version: "0.218.0", + }, + }, +] +`; + +snapshot[`collect - lockfile - deno.json 1`] = ` +[ + { + code: { + span: undefined, + specifier: "jsr:@core/match@0.1.x", + }, + from: { + name: "@core/match", + path: "", + protocol: "jsr:", + version: "0.1.0", + }, + to: { + name: "@core/match", + path: "", + protocol: "jsr:", + version: "0.1.9", }, - specifier: "https://deno.land/std@0.200.0/assert/assert.ts", }, - from: { - name: "deno.land/std", - path: "/assert/assert.ts", - protocol: "https:", - version: "0.200.0", + { + code: { + span: undefined, + specifier: "https://deno.land/std@0.200.0/bytes/copy.ts", + }, + from: { + name: "deno.land/std", + path: "/bytes/copy.ts", + protocol: "https:", + version: "0.200.0", + }, + to: { + name: "deno.land/std", + path: "/bytes/copy.ts", + protocol: "https:", + version: "0.218.0", + }, + }, + { + code: { + span: undefined, + specifier: "https://deno.land/std@0.200.0/bytes/copy.ts", + }, + from: { + name: "deno.land/std", + path: "/bytes/copy.ts", + protocol: "https:", + version: "0.200.0", + }, + to: { + name: "deno.land/std", + path: "/bytes/copy.ts", + protocol: "https:", + version: "0.218.0", + }, }, - to: { - name: "deno.land/std", - path: "/assert/assert.ts", - protocol: "https:", - version: "123.456.789", + { + code: { + span: undefined, + specifier: "npm:hono@^3", + }, + from: { + name: "hono", + path: "", + protocol: "npm:", + version: "3.0.0", + }, + to: { + name: "hono", + path: "", + protocol: "npm:", + version: "3.12.12", + }, }, -} +] `; -snapshot[`collect - jsonc - deno.jsonc 1`] = ` -{ - code: { - span: undefined, - specifier: "jsr:@luca/flag@1.0.0", - }, - from: { - name: "@luca/flag", - path: "", - protocol: "jsr:", - version: "1.0.0", - }, - to: { - name: "@luca/flag", - path: "", - protocol: "jsr:", - version: "123.456.789", - }, -} +snapshot[`collect - lockfile - deno.json 2`] = ` +[ + { + data: { + remote: { + "https://deno.land/std@0.200.0/bytes/copy.ts": "939d89e302a9761dcf1d9c937c7711174ed74c59eef40a1e4569a05c9de88219", + }, + version: "3", + }, + specifier: "https://deno.land/std@0.200.0/bytes/copy.ts", + }, + { + data: { + packages: { + npm: { + "hono@3.0.0": { + dependencies: {}, + integrity: "sha512-OBWEvcRydCOLdOiazqOmKy8PXciR8uIyUM9HpmXot7UmwNBGOdEJvTrVRIgjV603w+go6cTXBOvFiQClSABT6Q==", + }, + }, + specifiers: { + "npm:hono@3.0.0": "npm:hono@3.0.0", + }, + }, + remote: {}, + version: "3", + }, + specifier: "npm:hono@^3", + }, + { + data: { + packages: { + jsr: { + "@core/match@0.1.0": { + dependencies: [ + "npm:ts-toolbelt@9.6.0", + ], + }, + }, + npm: { + "ts-toolbelt@9.6.0": { + dependencies: {}, + integrity: "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==", + }, + }, + specifiers: { + "jsr:@core/match@0.1.0": "jsr:@core/match@0.1.0", + "npm:ts-toolbelt@9.6.0": "npm:ts-toolbelt@9.6.0", + }, + }, + remote: { + "https://jsr.io/@core/match/0.1.0/mod.ts": "15840318ac06cca93d260e9e4dd271d07749f6f463ab04bb0d6afdd72709eaf5", + }, + version: "3", + }, + specifier: "jsr:@core/match@0.1.x", + }, +] `; -snapshot[`collect - jsonc - deno.jsonc 2`] = ` -{ - code: { - span: undefined, - specifier: "https://deno.land/std@0.200.0/", - }, - from: { - name: "deno.land/std", - path: "/", - protocol: "https:", - version: "0.200.0", - }, - to: { - name: "deno.land/std", - path: "/", - protocol: "https:", - version: "123.456.789", - }, -} +snapshot[`collect - lockfile - mod.ts 1`] = ` +[ + { + code: { + span: undefined, + specifier: "jsr:@core/match@0.1.x", + }, + from: { + name: "@core/match", + path: "", + protocol: "jsr:", + version: "0.1.0", + }, + to: { + name: "@core/match", + path: "", + protocol: "jsr:", + version: "0.1.9", + }, + }, + { + code: { + span: { + end: { + character: 32, + line: 0, + }, + start: { + character: 21, + line: 0, + }, + }, + specifier: "std/bytes", + }, + from: { + name: "deno.land/std", + path: "/bytes/copy.ts", + protocol: "https:", + version: "0.200.0", + }, + to: { + name: "deno.land/std", + path: "/bytes/copy.ts", + protocol: "https:", + version: "0.218.0", + }, + }, + { + code: { + span: undefined, + specifier: "https://deno.land/std@0.200.0/bytes/copy.ts", + }, + from: { + name: "deno.land/std", + path: "/bytes/copy.ts", + protocol: "https:", + version: "0.200.0", + }, + to: { + name: "deno.land/std", + path: "/bytes/copy.ts", + protocol: "https:", + version: "0.218.0", + }, + }, + { + code: { + span: undefined, + specifier: "npm:hono@^3", + }, + from: { + name: "hono", + path: "", + protocol: "npm:", + version: "3.0.0", + }, + to: { + name: "hono", + path: "", + protocol: "npm:", + version: "3.12.12", + }, + }, +] `; -snapshot[`collect - jsonc - deno.jsonc 3`] = ` -{ - code: { - span: undefined, - specifier: "https://deno.land/x/deno_graph@0.50.0/mod.ts", - }, - from: { - name: "deno.land/x/deno_graph", - path: "/mod.ts", - protocol: "https:", - version: "0.50.0", - }, - to: { - name: "deno.land/x/deno_graph", - path: "/mod.ts", - protocol: "https:", - version: "123.456.789", - }, -} +snapshot[`collect - lockfile - mod.ts 2`] = ` +[ + { + data: { + remote: { + "https://deno.land/std@0.200.0/bytes/copy.ts": "939d89e302a9761dcf1d9c937c7711174ed74c59eef40a1e4569a05c9de88219", + }, + version: "3", + }, + specifier: "https://deno.land/std@0.200.0/bytes/copy.ts", + }, + { + data: { + packages: { + npm: { + "hono@3.0.0": { + dependencies: {}, + integrity: "sha512-OBWEvcRydCOLdOiazqOmKy8PXciR8uIyUM9HpmXot7UmwNBGOdEJvTrVRIgjV603w+go6cTXBOvFiQClSABT6Q==", + }, + }, + specifiers: { + "npm:hono@3.0.0": "npm:hono@3.0.0", + }, + }, + remote: {}, + version: "3", + }, + specifier: "npm:hono@^3", + }, + { + data: { + packages: { + jsr: { + "@core/match@0.1.0": { + dependencies: [ + "npm:ts-toolbelt@9.6.0", + ], + }, + }, + npm: { + "ts-toolbelt@9.6.0": { + dependencies: {}, + integrity: "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==", + }, + }, + specifiers: { + "jsr:@core/match@0.1.0": "jsr:@core/match@0.1.0", + "npm:ts-toolbelt@9.6.0": "npm:ts-toolbelt@9.6.0", + }, + }, + remote: { + "https://jsr.io/@core/match/0.1.0/mod.ts": "15840318ac06cca93d260e9e4dd271d07749f6f463ab04bb0d6afdd72709eaf5", + }, + version: "3", + }, + specifier: "jsr:@core/match@0.1.x", + }, +] `; -snapshot[`collect - jsonc - deno.jsonc 4`] = ` -{ - code: { - span: undefined, - specifier: "npm:node-emoji@1.0.0", - }, - from: { - name: "node-emoji", - path: "", - protocol: "npm:", - version: "1.0.0", - }, - to: { - name: "node-emoji", - path: "", - protocol: "npm:", - version: "123.456.789", - }, -} +snapshot[`collect - jsonc - deno.jsonc 1`] = ` +[ + { + code: { + span: undefined, + specifier: "jsr:@luca/flag@1.0.0", + }, + from: { + name: "@luca/flag", + path: "", + protocol: "jsr:", + version: "1.0.0", + }, + to: { + name: "@luca/flag", + path: "", + protocol: "jsr:", + version: "123.456.789", + }, + }, + { + code: { + span: undefined, + specifier: "https://deno.land/std@0.200.0/", + }, + from: { + name: "deno.land/std", + path: "/", + protocol: "https:", + version: "0.200.0", + }, + to: { + name: "deno.land/std", + path: "/", + protocol: "https:", + version: "0.218.0", + }, + }, + { + code: { + span: undefined, + specifier: "https://deno.land/x/deno_graph@0.50.0/mod.ts", + }, + from: { + name: "deno.land/x/deno_graph", + path: "/mod.ts", + protocol: "https:", + version: "0.50.0", + }, + to: { + name: "deno.land/x/deno_graph", + path: "/mod.ts", + protocol: "https:", + version: "123.456.789", + }, + }, + { + code: { + span: undefined, + specifier: "npm:node-emoji@2.0.0", + }, + from: { + name: "node-emoji", + path: "", + protocol: "npm:", + version: "2.0.0", + }, + to: { + name: "node-emoji", + path: "", + protocol: "npm:", + version: "123.456.789", + }, + }, +] `; snapshot[`collect - updated_and_outdated.ts 1`] = ` -{ - code: { - span: { - end: { - character: 84, - line: 1, - }, - start: { - character: 29, - line: 1, +[ + { + code: { + span: { + end: { + character: 84, + line: 1, + }, + start: { + character: 29, + line: 1, + }, }, + specifier: "https://deno.land/std@0.200.0/assert/assert_equals.ts", + }, + from: { + name: "deno.land/std", + path: "/assert/assert_equals.ts", + protocol: "https:", + version: "0.200.0", + }, + to: { + name: "deno.land/std", + path: "/assert/assert_equals.ts", + protocol: "https:", + version: "0.218.0", }, - specifier: "https://deno.land/std@0.200.0/assert/assert_equals.ts", - }, - from: { - name: "deno.land/std", - path: "/assert/assert_equals.ts", - protocol: "https:", - version: "0.200.0", - }, - to: { - name: "deno.land/std", - path: "/assert/assert_equals.ts", - protocol: "https:", - version: "123.456.789", }, -} +] `; snapshot[`collect - multiple_modules - mod.ts 1`] = ` -{ - code: { - span: { - end: { - character: 84, - line: 0, +[ + { + code: { + span: { + end: { + character: 84, + line: 0, + }, + start: { + character: 29, + line: 0, + }, }, - start: { - character: 29, - line: 0, + specifier: "https://deno.land/std@0.200.0/assert/assert_equals.ts", + }, + from: { + name: "deno.land/std", + path: "/assert/assert_equals.ts", + protocol: "https:", + version: "0.200.0", + }, + to: { + name: "deno.land/std", + path: "/assert/assert_equals.ts", + protocol: "https:", + version: "0.218.0", + }, + }, + { + code: { + span: { + end: { + character: 71, + line: 0, + }, + start: { + character: 23, + line: 0, + }, }, + specifier: "https://deno.land/std@0.200.0/assert/assert.ts", + }, + from: { + name: "deno.land/std", + path: "/assert/assert.ts", + protocol: "https:", + version: "0.200.0", + }, + to: { + name: "deno.land/std", + path: "/assert/assert.ts", + protocol: "https:", + version: "0.218.0", }, - specifier: "https://deno.land/std@0.200.0/assert/assert_equals.ts", }, - from: { - name: "deno.land/std", - path: "/assert/assert_equals.ts", - protocol: "https:", - version: "0.200.0", + { + code: { + span: { + end: { + character: 74, + line: 1, + }, + start: { + character: 28, + line: 1, + }, + }, + specifier: "https://deno.land/x/deno_graph@0.50.0/mod.ts", + }, + from: { + name: "deno.land/x/deno_graph", + path: "/mod.ts", + protocol: "https:", + version: "0.50.0", + }, + to: { + name: "deno.land/x/deno_graph", + path: "/mod.ts", + protocol: "https:", + version: "123.456.789", + }, }, - to: { - name: "deno.land/std", - path: "/assert/assert_equals.ts", - protocol: "https:", - version: "123.456.789", + { + code: { + span: { + end: { + character: 40, + line: 2, + }, + start: { + character: 18, + line: 2, + }, + }, + specifier: "npm:node-emoji@2.0.0", + }, + from: { + name: "node-emoji", + path: "", + protocol: "npm:", + version: "2.0.0", + }, + to: { + name: "node-emoji", + path: "", + protocol: "npm:", + version: "123.456.789", + }, }, -} -`; - -snapshot[`collect - multiple_modules - mod.ts 2`] = ` -{ - code: { - span: { - end: { - character: 71, - line: 0, - }, - start: { - character: 23, - line: 0, - }, - }, - specifier: "https://deno.land/std@0.200.0/assert/assert.ts", - }, - from: { - name: "deno.land/std", - path: "/assert/assert.ts", - protocol: "https:", - version: "0.200.0", - }, - to: { - name: "deno.land/std", - path: "/assert/assert.ts", - protocol: "https:", - version: "123.456.789", - }, -} -`; - -snapshot[`collect - multiple_modules - mod.ts 3`] = ` -{ - code: { - span: { - end: { - character: 74, - line: 1, - }, - start: { - character: 28, - line: 1, - }, - }, - specifier: "https://deno.land/x/deno_graph@0.50.0/mod.ts", - }, - from: { - name: "deno.land/x/deno_graph", - path: "/mod.ts", - protocol: "https:", - version: "0.50.0", - }, - to: { - name: "deno.land/x/deno_graph", - path: "/mod.ts", - protocol: "https:", - version: "123.456.789", - }, -} -`; - -snapshot[`collect - multiple_modules - mod.ts 4`] = ` -{ - code: { - span: { - end: { - character: 40, - line: 2, - }, - start: { - character: 18, - line: 2, - }, - }, - specifier: "npm:node-emoji@2.0.0", - }, - from: { - name: "node-emoji", - path: "", - protocol: "npm:", - version: "2.0.0", - }, - to: { - name: "node-emoji", - path: "", - protocol: "npm:", - version: "123.456.789", - }, -} +] `; snapshot[`collect - import.ts 1`] = ` -{ - code: { - span: { - end: { - character: 71, - line: 0, - }, - start: { - character: 23, - line: 0, +[ + { + code: { + span: { + end: { + character: 71, + line: 0, + }, + start: { + character: 23, + line: 0, + }, }, + specifier: "https://deno.land/std@0.200.0/assert/assert.ts", + }, + from: { + name: "deno.land/std", + path: "/assert/assert.ts", + protocol: "https:", + version: "0.200.0", + }, + to: { + name: "deno.land/std", + path: "/assert/assert.ts", + protocol: "https:", + version: "0.218.0", }, - specifier: "https://deno.land/std@0.200.0/assert/assert.ts", - }, - from: { - name: "deno.land/std", - path: "/assert/assert.ts", - protocol: "https:", - version: "0.200.0", - }, - to: { - name: "deno.land/std", - path: "/assert/assert.ts", - protocol: "https:", - version: "123.456.789", }, -} +] `; snapshot[`collect - unversioned.ts 1`] = ` -{ - code: { - span: { - end: { - character: 63, - line: 0, - }, - start: { - character: 23, - line: 0, +[ + { + code: { + span: { + end: { + character: 63, + line: 0, + }, + start: { + character: 23, + line: 0, + }, }, + specifier: "https://deno.land/std/assert/assert.ts", + }, + from: { + name: "deno.land/std", + path: "/assert/assert.ts", + protocol: "https:", + version: undefined, + }, + to: { + name: "deno.land/std", + path: "/assert/assert.ts", + protocol: "https:", + version: "0.218.0", }, - specifier: "https://deno.land/std/assert/assert.ts", - }, - from: { - name: "deno.land/std", - path: "/assert/assert.ts", - protocol: "https:", - version: undefined, - }, - to: { - name: "deno.land/std", - path: "/assert/assert.ts", - protocol: "https:", - version: "123.456.789", }, -} +] `; snapshot[`collect - jsr.ts 1`] = ` -{ - code: { - span: { - end: { - character: 48, - line: 0, - }, - start: { - character: 23, - line: 0, +[ + { + code: { + span: { + end: { + character: 48, + line: 0, + }, + start: { + character: 23, + line: 0, + }, }, + specifier: "jsr:@std/assert@0.210.0", + }, + from: { + name: "@std/assert", + path: "", + protocol: "jsr:", + version: "0.210.0", + }, + to: { + name: "@std/assert", + path: "", + protocol: "jsr:", + version: "123.456.789", }, - specifier: "jsr:@std/assert@0.210.0", - }, - from: { - name: "@std/assert", - path: "", - protocol: "jsr:", - version: "0.210.0", - }, - to: { - name: "@std/assert", - path: "", - protocol: "jsr:", - version: "123.456.789", }, -} +] `;