Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: accept deno.json as an argument #136

Merged
merged 11 commits into from
Mar 5, 2024
42 changes: 17 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,13 @@ Alternatively, you may prefer to run the remote script directly through

#### Usage

```
````
> molt --help
Usage: molt <modules...>

Description:

Check updates to dependencies in Deno modules
Check updates to dependencies in Deno modules and configuration files

Options:

Expand All @@ -104,56 +104,48 @@ Options:

Examples:

Check updates in a module: molt deps.ts
Check import maps in a config: molt deno.json
Check imports in a module: molt deps.ts
Include multiple modules: molt mod.ts lib.ts
Target all .ts files: molt ./**/*.ts
Specify an import map: molt mod.ts --import-map deno.json
Ignore specific dependencies: molt deps.ts --ignore=deno_graph,node_emoji
Only check deno_std: molt deps.ts --only deno.land/std
```
Ignore specified dependencies: molt deps.ts --ignore=deno_graph,node_emoji
Check deno_std only: molt deps.ts --only deno.land/std

> [!Note]\
> Molt CLI automatically uses import maps defined in `deno.json` or `deno.jsonc`
> if available.\
> You can't, however, use import maps as entrypoints.
> Molt CLI automatically finds `deno.json` or `deno.jsonc` in the current
> working directory or its parent directories and uses import maps defined in
> the file if available.\

#### Examples

##### Check for updates

```sh
> molt mod.ts
> molt deno.json
📦 @luca/flag 1.0.0 => 123.456.789
📦 deno.land/std 0.200.0 => 123.456.789
lib.ts 0.200.0
mod.ts 0.200.0

📦 deno.land/x/deno_graph 0.50.0 => 123.456.789
mod.ts 0.50.0

📦 node-emoji 2.0.0 => 123.456.789
mod.ts 2.0.0
```
📦 node-emoji 1.0.0 => 123.456.789
````

##### Write changes to files

```sh
> molt mod.ts --write
> molt deno.json --write
...
💾 lib.ts
💾 mod.ts
💾 deno.json
```

##### Commit changes to git

```sh
> molt mod.ts --commit --pre-commit=test --prefix :package: --summary title.txt --report report.md
> molt deno.json --commit --prefix :package:
...
📝 :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 2.0.0 to 123.456.789

📄 title.txt
📄 report.md
```

## Compatibility with registries
Expand Down
51 changes: 28 additions & 23 deletions cli.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { distinct, filterKeys, mapEntries } from "./lib/std/collections.ts";
import { parse as parseJsonc } from "./lib/std/jsonc.ts";
import { extname, relative } from "./lib/std/path.ts";
import { relative } from "./lib/std/path.ts";
import { colors, Command } from "./lib/x/cliffy.ts";
import { $ } from "./lib/x/dax.ts";
import { ensure, is } from "./lib/x/unknownutil.ts";
Expand All @@ -19,9 +19,12 @@ const { gray, yellow, bold, cyan } = colors;

