From 5cf21d4d6f34319015d6142248349a16b35b2074 Mon Sep 17 00:00:00 2001 From: Olaf Alders Date: Tue, 7 Apr 2026 03:01:45 +0000 Subject: [PATCH 1/5] Add CLAUDE.md with development commands and architecture overview Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..250d644 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,21 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What This Is + +`diff-lockfiles` is a CLI tool that diffs `package-lock.json` files across git commit ranges, showing which dependencies changed versions. Fork of [lock-diff](https://github.com/mxweaver/lock-diff). ES module project (`"type": "module"`). + +## Commands + +- **Test:** `NODE_OPTIONS=--experimental-vm-modules npm test` +- **Run single test:** `NODE_OPTIONS=--experimental-vm-modules npx jest __tests__/index.test.js` +- **Lint:** `npm run lint` +- **Run CLI:** `./bin/diff-lockfiles.js ` (e.g., `./bin/diff-lockfiles.js HEAD~1 HEAD`) + +## Architecture + +- `bin/diff-lockfiles.js` — CLI entry point using Commander. Runs `git diff` to find changed lockfiles between two refs, parses them via `git show`, then calls `diff()` and `print()`. +- `lib/index.js` — Core logic. Exports `diff(oldLock, newLock, shallow)` which compares `.packages` entries using semver, and `print(changes, options)` which formats output as table, json, markdown, or text. Color support via chalk. +- `__tests__/index.test.js` — Jest tests for the `diff()` function. +- `data/` — Fixture lockfiles (lodash version variants) for testing. From 076e227e38f1a0f579ce3978b416ba3fe5adb4d8 Mon Sep 17 00:00:00 2001 From: Olaf Alders Date: Tue, 7 Apr 2026 04:01:41 +0000 Subject: [PATCH 2/5] Add format() export and migrate to ESLint 9 flat config Refactor print functions to return strings via new format() export, enabling programmatic use (e.g. in GitHub Actions). print() now delegates to format() and remains backward compatible. Replace .eslintrc.json with eslint.config.js for ESLint 9 support. Co-Authored-By: Claude Opus 4.6 --- .eslintrc.json | 15 ------------ eslint.config.js | 11 +++++++++ lib/index.js | 61 ++++++++++++++++++++++-------------------------- 3 files changed, 39 insertions(+), 48 deletions(-) delete mode 100644 .eslintrc.json create mode 100644 eslint.config.js diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 71b1354..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "env": { - "node": true, - "es6": true, - "jest": true - }, - "rules": { - "no-param-reassign": "off" - }, - "parser": "babel-eslint", - "parserOptions": { - "sourceType": "module", - "allowImportExportEverywhere": true - } -} diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..9314f65 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,11 @@ +export default [ + { + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + rules: { + 'no-param-reassign': 'off', + }, + }, +]; diff --git a/lib/index.js b/lib/index.js index 61100af..35b06e6 100644 --- a/lib/index.js +++ b/lib/index.js @@ -39,45 +39,42 @@ export function diff(oldLock, newLock, shallow) { return changes; } -function printJSON(changes) { - /* eslint-disable no-console */ - console.log(JSON.stringify(changes)); +function formatJSON(changes) { + return JSON.stringify(changes); } -function printText(changes, options) { - /* eslint-disable no-console */ +function formatText(changes, options) { + const lines = []; Object.entries(changes).forEach(([name, [oldVersion, newVersion]]) => { if (!oldVersion) { if (options.color) { - console.log(`${name} ${chalk.green('added')}`); + lines.push(`${name} ${chalk.green('added')}`); } else { - console.log(`${name} added`); + lines.push(`${name} added`); } } else if (!newVersion) { if (options.color) { - console.log(`${name} ${chalk.red('removed')}`); + lines.push(`${name} ${chalk.red('removed')}`); } else { - console.log(`${name} removed`); + lines.push(`${name} removed`); } } else if (!semver.eq(oldVersion, newVersion)) { if (options.color) { const color = semver.gt(oldVersion, newVersion) ? chalk.red : chalk.green; - console.log(`${name} ${color(`${oldVersion} -> ${newVersion}`)}`); + lines.push(`${name} ${color(`${oldVersion} -> ${newVersion}`)}`); } else { - console.log(`${name} ${oldVersion} -> ${newVersion}`); + lines.push(`${name} ${oldVersion} -> ${newVersion}`); } } }); - /* eslint-enable no-console */ + return lines.join('\n'); } -function printTable(changes, options) { - /* eslint-disable no-console */ - +function formatTable(changes, options) { let data = Object.entries(changes) .map(([name, [oldVersion, newVersion]]) => ([ name, @@ -110,14 +107,10 @@ function printTable(changes, options) { data[0] = data[0].map((heading) => chalk.bold(heading)); } - console.log(table(data)); - - /* eslint-disable no-console */ + return table(data); } -function printMarkdown(changes, options) { - /* eslint-disable no-console */ - +function formatMarkdown(changes, options) { // Helper function to format version changes with markdown emphasis function formatVersionChange(oldVersion, newVersion) { if (!oldVersion) return `**${newVersion}** (added)`; @@ -144,30 +137,32 @@ function printMarkdown(changes, options) { ]) ]; + const parts = []; + // Add title if provided if (options.title && options.title !== '') { - console.log(`## ${options.title}\n`); + parts.push(`## ${options.title}\n`); } - console.log(markdownTable(tableData)); + parts.push(markdownTable(tableData)); - /* eslint-enable no-console */ + return parts.join('\n'); } -export function print(changes, options) { +export function format(changes, options) { switch (options.format) { case 'json': - printJSON(changes, options); - break; + return formatJSON(changes, options); case 'table': - printTable(changes, options); - break; + return formatTable(changes, options); case 'markdown': - printMarkdown(changes, options); - break; + return formatMarkdown(changes, options); case 'text': default: - printText(changes, options); - break; + return formatText(changes, options); } } + +export function print(changes, options) { + console.log(format(changes, options)); +} From 176faafd47062b445a2eccd8a37fd0a06d14ee8b Mon Sep 17 00:00:00 2001 From: Olaf Alders Date: Tue, 7 Apr 2026 04:04:14 +0000 Subject: [PATCH 3/5] Add tests for format(), fix empty-changes behavior, update CLAUDE.md - Add tests covering all format() output modes (json, text, markdown, table) plus edge cases (empty changes, title, default format) - Fix print() to not emit blank line when changes are empty - Document format() export in CLAUDE.md Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 2 +- __tests__/index.test.js | 48 ++++++++++++++++++++++++++++++++++++++++- lib/index.js | 5 ++++- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 250d644..7cabdf1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,6 +16,6 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Architecture - `bin/diff-lockfiles.js` — CLI entry point using Commander. Runs `git diff` to find changed lockfiles between two refs, parses them via `git show`, then calls `diff()` and `print()`. -- `lib/index.js` — Core logic. Exports `diff(oldLock, newLock, shallow)` which compares `.packages` entries using semver, and `print(changes, options)` which formats output as table, json, markdown, or text. Color support via chalk. +- `lib/index.js` — Core logic. Exports `diff(oldLock, newLock, shallow)` which compares `.packages` entries using semver, `format(changes, options)` which returns formatted strings (table, json, markdown, or text), and `print(changes, options)` which formats and writes to stdout. Color support via chalk. - `__tests__/index.test.js` — Jest tests for the `diff()` function. - `data/` — Fixture lockfiles (lodash version variants) for testing. diff --git a/__tests__/index.test.js b/__tests__/index.test.js index 5ac59da..d23320d 100644 --- a/__tests__/index.test.js +++ b/__tests__/index.test.js @@ -1,4 +1,4 @@ -import { diff } from '../lib/index.js'; +import { diff, format } from '../lib/index.js'; describe('diff', () => { it('returns an empty object when given two empty objects', () => { @@ -15,3 +15,49 @@ describe('diff', () => { expect(changes).toEqual({}); }); }); + +describe('format', () => { + const changes = { + 'node_modules/foo': ['1.0.0', '2.0.0'], + 'node_modules/bar': [null, '1.0.0'], + 'node_modules/baz': ['3.0.0', null], + }; + + it('returns empty string for empty changes with text format', () => { + expect(format({}, { format: 'text' })).toBe(''); + }); + + it('returns JSON string for json format', () => { + const result = format(changes, { format: 'json' }); + expect(JSON.parse(result)).toEqual(changes); + }); + + it('returns text with arrows for text format', () => { + const result = format(changes, { format: 'text', color: false }); + expect(result).toContain('node_modules/foo 1.0.0 -> 2.0.0'); + expect(result).toContain('node_modules/bar added'); + expect(result).toContain('node_modules/baz removed'); + }); + + it('returns markdown table for markdown format', () => { + const result = format(changes, { format: 'markdown' }); + expect(result).toContain('| Package'); + expect(result).toContain('node_modules/foo'); + }); + + it('returns table for table format', () => { + const result = format(changes, { format: 'table', title: '', color: false }); + expect(result).toContain('package'); + expect(result).toContain('node_modules/foo'); + }); + + it('includes title in markdown format', () => { + const result = format(changes, { format: 'markdown', title: 'package-lock.json' }); + expect(result).toContain('## package-lock.json'); + }); + + it('defaults to text format', () => { + const result = format(changes, { format: 'unknown', color: false }); + expect(result).toContain('node_modules/foo 1.0.0 -> 2.0.0'); + }); +}); diff --git a/lib/index.js b/lib/index.js index 35b06e6..1847fb5 100644 --- a/lib/index.js +++ b/lib/index.js @@ -164,5 +164,8 @@ export function format(changes, options) { } export function print(changes, options) { - console.log(format(changes, options)); + const output = format(changes, options); + if (output) { + console.log(output); + } } From 20d775b5189a55e954c718ee8c7eb410f39adfd4 Mon Sep 17 00:00:00 2001 From: Olaf Alders Date: Tue, 7 Apr 2026 14:11:40 +0000 Subject: [PATCH 4/5] Add empty JSON test case and update CLAUDE.md test description Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 2 +- __tests__/index.test.js | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7cabdf1..4cb73bc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,5 +17,5 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - `bin/diff-lockfiles.js` — CLI entry point using Commander. Runs `git diff` to find changed lockfiles between two refs, parses them via `git show`, then calls `diff()` and `print()`. - `lib/index.js` — Core logic. Exports `diff(oldLock, newLock, shallow)` which compares `.packages` entries using semver, `format(changes, options)` which returns formatted strings (table, json, markdown, or text), and `print(changes, options)` which formats and writes to stdout. Color support via chalk. -- `__tests__/index.test.js` — Jest tests for the `diff()` function. +- `__tests__/index.test.js` — Jest tests for `diff()` and `format()`. - `data/` — Fixture lockfiles (lodash version variants) for testing. diff --git a/__tests__/index.test.js b/__tests__/index.test.js index d23320d..fe50533 100644 --- a/__tests__/index.test.js +++ b/__tests__/index.test.js @@ -27,6 +27,10 @@ describe('format', () => { expect(format({}, { format: 'text' })).toBe(''); }); + it('returns empty object string for empty changes with json format', () => { + expect(format({}, { format: 'json' })).toBe('{}'); + }); + it('returns JSON string for json format', () => { const result = format(changes, { format: 'json' }); expect(JSON.parse(result)).toEqual(changes); From bbcb86d89aca33bfc8065d175479e56878b89f77 Mon Sep 17 00:00:00 2001 From: Olaf Alders Date: Tue, 7 Apr 2026 14:30:02 +0000 Subject: [PATCH 5/5] Add Node 22 and 24 to CI test matrix, drop EOL Node 18 Co-Authored-By: Claude Opus 4.6 --- .github/workflows/node.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml index 1b9cb25..819c557 100644 --- a/.github/workflows/node.yml +++ b/.github/workflows/node.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [18.x, 20.x] + node-version: [20.x, 22.x, 24.x] steps: - uses: actions/checkout@v5 - name: Use Node.js ${{ matrix.node-version }}