diff --git a/deno.lock b/deno.lock index 5a8fe9c0..5be3c10d 100644 --- a/deno.lock +++ b/deno.lock @@ -93,6 +93,9 @@ "https://deno.land/std@0.204.0/assert/assert_throws.ts": "63784e951475cb7bdfd59878cd25a0931e18f6dc32a6077c454b2cd94f4f4bcd", "https://deno.land/std@0.204.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", "https://deno.land/std@0.204.0/assert/equal.ts": "9f1a46d5993966d2596c44e5858eec821859b45f783a5ee2f7a695dfc12d8ece", + "https://deno.land/std@0.204.0/async/deferred.ts": "42790112f36a75a57db4a96d33974a936deb7b04d25c6084a9fa8a49f135def8", + "https://deno.land/std@0.204.0/bytes/concat.ts": "d26d6f3d7922e6d663dacfcd357563b7bf4a380ce5b9c2bbe0c8586662f25ce2", + "https://deno.land/std@0.204.0/bytes/copy.ts": "939d89e302a9761dcf1d9c937c7711174ed74c59eef40a1e4569a05c9de88219", "https://deno.land/std@0.204.0/collections/distinct.ts": "01595bd8e608a5e2de8a8631055587e6f01d68861e463eef34f8e295713500b8", "https://deno.land/std@0.204.0/collections/filter_keys.ts": "a29cfe8730ddb54e9e071ea45e8a82e166c7629d18675652def70c1bf80e2ef6", "https://deno.land/std@0.204.0/collections/max_by.ts": "9d5940986aac51b2e4feaebef9cd8bf6e1eceeee5edcf3303e334b737f99520d", @@ -100,7 +103,9 @@ "https://deno.land/std@0.204.0/fs/_util.ts": "fbf57dcdc9f7bc8128d60301eece608246971a7836a3bb1e78da75314f08b978", "https://deno.land/std@0.204.0/fs/ensure_dir.ts": "dc64c4c75c64721d4e3fb681f1382f803ff3d2868f08563ff923fdd20d071c40", "https://deno.land/std@0.204.0/fs/ensure_file.ts": "39ac83cc283a20ec2735e956adf5de3e8a3334e0b6820547b5772f71c49ae083", + "https://deno.land/std@0.204.0/fs/eol.ts": "f1f2eb348a750c34500741987b21d65607f352cf7205f48f4319d417fff42842", "https://deno.land/std@0.204.0/fs/exists.ts": "cb59a853d84871d87acab0e7936a4dac11282957f8e195102c5a7acb42546bb8", + "https://deno.land/std@0.204.0/io/buffer.ts": "2108faba32659e5a390bb59b1b4578ff0120b185d9310dd6fbf3b3a3d5775715", "https://deno.land/std@0.204.0/json/common.ts": "ecd5e87d45b5f0df33238ed8b1746e1444da7f5c86ae53d0f0b04280f41a25bb", "https://deno.land/std@0.204.0/jsonc/parse.ts": "c1096e2b7ffb4996d7ed841dfdb29a4fccc78edcc55299beaa20d6fe5facf7b6", "https://deno.land/std@0.204.0/path/_common/assert_path.ts": "061e4d093d4ba5aebceb2c4da3318bfe3289e868570e9d3a8e327d91c2958946", @@ -172,6 +177,32 @@ "https://deno.land/std@0.204.0/path/windows/separator.ts": "ae21f27015f10510ed1ac4a0ba9c4c9c967cbdd9d9e776a3e4967553c397bd5d", "https://deno.land/std@0.204.0/path/windows/to_file_url.ts": "8e9ea9e1ff364aa06fa72999204229952d0a279dbb876b7b838b2b2fea55cce3", "https://deno.land/std@0.204.0/path/windows/to_namespaced_path.ts": "e0f4d4a5e77f28a5708c1a33ff24360f35637ba6d8f103d19661255ef7bfd50d", + "https://deno.land/std@0.204.0/streams/_common.ts": "3b2c1f0287ce2ad51fff4091a7d0f48375c85b0ec341468e76d5ac13bb0014dd", + "https://deno.land/std@0.204.0/streams/buffer.ts": "6cd773d22cf21bb988a98cc551b5abfc4c3b03516f93eaa3fb6f2f6e16032deb", + "https://deno.land/std@0.204.0/streams/byte_slice_stream.ts": "c46d7c74836fc8c1a9acd9fe211cbe1bbaaee1b36087c834fb03af4991135c3a", + "https://deno.land/std@0.204.0/streams/copy.ts": "bbd406bd9a71034a412c05bb0fd8b7964cc7f2e2ad0f0a1cc0463cdc816d540b", + "https://deno.land/std@0.204.0/streams/delimiter_stream.ts": "ea793d21bfaae27163f0b50d26298a2ad543803283aeaff082df50dd9c08614f", + "https://deno.land/std@0.204.0/streams/early_zip_readable_streams.ts": "4005fa74162b943f79899e5d7cb96adcbc0a6b867f9144974ed12d30e0a556e1", + "https://deno.land/std@0.204.0/streams/iterate_reader.ts": "3b42d3056c8ccade561f1c7ac22d5e671e745933d9f9168fd3b5913588d911c3", + "https://deno.land/std@0.204.0/streams/limited_bytes_transform_stream.ts": "05dc592ffaab83257494d22dd53917e56243c26e5e3129b3f13ddbbbc4785048", + "https://deno.land/std@0.204.0/streams/limited_transform_stream.ts": "d69ab790232c1b86f53621ad41ef03c235f2abb4b7a1cd51960ad6e12ee55e38", + "https://deno.land/std@0.204.0/streams/merge_readable_streams.ts": "6ec024e26e118a7c7cc5b2a0c062ace693b7d38bbcda5707975ccad423940f1b", + "https://deno.land/std@0.204.0/streams/mod.ts": "e133c271474f1e9dc19f5c4959406f5c2101540c56a3e7d7abc6bdb7bcef807e", + "https://deno.land/std@0.204.0/streams/read_all.ts": "3b20a50af87d1bfebefa9c2dbda49e2b214d8ab0382ffdcc8ce858af80a912be", + "https://deno.land/std@0.204.0/streams/readable_stream_from_reader.ts": "c4078b6fd4832748f979cb211facf483c82a840f3c0d88752f46d4dcddb3110a", + "https://deno.land/std@0.204.0/streams/reader_from_iterable.ts": "91d578a6c75e443654a75580a67cf3af1eeb4b186fa0b309f35b49c9c53be2b7", + "https://deno.land/std@0.204.0/streams/reader_from_stream_reader.ts": "53dfdb6c608005eb9e0610a9f9cbc51c66f34a519363f1abd4e8a09ab4ad8f9e", + "https://deno.land/std@0.204.0/streams/text_delimiter_stream.ts": "f0dc8ff953a8a77f0d1fa8db1fee62de817f36e20d79b00b1362847e30fbdd90", + "https://deno.land/std@0.204.0/streams/text_line_stream.ts": "0f2c4b33a5fdb2476f2e060974cba1347cefe99a4af33c28a57524b1a34750fa", + "https://deno.land/std@0.204.0/streams/to_array_buffer.ts": "de5803ef98cf2bfd8eeaaa17ae4f4d0364377bd6f6e425110e7010c1cb48196b", + "https://deno.land/std@0.204.0/streams/to_blob.ts": "4cf929b3ffe5d4dbe445d572246159ee2eebf69e998a0ba867b9048fb6e92797", + "https://deno.land/std@0.204.0/streams/to_json.ts": "10635fac9826392bb8e70bc8354d3beb1058fa1676aa9b6f98257a0d5575f520", + "https://deno.land/std@0.204.0/streams/to_text.ts": "4018b5f7a0e7ab28d6957c239e401f8b534ef326b7ebd282ca76b3cd55380aab", + "https://deno.land/std@0.204.0/streams/to_transform_stream.ts": "50af06140c414090df8c3879f46bed4043a5d73caa60352c0c9682a88ec7a8c9", + "https://deno.land/std@0.204.0/streams/writable_stream_from_writer.ts": "b5ee3ef4d5be0b3214cddf0f87e804a6a31b9a5632c96277912e7543eaec9f40", + "https://deno.land/std@0.204.0/streams/write_all.ts": "4cdd36256f892fe7aead46338054f6ea813a63765e87bda4c60e8c5a57d1c5c1", + "https://deno.land/std@0.204.0/streams/writer_from_stream_writer.ts": "7188ce589d6179693b488b478c05137d4d15b93735ca26ec01e6e44aed8cb0c6", + "https://deno.land/std@0.204.0/streams/zip_readable_streams.ts": "721c5bce8862c8225e60995b2d61c7b1b1d5b5178b60d303a01f453d5c26bb62", "https://deno.land/std@0.204.0/testing/_test_suite.ts": "30f018feeb3835f12ab198d8a518f9089b1bcb2e8c838a8b615ab10d5005465c", "https://deno.land/std@0.204.0/testing/bdd.ts": "3f446df5ef8e856a869e8eec54c8482590415741ff0b6358a00c43486cc15769", "https://deno.land/std@0.204.0/testing/snapshot.ts": "d53cc4ad3250e3a826df9a1a90bc19c9a92c8faa8fd508d16b5e6ce8699310ca", diff --git a/lib/__snapshots__/file_test.ts.snap b/lib/__snapshots__/file_test.ts.snap index d5c5e37c..57485091 100644 --- a/lib/__snapshots__/file_test.ts.snap +++ b/lib/__snapshots__/file_test.ts.snap @@ -1,24 +1,22 @@ export const snapshot = {}; snapshot[`writeAll > direct import 1`] = ` -'import { VERSION } from "https://deno.land/std@0.204.0/version.ts"; -import { createGraph } from "https://deno.land/x/deno_graph@0.59.1/mod.ts"; +'import { VERSION } from "https://deno.land/std@0.205.0/version.ts"; +import { createGraph } from "https://deno.land/x/deno_graph@0.59.2/mod.ts"; import { emojify } from "npm:node-emoji@2.1.0"; -import { noop } from "./lib.ts"; -' +import { noop } from "./lib.ts";' `; snapshot[`writeAll > direct import 2`] = ` -'import { VERSION } from "https://deno.land/std@0.204.0/version.ts"; -export const noop = () => {}; -' +'import { VERSION } from "https://deno.land/std@0.205.0/version.ts"; +export const noop = () => {};' `; snapshot[`writeAll > import map 1`] = ` '{ "imports": { "std/": "https://deno.land/std@0.200.0/", - "deno_graph": "https://deno.land/x/deno_graph@0.59.1/mod.ts", + "deno_graph": "https://deno.land/x/deno_graph@0.59.2/mod.ts", "node-emoji": "npm:node-emoji@2.1.0", "/": "./" } @@ -26,26 +24,20 @@ snapshot[`writeAll > import map 1`] = ` `; snapshot[`writeAll > import map 2`] = ` -'import { VERSION } from "https://deno.land/std@0.204.0/version.ts"; -export const noop = () => {}; -' +'import { VERSION } from "https://deno.land/std@0.205.0/version.ts"; +export const noop = () => {};' `; -snapshot[`writeAll > import map with no resolve 1`] = ` -'import {} from "https://deno.land/std@0.204.0/version.ts"; -' -`; +snapshot[`writeAll > import map with no resolve 1`] = `'import {} from "https://deno.land/std@0.205.0/version.ts";'`; snapshot[`writeAll > unversioned specifiers 1`] = ` -'import { VERSION } from "https://deno.land/std@0.204.0/version.ts"; -import { createGraph } from "https://deno.land/x/deno_graph@0.59.1/mod.ts"; +'import { VERSION } from "https://deno.land/std@0.205.0/version.ts"; +import { createGraph } from "https://deno.land/x/deno_graph@0.59.2/mod.ts"; import { emojify } from "npm:node-emoji@2.1.0"; -import { noop } from "./lib.ts"; -' +import { noop } from "./lib.ts";' `; snapshot[`writeAll > unversioned specifiers 2`] = ` -'import { VERSION } from "https://deno.land/std@0.204.0/version.ts"; -export const noop = () => {}; -' +'import { VERSION } from "https://deno.land/std@0.205.0/version.ts"; +export const noop = () => {};' `; diff --git a/lib/__snapshots__/git_test.ts.snap b/lib/__snapshots__/git_test.ts.snap deleted file mode 100644 index 62064bb0..00000000 --- a/lib/__snapshots__/git_test.ts.snap +++ /dev/null @@ -1,40 +0,0 @@ -export const snapshot = {}; - -snapshot[`commitAll() > no grouping 1`] = ` -[ - 'import { VERSION } from "https://deno.land/std@0.204.0/version.ts"; -import { createGraph } from "https://deno.land/x/deno_graph@0.59.1/mod.ts"; -import { emojify } from "npm:node-emoji@2.1.0"; -import { noop } from "./lib.ts"; -', - 'import { VERSION } from "https://deno.land/std@0.204.0/version.ts"; -export const noop = () => {}; -', -] -`; - -snapshot[`commitAll() > group by dependency name 1`] = ` -[ - 'import { VERSION } from "https://deno.land/std@0.200.0/version.ts"; -import { createGraph } from "https://deno.land/x/deno_graph@0.50.0/mod.ts"; -import { emojify } from "npm:node-emoji@2.1.0"; -import { noop } from "./lib.ts"; -', - 'import { VERSION } from "https://deno.land/std@0.204.0/version.ts"; -export const noop = () => {}; -', -] -`; - -snapshot[`commitAll() > group by module (file) name 1`] = ` -[ - 'import { VERSION } from "https://deno.land/std@0.204.0/version.ts"; -import { createGraph } from "https://deno.land/x/deno_graph@0.59.1/mod.ts"; -import { emojify } from "npm:node-emoji@2.1.0"; -import { noop } from "./lib.ts"; -', - 'import { VERSION } from "https://deno.land/std@0.204.0/version.ts"; -export const noop = () => {}; -', -] -`; diff --git a/lib/__snapshots__/update_test.ts.snap b/lib/__snapshots__/update_test.ts.snap index 2c0ebff9..d3d015ec 100644 --- a/lib/__snapshots__/update_test.ts.snap +++ b/lib/__snapshots__/update_test.ts.snap @@ -3,18 +3,18 @@ export const snapshot = {}; snapshot[`_create > https://deno.land/std 1`] = ` { code: { - span: undefined, + span: {}, specifier: "https://deno.land/std@0.1.0/version.ts", }, name: "deno.land/std", path: "/version.ts", specifier: { from: "https://deno.land/std@0.1.0/version.ts", - to: "https://deno.land/std@0.204.0/version.ts", + to: "https://deno.land/std@0.205.0/version.ts", }, version: { from: "0.1.0", - to: "0.204.0", + to: "0.205.0", }, } `; @@ -22,18 +22,18 @@ snapshot[`_create > https://deno.land/std 1`] = ` snapshot[`_create > https://deno.land/std - unversioned 1`] = ` { code: { - span: undefined, + span: {}, specifier: "https://deno.land/std/version.ts", }, name: "deno.land/std", path: "/version.ts", specifier: { from: "https://deno.land/std/version.ts", - to: "https://deno.land/std@0.204.0/version.ts", + to: "https://deno.land/std@0.205.0/version.ts", }, version: { from: undefined, - to: "0.204.0", + to: "0.205.0", }, } `; @@ -41,18 +41,18 @@ snapshot[`_create > https://deno.land/std - unversioned 1`] = ` snapshot[`_create > https://deno.land/x/deno_graph 1`] = ` { code: { - span: undefined, + span: {}, specifier: "https://deno.land/x/deno_graph@0.1.0/mod.ts", }, name: "deno.land/x/deno_graph", path: "/mod.ts", specifier: { from: "https://deno.land/x/deno_graph@0.1.0/mod.ts", - to: "https://deno.land/x/deno_graph@0.59.1/mod.ts", + to: "https://deno.land/x/deno_graph@0.59.2/mod.ts", }, version: { from: "0.1.0", - to: "0.59.1", + to: "0.59.2", }, } `; @@ -60,7 +60,7 @@ snapshot[`_create > https://deno.land/x/deno_graph 1`] = ` snapshot[`_create > npm:node-emoji 1`] = ` { code: { - span: undefined, + span: {}, specifier: "npm:node-emoji@1.0.0", }, name: "node-emoji", @@ -79,7 +79,7 @@ snapshot[`_create > npm:node-emoji 1`] = ` snapshot[`_create > npm:node-emoji - unversioned 1`] = ` { code: { - span: undefined, + span: {}, specifier: "npm:node-emoji", }, name: "node-emoji", @@ -114,11 +114,11 @@ snapshot[`collect > direct import 1`] = ` path: "/version.ts", specifier: { from: "https://deno.land/std@0.200.0/version.ts", - to: "https://deno.land/std@0.204.0/version.ts", + to: "https://deno.land/std@0.205.0/version.ts", }, version: { from: "0.200.0", - to: "0.204.0", + to: "0.205.0", }, } `; @@ -142,11 +142,11 @@ snapshot[`collect > direct import 2`] = ` path: "/mod.ts", specifier: { from: "https://deno.land/x/deno_graph@0.50.0/mod.ts", - to: "https://deno.land/x/deno_graph@0.59.1/mod.ts", + to: "https://deno.land/x/deno_graph@0.59.2/mod.ts", }, version: { from: "0.50.0", - to: "0.59.1", + to: "0.59.2", }, } `; @@ -198,11 +198,11 @@ snapshot[`collect > direct import 4`] = ` path: "/version.ts", specifier: { from: "https://deno.land/std@0.200.0/version.ts", - to: "https://deno.land/std@0.204.0/version.ts", + to: "https://deno.land/std@0.205.0/version.ts", }, version: { from: "0.200.0", - to: "0.204.0", + to: "0.205.0", }, } `; @@ -226,11 +226,11 @@ snapshot[`collect > import map 1`] = ` path: "/version.ts", specifier: { from: "https://deno.land/std@0.200.0/version.ts", - to: "https://deno.land/std@0.204.0/version.ts", + to: "https://deno.land/std@0.205.0/version.ts", }, version: { from: "0.200.0", - to: "0.204.0", + to: "0.205.0", }, } `; @@ -254,11 +254,11 @@ snapshot[`collect > import map 2`] = ` path: "/mod.ts", specifier: { from: "https://deno.land/x/deno_graph@0.50.0/mod.ts", - to: "https://deno.land/x/deno_graph@0.59.1/mod.ts", + to: "https://deno.land/x/deno_graph@0.59.2/mod.ts", }, version: { from: "0.50.0", - to: "0.59.1", + to: "0.59.2", }, } `; @@ -310,38 +310,11 @@ snapshot[`collect > import map 4`] = ` path: "/version.ts", specifier: { from: "https://deno.land/std@0.200.0/version.ts", - to: "https://deno.land/std@0.204.0/version.ts", + to: "https://deno.land/std@0.205.0/version.ts", }, version: { from: "0.200.0", - to: "0.204.0", + to: "0.205.0", }, } `; - -snapshot[`applyToModule > https://deno.land/x/deno_graph 1`] = ` -'import { VERSION } from "https://deno.land/std@0.200.0/version.ts"; -import { createGraph } from "https://deno.land/x/deno_graph@0.59.1/mod.ts"; -import { emojify } from "npm:node-emoji@1.0.0"; -import { noop } from "./lib.ts"; -' -`; - -snapshot[`applyToModule > npm:node-emoji 1`] = ` -'import { VERSION } from "https://deno.land/std@0.200.0/version.ts"; -import { createGraph } from "https://deno.land/x/deno_graph@0.50.0/mod.ts"; -import { emojify } from "npm:node-emoji@2.1.0"; -import { noop } from "./lib.ts"; -' -`; - -snapshot[`applyToImportMap > deno_graph 1`] = ` -'{ - "imports": { - "std/": "https://deno.land/std@0.200.0/", - "deno_graph": "https://deno.land/x/deno_graph@0.59.1/mod.ts", - "node-emoji": "npm:node-emoji@1.0.0", - "/": "./" - } -}' -`; diff --git a/lib/file.ts b/lib/file.ts index 7f1a3017..925c73aa 100644 --- a/lib/file.ts +++ b/lib/file.ts @@ -1,11 +1,16 @@ +import { assertExists } from "./std/assert.ts"; +import { parse as parseJsonc } from "./std/jsonc.ts"; +import { detectEOL } from "./std/fs.ts"; +import { TextLineStream } from "./std/streams.ts"; import { DependencyUpdate } from "./update.ts"; +import { ImportMapJson } from "./import_map.ts"; import { URI } from "./uri.ts"; export interface FileUpdate { + /** The type of the updated file. */ + kind: "module" | "import-map"; /** The specifier of the updated dependency (a remote module.) */ specifier: URI<"file">; - /** The updated content of the module. */ - content: string; /** The dependency updates in the module. */ dependencies: DependencyUpdate[]; } @@ -16,31 +21,27 @@ export const FileUpdate = { writeAll, }; -async function collect( +function collect( dependencies: DependencyUpdate[], -): Promise { +): FileUpdate[] { /** A map of module specifiers to the module content updates. */ - const results = new Map, FileUpdate>(); + const fileToDepsMap = new Map, DependencyUpdate[]>(); for (const dependency of dependencies) { const referrer = dependency.map?.source ?? dependency.referrer; - const current = results.get(referrer) ?? { - specifier: referrer, - content: await Deno.readTextFile(new URL(referrer)), - dependencies: [], - } satisfies FileUpdate; - const content = dependency.map - ? DependencyUpdate.applyToImportMap(dependency, current.content) - : DependencyUpdate.applyToModule(dependency, current.content); - results.set(referrer, { - specifier: current.specifier, - content, - dependencies: current.dependencies.concat(dependency), - }); + const deps = fileToDepsMap.get(referrer) ?? + fileToDepsMap.set(referrer, []).get(referrer)!; + deps.push(dependency); } - return Array.from(results.values()); + return Array.from(fileToDepsMap.entries()).map(( + [specifier, dependencies], + ) => ({ + kind: dependencies[0].map ? "import-map" : "module", + specifier, + dependencies, + })); } -export async function writeAll( +async function writeAll( updates: FileUpdate[], options?: { onWrite?: (result: FileUpdate) => void | Promise; @@ -52,8 +53,78 @@ export async function writeAll( } } -export async function write( - result: FileUpdate, +function write( + update: FileUpdate, +) { + switch (update.kind) { + case "module": + return writeToModule(update); + case "import-map": + return writeToImportMap(update); + } +} + +async function writeToModule( + update: FileUpdate, ) { - await Deno.writeTextFile(new URL(result.specifier), result.content); + const lineToUpdateMap = new Map( + update.dependencies.map(( + dependency, + ) => [dependency.code.span.start.line, dependency]), + ); + const lines: string[] = []; + const content = await Deno.readTextFile(new URL(update.specifier)); + await ReadableStream.from(content) + .pipeThrough(new TextLineStream({ allowCR: true })) + .pipeThrough( + new TextLineTransformer( + (current, line) => { + const update = lineToUpdateMap.get(current); + return update + ? line.replace(update.specifier.from, update.specifier.to) + : line; + }, + ), + ) + .pipeTo( + new WritableStream({ + write(line) { + lines.push(line); + }, + }), + ); + const eol = detectEOL(content) ?? "\n"; + await Deno.writeTextFile(new URL(update.specifier), lines.join(eol)); +} + +class TextLineTransformer extends TransformStream { + #current = 0; + constructor( + transform: (current: number, line: string) => string, + ) { + super({ + transform: (line, controller) => { + controller.enqueue(transform(this.#current++, line)); + }, + }); + } +} + +async function writeToImportMap( + /** The dependency update to apply. */ + update: FileUpdate, +): Promise { + const content = await Deno.readTextFile(new URL(update.specifier)); + const json = parseJsonc(content) as unknown as ImportMapJson; + for (const dependency of update.dependencies) { + assertExists(dependency.map); + json.imports[dependency.map.from] = dependency.map.to.replace( + dependency.specifier.from, + dependency.specifier.to, + ); + } + await Deno.writeTextFile( + new URL(update.specifier), + JSON.stringify(json, null, 2), + ); } diff --git a/lib/file_test.ts b/lib/file_test.ts index 04b6cee1..58347ba9 100644 --- a/lib/file_test.ts +++ b/lib/file_test.ts @@ -5,8 +5,10 @@ import { beforeEach, describe, it, + SpyCall, } from "./std/testing.ts"; import { assertArrayIncludes, assertEquals } from "./std/assert.ts"; +import { EOL, formatEOL } from "./std/fs.ts"; import { assertFindSpyCallArg, FileSystemFake, @@ -17,14 +19,14 @@ import { FileUpdate } from "./file.ts"; describe("collect", () => { it("direct import", async () => { - const results = await FileUpdate.collect( + const results = FileUpdate.collect( await DependencyUpdate.collect("./test/fixtures/direct-import/mod.ts"), ); assertEquals(results.length, 2); }); it("import map", async () => { - const results = await FileUpdate.collect( + const results = FileUpdate.collect( await DependencyUpdate.collect( "./test/fixtures/import-map/mod.ts", { importMap: "./test/fixtures/import-map/deno.json" }, @@ -42,7 +44,7 @@ describe("collect", () => { }); it("import map with no resolve", async () => { - const results = await FileUpdate.collect( + const results = FileUpdate.collect( await DependencyUpdate.collect( "./test/fixtures/import-map-no-resolve/mod.ts", { importMap: "./test/fixtures/import-map-no-resolve/deno.json" }, @@ -68,7 +70,7 @@ describe("writeAll", () => { }); it("direct import", async (t) => { - const results = await FileUpdate.collect( + const results = FileUpdate.collect( await DependencyUpdate.collect("./test/fixtures/direct-import/mod.ts"), ); await FileUpdate.writeAll(results); @@ -77,18 +79,18 @@ describe("writeAll", () => { 0, new URL("../test/fixtures/direct-import/mod.ts", import.meta.url), ); - await assertSnapshot(t, call_1.args[1]); + await assertWriteTextFileSnapshot(t, call_1); const call_2 = assertFindSpyCallArg( writeTextFileStub, 0, new URL("../test/fixtures/direct-import/lib.ts", import.meta.url), ); - await assertSnapshot(t, call_2.args[1]); + await assertWriteTextFileSnapshot(t, call_2); assertSpyCalls(writeTextFileStub, 2); }); it("import map", async (t) => { - const results = await FileUpdate.collect( + const results = FileUpdate.collect( await DependencyUpdate.collect( "./test/fixtures/import-map/mod.ts", { importMap: "./test/fixtures/import-map/deno.json" }, @@ -100,18 +102,18 @@ describe("writeAll", () => { 0, new URL("../test/fixtures/import-map/deno.json", import.meta.url), ); - await assertSnapshot(t, call_1.args[1]); + await assertWriteTextFileSnapshot(t, call_1); const call_2 = assertFindSpyCallArg( writeTextFileStub, 0, new URL("../test/fixtures/import-map/lib.ts", import.meta.url), ); - await assertSnapshot(t, call_2.args[1]); + await assertWriteTextFileSnapshot(t, call_2); assertSpyCalls(writeTextFileStub, 2); }); it("import map with no resolve", async (t) => { - const results = await FileUpdate.collect( + const results = FileUpdate.collect( await DependencyUpdate.collect( "./test/fixtures/import-map-no-resolve/mod.ts", { importMap: "./test/fixtures/import-map-no-resolve/deno.json" }, @@ -123,12 +125,12 @@ describe("writeAll", () => { 0, new URL("../test/fixtures/import-map-no-resolve/mod.ts", import.meta.url), ); - await assertSnapshot(t, call.args[1]); + await assertWriteTextFileSnapshot(t, call); assertSpyCalls(writeTextFileStub, 1); }); it("unversioned specifiers", async (t) => { - const results = await FileUpdate.collect( + const results = FileUpdate.collect( await DependencyUpdate.collect("./test/fixtures/unversioned/mod.ts"), ); await FileUpdate.writeAll(results); @@ -137,13 +139,20 @@ describe("writeAll", () => { 0, new URL("../test/fixtures/unversioned/mod.ts", import.meta.url), ); - await assertSnapshot(t, call_1.args[1]); + await assertWriteTextFileSnapshot(t, call_1); const call_2 = assertFindSpyCallArg( writeTextFileStub, 0, new URL("../test/fixtures/unversioned/lib.ts", import.meta.url), ); - await assertSnapshot(t, call_2.args[1]); + await assertWriteTextFileSnapshot(t, call_2); assertSpyCalls(writeTextFileStub, 2); }); }); + +async function assertWriteTextFileSnapshot( + t: Deno.TestContext, + call: SpyCall, +) { + await assertSnapshot(t, formatEOL(call.args[1], EOL.LF)); +} diff --git a/lib/git.ts b/lib/git.ts index d96a7020..8426d327 100644 --- a/lib/git.ts +++ b/lib/git.ts @@ -103,7 +103,7 @@ async function execGitCommit( commit: GitCommit, options?: CommitOptions, ) { - const results = await FileUpdate.collect(commit.updates); + const results = FileUpdate.collect(commit.updates); await FileUpdate.writeAll(results); await options?.preCommit?.(commit); await _add(results, options?.gitAddOptions ?? []); diff --git a/lib/git_test.ts b/lib/git_test.ts index 912ba67d..a63aa15a 100644 --- a/lib/git_test.ts +++ b/lib/git_test.ts @@ -1,12 +1,12 @@ import { afterAll, - assertSnapshot, assertSpyCalls, beforeAll, beforeEach, describe, it, } from "./std/testing.ts"; +import { assertArrayIncludes } from "./std/assert.ts"; import { assertFindSpyCall, createCommandStub, @@ -48,17 +48,26 @@ describe("commitAll()", () => { Deno.Command = CommandStub; }); + const expected = [ + `import { VERSION } from "https://deno.land/std@0.205.0/version.ts"; +import { createGraph } from "https://deno.land/x/deno_graph@0.59.2/mod.ts"; +import { emojify } from "npm:node-emoji@2.1.0"; +import { noop } from "./lib.ts";`, + `import { VERSION } from "https://deno.land/std@0.205.0/version.ts"; +export const noop = () => {};`, + ]; + // "git add src/fixtures/mod.ts src/fixtures/lib.ts", - it("no grouping", async (t) => { + it("no grouping", async () => { await commitAll(updates); // TODO: Can't test this because of the order of targets is not guaranteed. // assertGitAdd(CommandStub, "src/fixtures/mod.ts", "src/fixtures/lib.ts"); assertGitCommit(CommandStub, "build(deps): update dependencies"); assertSpyCalls(CommandStub, 2); - await assertSnapshot(t, Array.from(fileSystemFake.values())); + assertArrayIncludes(Array.from(fileSystemFake.values()), expected); }); - it("group by dependency name", async (t) => { + it("group by dependency name", async () => { await commitAll(updates, { groupBy: (update) => update.name, composeCommitMessage: ({ group }) => `build(deps): update ${group}`, @@ -71,10 +80,10 @@ describe("commitAll()", () => { // assertGitAdd(CommandStub, "src/fixtures/lib.ts", "src/fixtures/mod.ts"); assertGitCommit(CommandStub, "build(deps): update deno.land/std"); assertSpyCalls(CommandStub, 6); - await assertSnapshot(t, Array.from(fileSystemFake.values())); + assertArrayIncludes(Array.from(fileSystemFake.values()), expected); }); - it("group by module (file) name", async (t) => { + it("group by module (file) name", async () => { await commitAll(updates, { groupBy: (update) => update.referrer, composeCommitMessage: ({ group }) => { @@ -86,15 +95,19 @@ describe("commitAll()", () => { assertGitAdd(CommandStub, "test/fixtures/direct-import/mod.ts"); assertGitCommit( CommandStub, - "build(deps): update test/fixtures/direct-import/mod.ts", + `build(deps): update ${ + normalizePath("test/fixtures/direct-import/mod.ts") + }`, ); assertGitAdd(CommandStub, "test/fixtures/direct-import/lib.ts"); assertGitCommit( CommandStub, - "build(deps): update test/fixtures/direct-import/lib.ts", + `build(deps): update ${ + normalizePath("test/fixtures/direct-import/lib.ts") + }`, ); assertSpyCalls(CommandStub, 4); - await assertSnapshot(t, Array.from(fileSystemFake.values())); + assertArrayIncludes(Array.from(fileSystemFake.values()), expected); }); }); diff --git a/lib/std/fs.ts b/lib/std/fs.ts index 919aaa5c..a1977f4e 100644 --- a/lib/std/fs.ts +++ b/lib/std/fs.ts @@ -1 +1,6 @@ export { existsSync } from "https://deno.land/std@0.204.0/fs/exists.ts"; +export { + detect as detectEOL, + EOL, + format as formatEOL, +} from "https://deno.land/std@0.204.0/fs/eol.ts"; diff --git a/lib/std/streams.ts b/lib/std/streams.ts new file mode 100644 index 00000000..5a42f6a8 --- /dev/null +++ b/lib/std/streams.ts @@ -0,0 +1 @@ +export { TextLineStream } from "https://deno.land/std@0.204.0/streams/mod.ts"; diff --git a/lib/testing.ts b/lib/testing.ts index 72d32d1a..5e765b91 100644 --- a/lib/testing.ts +++ b/lib/testing.ts @@ -9,6 +9,7 @@ import { stub, } from "./std/testing.ts"; import { AssertionError } from "./std/assert.ts"; +import { EOL, formatEOL } from "./std/fs.ts"; import { URI } from "./uri.ts"; export function createCommandStub(): ConstructorSpy< @@ -53,9 +54,10 @@ export const ReadTextFileStub = { Deno, "readTextFile", async (path) => { - return fs.get(URI.from(path)) ?? options?.readThrough - ? await original(path) - : _throw(new Deno.errors.NotFound(`File not found: ${path}`)); + return fs.get(URI.from(path)) ?? + (options?.readThrough + ? await original(path) + : _throw(new Deno.errors.NotFound(`File not found: ${path}`))); }, ); }, @@ -69,9 +71,9 @@ export const WriteTextFileStub = { return stub( Deno, "writeTextFile", - // deno-lint-ignore require-await - async (path, data) => { - fs.set(URI.from(path), data.toString()); + (path, data) => { + fs.set(URI.from(path), formatEOL(data.toString(), EOL.LF)); + return Promise.resolve(); }, ); }, @@ -96,7 +98,9 @@ export function assertFindSpyCall< } }); if (!call) { - throw new AssertionError("Expected spy call does not exist"); + throw new AssertionError( + `Expected spy call does not exist: ${JSON.stringify(expected)}`, + ); } return call; } diff --git a/lib/update.ts b/lib/update.ts index e2974498..4f2507fe 100644 --- a/lib/update.ts +++ b/lib/update.ts @@ -8,7 +8,7 @@ import { } from "./x/deno_graph.ts"; import { URI } from "./uri.ts"; import type { Maybe } from "./types.ts"; -import { ImportMap, ImportMapJson } from "./import_map.ts"; +import { ImportMap } from "./import_map.ts"; import { Dependency, type DependencyProps, parseSemVer } from "./dependency.ts"; type DependencyJson = NonNullable[number]; @@ -26,16 +26,13 @@ export interface DependencyUpdate extends Omit { to: URI<"http" | "https" | "npm">; }; version: VersionProp; - /** The code of the dependency. */ - code?: { + /** The code of the dependency. Note that `type` in the DependencyJSON is + * merged into `code` here for convenience. */ + code: { /** The original specifier of the dependency appeared in the code. */ specifier: string; span: NonNullable["span"]; }; - type?: { - specifier: string; - span: NonNullable["span"]; - }; /** The specifier of the module that imports the dependency. */ referrer: URI<"file">; /** Information about the import map used to resolve the dependency. */ @@ -51,8 +48,6 @@ export interface DependencyUpdate extends Omit { export const DependencyUpdate = { collect, - applyToModule, - applyToImportMap, }; class DenoGraph { @@ -131,10 +126,7 @@ export async function collect( export async function _create( dependency: DependencyJson, referrer: URI<"file">, - options?: { - importMap?: ImportMap; - force?: boolean; - }, + options?: { importMap?: ImportMap }, ): Promise { const specifier = dependency.code?.specifier ?? dependency.type?.specifier; if (!specifier) { @@ -153,22 +145,24 @@ export async function _create( dependency.specifier, referrer, ); + const span = dependency.code?.span ?? dependency.type?.span; + if (!span) { + throw new Error( + `The dependency ${dependency.specifier} in ${ + URI.relative(referrer) + } has no span.`, + ); + } return { ...props, - // We prefer to put the fully resolved specifier here. specifier: { from: URI.ensure("http", "https", "npm")(specifier), to: URI.ensure("http", "https", "npm")(latest.href), }, - code: dependency.code && { - // We prefer to put the original specifier here. - specifier: dependency.specifier, - span: dependency.code.span, - }, - type: dependency.type && { + code: { // We prefer to put the original specifier here. specifier: dependency.specifier, - span: dependency.type.span, + span, }, version: { from: parseSemVer(specifier), @@ -185,38 +179,6 @@ export async function _create( }; } -function applyToModule( - /** The dependency update to apply. */ - update: DependencyUpdate, - /** Content of the module to update. */ - content: string, -): string { - const span = update.code?.span ?? update.type?.span!; - if (span.start.line !== span.end.line) { - throw new Error( - `The import specifier ${update.specifier} in ${update.referrer} is not a single line`, - ); - } - const line = span.start.line; - const lines = content.split("\n"); - lines[line] = lines[line].replace(update.specifier.from, update.specifier.to); - return lines.join("\n"); -} - -export function applyToImportMap( - /** The dependency update to apply. */ - update: DependencyUpdate, - /** Content of the import map to update. */ - content: string, -): string { - const json = JSON.parse(content) as ImportMapJson; - json.imports[update.map!.from] = update.map!.to.replace( - update.specifier.from, - update.specifier.to, - ); - return JSON.stringify(json, null, 2); -} - export function createVersionProp( dependencies: DependencyUpdate[], ): Maybe { diff --git a/lib/update_test.ts b/lib/update_test.ts index db069e90..a4d7586f 100644 --- a/lib/update_test.ts +++ b/lib/update_test.ts @@ -3,7 +3,6 @@ import { assertSnapshot, beforeAll, describe, it } from "./std/testing.ts"; import { assertEquals, assertExists, - assertNotEquals, assertObjectMatch, assertThrows, } from "./std/assert.ts"; @@ -30,6 +29,7 @@ describe("_create", () => { const update = await _create({ specifier: "https://deno.land/std@0.1.0/version.ts", code: { + span: {}, specifier: "https://deno.land/std@0.1.0/version.ts", } as any, }, URI.from("test/fixtures/direct-import/mod.ts")); @@ -40,6 +40,7 @@ describe("_create", () => { const update = await _create({ specifier: "https://deno.land/std/version.ts", code: { + span: {}, specifier: "https://deno.land/std/version.ts", } as any, }, URI.from("test/fixtures/direct-import/mod.ts")); @@ -50,6 +51,7 @@ describe("_create", () => { const update = await _create({ specifier: "https://deno.land/x/deno_graph@0.1.0/mod.ts", code: { + span: {}, specifier: "https://deno.land/x/deno_graph@0.1.0/mod.ts", } as any, }, URI.from("test/fixtures/direct-import/mod.ts")); @@ -60,6 +62,7 @@ describe("_create", () => { const update = await _create({ specifier: "npm:node-emoji@1.0.0", code: { + span: {}, specifier: "npm:node-emoji@1.0.0", } as any, }, URI.from("test/fixtures/direct-import/mod.ts")); @@ -70,6 +73,7 @@ describe("_create", () => { const update = await _create({ specifier: "npm:node-emoji", code: { + span: {}, specifier: "npm:node-emoji", } as any, }, URI.from("test/fixtures/direct-import/mod.ts")); @@ -90,6 +94,7 @@ describe("_create - with import map", () => { { specifier: "std/version.ts", code: { + span: {}, specifier: "https://deno.land/std@0.200.0/version.ts", } as any, }, @@ -143,65 +148,6 @@ describe("collect", () => { }); }); -describe("applyToModule", () => { - let updates: DependencyUpdate[]; - let content: string; - beforeAll(async () => { - updates = await DependencyUpdate.collect( - "./test/fixtures/direct-import/mod.ts", - ); - content = await Deno.readTextFile("./test/fixtures/direct-import/mod.ts"); - }); - it("https://deno.land/x/deno_graph", async (t) => { - const update = updates.find((update) => - update.specifier.from.includes("deno.land/x/deno_graph") - )!; - const result = DependencyUpdate.applyToModule( - update, - content, - ); - assertExists(result); - assertNotEquals(result, content); - await assertSnapshot(t, result); - }); - it("npm:node-emoji", async (t) => { - const update = updates.find((update) => - update.specifier.from.includes("node-emoji") - )!; - const result = DependencyUpdate.applyToModule( - update, - content, - ); - assertExists(result); - assertNotEquals(result, content); - await assertSnapshot(t, result); - }); -}); - -describe("applyToImportMap", () => { - let updates: DependencyUpdate[]; - let content: string; - beforeAll(async () => { - updates = await DependencyUpdate.collect( - "./test/fixtures/import-map/mod.ts", - { importMap: "test/fixtures/import-map/deno.json" }, - ); - content = await Deno.readTextFile("test/fixtures/import-map/deno.json"); - }); - it("deno_graph", async (t) => { - const update = updates.find((update) => - update.code?.specifier === "deno_graph" - )!; - const result = DependencyUpdate.applyToImportMap( - update, - content, - ); - assertExists(result); - assertNotEquals(result, content); - await assertSnapshot(t, result); - }); -}); - describe("createVersionProps()", () => { it("single version", () => { assertEquals(