const main = new Command()
.name("molt")
.description("Check updates to dependencies in Deno modules")
.description(
"Check updates to dependencies in Deno modules and configuration files",
)
.versionOption("-v, --version", "Print version info.", versionCommand)
.example("Check updates in a module", "molt deps.ts")
.example("Check import maps in a config", "molt deno.json")
.example("Check imports in a module", "molt deps.ts")
.example("Include multiple modules", "molt mod.ts lib.ts")
.example("Target all .ts files", "molt ./**/*.ts")
.option("--import-map <file:string>", "Specify import map file")
Expand Down Expand Up @@ -51,16 +54,16 @@ const main = new Command()
.option("--summary <file:string>", "Write a summary of changes to file")
.option("--report <file:string>", "Write a report of changes to file")
.arguments("<modules...:string>")
.action(async function (options, ...entrypoints) {
.action(async function (options, ...files) {
if (options.importMap) {
if (await $.path(options.importMap).exists() === false) {
console.error(`Import map ${options.importMap} does not exist.`);
Deno.exit(1);
}
}
ensureJsFiles(entrypoints);
const updates = await collectUpdates(entrypoints, options);
printUpdates(updates);
ensureFiles(files);
const updates = await collectUpdates(files, options);
printUpdates(files, updates);
if (options.write) {
return writeUpdates(updates, options);
}
Expand Down Expand Up @@ -102,7 +105,6 @@ async function collectUpdates(
const updates = await Promise.all(
entrypoints.map(async (entrypoint) =>
await collect(entrypoint, {
findImportMap: options.importMap === undefined,
ignore: options.ignore
? (dep) => options.ignore!.some((it) => dep.name.includes(it))
: undefined,
Expand Down Expand Up @@ -149,27 +151,33 @@ async function getTasks() {

const toRelativePath = (path: string) => relative(Deno.cwd(), path);

function printUpdates(updates: DependencyUpdate[]) {
function printUpdates(
files: string[],
updates: DependencyUpdate[],
) {
const dependencies = new Map<string, DependencyUpdate[]>();
for (const u of 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;
for (const [name, list] of dependencies.entries()) {
const froms = distinct(list.map((u) => u.from.version)).join(", ");
console.log(
`📦 ${bold(name)} ${yellow(froms)} => ${yellow(list[0].to.version)}`,
);
distinct(
list.map((u) => {
const source = toRelativePath(u.map?.source ?? u.referrer);
return ` ${source} ` + gray(u.from.version ?? "");
}),
).forEach((line) => console.log(line));
if (++count < dependencies.size) {
console.log();
if (files.length > 1 || nWrites > 1) {
distinct(
list.map((u) => {
const source = toRelativePath(u.map?.source ?? u.referrer);
return ` ${source} ` + gray(u.from.version ?? "");
}),
).forEach((line) => console.log(line));
if (++count < dependencies.size) {
console.log();
}
}
}
}
Expand Down Expand Up @@ -271,17 +279,14 @@ async function runTask([name, args]: [string, string[]]) {
}
}

function ensureJsFiles(paths: string[]) {
function ensureFiles(paths: string[]) {
for (const path of paths) {
if (!["", ".js", ".ts", ".jsx", ".tsx"].includes(extname(path))) {
throw new Error(`❌ file must be javascript or typescript: "${path}"`);
}
try {
if (!Deno.statSync(path).isFile) {
throw new Error(`❌ not a file: "${path}"`);
throw new Error(`Not a valid file: "${path}"`);
}
} catch {
throw new Error(`❌ path does not exist: "${path}"`);
throw new Error(`Path does not exist: "${path}"`);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"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",
"all": "deno fmt && deno lint && deno task -q check && deno task lock && deno task -q test",
"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",
"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",
Expand Down
31 changes: 17 additions & 14 deletions lib/file.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { parse as parseJsonc } from "./std/jsonc.ts";
import { detectEOL } from "./std/fs.ts";
import { toUrl } from "./dependency.ts";
import { type DependencyUpdate } from "./update.ts";
import { ImportMapJson } from "./import_map.ts";
import { readImportMapJson } from "./import_map.ts";

/**
* Write the given array of DependencyUpdate to files.
Expand All @@ -27,16 +26,21 @@ export function writeAll(
return write(associateByFile(updates), options);
}

type FileKind = "module" | "import_map";

/**
* A collection of updates to dependencies associated with a file.
*/
export interface FileUpdate {
/** The full path to the file being updated. */
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: "module" | "import_map";
kind: Kind;
/** The updates to dependencies associated with the file. */
dependencies: DependencyUpdate[];
dependencies: DependencyUpdate<Kind extends "import_map" ? true : false>[];
}

/**
Expand Down Expand Up @@ -83,16 +87,16 @@ function _write(
) {
switch (update.kind) {
case "module":
return writeToModule(update);
return writeToModule(update as FileUpdate<"module">);
case "import_map":
return writeToImportMap(update);
return writeToImportMap(update as FileUpdate<"import_map">);
}
}

async function writeToModule(
update: FileUpdate,
update: FileUpdate<"module">,
) {
const lineToDependencyMap = new Map<number, DependencyUpdate>(
const lineToDependencyMap = new Map(
update.dependencies.map((
dependency,
) => [dependency.code.span.start.line, dependency]),
Expand Down Expand Up @@ -121,12 +125,11 @@ async function writeToModule(

async function writeToImportMap(
/** The dependency update to apply. */
update: FileUpdate,
update: FileUpdate<"import_map">,
) {
const content = await Deno.readTextFile(update.path);
const json = parseJsonc(content) as unknown as ImportMapJson;
const json = await readImportMapJson(update.path);
for (const dependency of update.dependencies) {
json.imports[dependency.map!.key!] = toUrl(dependency.to);
json.imports[dependency.map.key] = toUrl(dependency.to);
}
await Deno.writeTextFile(update.path, JSON.stringify(json, null, 2));
}
2 changes: 1 addition & 1 deletion lib/file_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ WriteTextFileStub.create(fs);
async function test(path: string, name = toName(path)) {
const updates = await DependencyUpdate.collect(
new URL(path, import.meta.url),
{ findImportMap: true },
{ cwd: new URL(dirname(path), import.meta.url) },
);
const results = associateByFile(updates);

Expand Down
25 changes: 20 additions & 5 deletions lib/import_map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@ import { assertEquals } from "./std/assert.ts";
import { maxBy } from "./std/collections.ts";
import { parse as parseJsonc } from "./std/jsonc.ts";
import { type ImportMapJson, parseFromJson } from "./x/import_map.ts";
import { is } from "./x/unknownutil.ts";
import { ensure, is } from "./x/unknownutil.ts";
import { toPath } from "./path.ts";

export type { ImportMapJson };

export interface ImportMapResolveResult {
export interface ImportMapResolveResult<HasKey extends boolean = boolean> {
/** The fully-resolved URL string of the import specifier. */
resolved: string;
/** The key of the import map that matched with the import specifier. */
key?: string;
key: HasKey extends true ? string : undefined;
/** The mapped value by the import map corresponding to the key. */
value?: string;
value: HasKey extends true ? string : undefined;
}

export interface ImportMap {
Expand Down Expand Up @@ -45,6 +45,20 @@ const isImportMapReferrer = is.ObjectOf({
importMap: is.String,
});

/**
* Read and parse a JSON including import maps from the given file path or URL.
*/
export async function readImportMapJson(
url: string | URL,
): Promise<ImportMapJson> {
const data = await Deno.readTextFile(url);
try {
return ensure(parseJsonc(data), isImportMapJson);
} catch {
throw new SyntaxError(`${url} does not have a valid import map`);
}
}

/**
* Read an import map from the given file path or URL.
* @param url - The URL of the import map.
Expand Down Expand Up @@ -91,7 +105,8 @@ export async function readFromJson(
}
return {
resolved,
...replacement,
key: replacement?.key,
value: replacement?.value,
};
},
resolveInner: inner.resolve.bind(inner),
Expand Down
2 changes: 2 additions & 0 deletions lib/import_map_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ describe("resolve()", () => {
{
resolved:
new URL("../test/data/import_map/lib.ts", import.meta.url).href,
key: undefined,
value: undefined,
},
);
});
Expand Down
Loading
Loading