Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 0 additions & 15 deletions .eslintrc.json

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/node.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
21 changes: 21 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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 <from> <to>` (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.
52 changes: 51 additions & 1 deletion __tests__/index.test.js
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -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');
});
});
11 changes: 11 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default [
{
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
rules: {
'no-param-reassign': 'off',
},
},
];
64 changes: 31 additions & 33 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)`;
Expand All @@ -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);
}
}