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/.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 }} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4cb73bc --- /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, `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 `diff()` and `format()`. +- `data/` — Fixture lockfiles (lodash version variants) for testing. diff --git a/__tests__/index.test.js b/__tests__/index.test.js index 5ac59da..fe50533 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,53 @@ 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 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); + }); + + 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/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..1847fb5 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,35 @@ 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) { + const output = format(changes, options); + if (output) { + console.log(output); } }