Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
coverage
lib
node_modules
patchpulse.config.json
STREAMING_TODO.md
3 changes: 0 additions & 3 deletions .patchpulse.config.json

This file was deleted.

2 changes: 1 addition & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
lib
node_modules
.patchpulse.config.json.example
patchpulse.config.json.example
48 changes: 42 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "patch-pulse",
"version": "2.6.1",
"version": "2.7.0",
"description": "Check for outdated npm dependencies",
"type": "module",
"bin": {
Expand Down Expand Up @@ -58,7 +58,7 @@
},
"devDependencies": {
"@eslint/js": "9.29.0",
"@types/node": "24.0.3",
"@types/node": "24.0.4",
"@typescript-eslint/eslint-plugin": "8.35.0",
"@typescript-eslint/parser": "8.35.0",
"@vitest/coverage-v8": "3.2.4",
Expand Down
File renamed without changes.
34 changes: 22 additions & 12 deletions src/core/dependency-checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,25 +29,29 @@ export async function checkDependencyVersions(

for (let i = 0; i < packageNames.length; i += concurrencyLimit) {
const batch = packageNames.slice(i, i + concurrencyLimit);
const batchPromises = batch.map(async name => {
const version = dependencies[name];
const batchPromises = batch.map(async packageName => {
const version = dependencies[packageName];

// Check if package should be skipped
const isSkipped = config ? shouldSkipPackage(name, config) : false;
const isSkipped = shouldSkipPackage({ packageName, config });

let latestVersion: string | undefined;
let isOutdated = false;
let updateType: 'patch' | 'minor' | 'major' | undefined;

if (!isSkipped) {
latestVersion = await getLatestVersion(name);
isOutdated = latestVersion
? isVersionOutdated(version, latestVersion)
: false;
updateType =
latestVersion && isOutdated
latestVersion = await getLatestVersion(packageName);

// Don't try to compare versions if current version is not a standard semver
const isStandardSemver = /^\d+\.\d+\.\d+/.test(version);
if (!isStandardSemver) {
isOutdated = false;
updateType = undefined;
} else if (latestVersion) {
isOutdated = isVersionOutdated(version, latestVersion);
updateType = isOutdated
? getUpdateType(version, latestVersion)
: undefined;
}
}

// Update progress for each completed package
Expand All @@ -57,7 +61,7 @@ export async function checkDependencyVersions(
);

return {
name,
packageName,
currentVersion: version,
latestVersion,
isOutdated,
Expand Down Expand Up @@ -88,6 +92,12 @@ function displayResults(dependencyInfos: DependencyInfo[]): void {
} else if (!dep.latestVersion) {
status = chalk.red('NOT FOUND');
versionInfo = `${dep.currentVersion} (not found on npm registry)`;
} else if (dep.currentVersion === 'latest') {
status = chalk.cyan('LATEST TAG');
versionInfo = `latest → ${chalk.cyan(dep.latestVersion)} (actual latest version)`;
} else if (!/^\d+\.\d+\.\d+/.test(dep.currentVersion)) {
status = chalk.blue('VERSION RANGE');
versionInfo = `${dep.currentVersion} → ${chalk.cyan(dep.latestVersion)} (latest available)`;
} else if (dep.isOutdated) {
const updateTypeColor = {
major: chalk.yellow,
Expand All @@ -102,7 +112,7 @@ function displayResults(dependencyInfos: DependencyInfo[]): void {
}

console.log(
`${status} ${chalk.white(dep.name)} ${chalk.gray(versionInfo)}`
`${status} ${chalk.white(dep.packageName)} ${chalk.gray(versionInfo)}`
);
}
}
2 changes: 1 addition & 1 deletion src/gen/version.gen.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// Auto-generated file - do not edit manually
export const VERSION = '2.6.1';
export const VERSION = '2.7.0';
11 changes: 2 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,7 @@
import chalk from 'chalk';
import { join } from 'path';
import { checkDependencyVersions } from './core/dependency-checker';
import {
mergeConfigs,
parseCliConfig,
readConfigFile,
} from './services/config';
import { getConfig } from './services/config';
import { checkForCliUpdate } from './services/npm';
import { readPackageJson } from './services/package';
import { type DependencyInfo } from './types';
Expand All @@ -32,10 +28,7 @@ async function main(): Promise<void> {
const packageJson = await readPackageJson(packageJsonPath);
const allDependencies: DependencyInfo[] = [];

// Read configuration
const fileConfig = readConfigFile();
const cliConfig = parseCliConfig(process.argv.slice(2));
const config = mergeConfigs(fileConfig, cliConfig);
const config = getConfig();

const dependencyTypeLabels: Record<string, string> = {
dependencies: 'Dependencies',
Expand Down
83 changes: 65 additions & 18 deletions src/services/__tests__/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { existsSync, readFileSync } from 'fs';
import { join } from 'path';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import {
getConfig,
mergeConfigs,
parseCliConfig,
readConfigFile,
Expand All @@ -23,10 +24,17 @@ describe('Configuration Service', () => {
vi.clearAllMocks();
});

describe('getConfig', () => {
it('should return the config', () => {
const config = getConfig();
expect(config).toEqual({ skip: [] });
});
});

describe('readConfigFile', () => {
it('should return null when no config file exists', () => {
vi.mocked(existsSync).mockReturnValue(false);
vi.mocked(join).mockReturnValue('/test/.patchpulse.config.json');
vi.mocked(join).mockReturnValue('/test/patchpulse.config.json');

const result = readConfigFile('/test');

Expand Down Expand Up @@ -114,7 +122,7 @@ describe('Configuration Service', () => {
const result = mergeConfigs(fileConfig, cliConfig);

expect(result).toEqual({
skip: ['express', 'test-*'],
skip: ['lodash', '@types/*', 'express', 'test-*'],
});
});

Expand Down Expand Up @@ -149,50 +157,89 @@ describe('Configuration Service', () => {
skip: ['lodash', 'express'],
};

expect(shouldSkipPackage('lodash', config)).toBe(true);
expect(shouldSkipPackage('express', config)).toBe(true);
expect(shouldSkipPackage('chalk', config)).toBe(false);
expect(shouldSkipPackage({ packageName: 'lodash', config })).toBe(true);
expect(shouldSkipPackage({ packageName: 'express', config })).toBe(true);
expect(shouldSkipPackage({ packageName: 'chalk', config })).toBe(false);
});

it('should skip packages matching patterns', () => {
const config: PatchPulseConfig = {
skip: ['@types/*', 'test-*'],
};

expect(shouldSkipPackage('@types/node', config)).toBe(true);
expect(shouldSkipPackage('test-utils', config)).toBe(true);
expect(shouldSkipPackage('@typescript-eslint/parser', config)).toBe(
false
);
expect(shouldSkipPackage('chalk', config)).toBe(false);
expect(
shouldSkipPackage({
packageName: '@types/node',
config,
})
).toBe(true);
expect(
shouldSkipPackage({
packageName: 'test-utils',
config,
})
).toBe(true);
expect(
shouldSkipPackage({
packageName: '@typescript-eslint/parser',
config,
})
).toBe(false);
expect(shouldSkipPackage({ packageName: 'chalk', config })).toBe(false);
});

it('should handle invalid regex patterns gracefully', () => {
const config: PatchPulseConfig = {
skip: ['[invalid-regex'],
};

expect(shouldSkipPackage('test-package', config)).toBe(false);
expect(
shouldSkipPackage({
packageName: 'test-package',
config,
})
).toBe(false);
});

it('should check both exact matches and patterns', () => {
const config: PatchPulseConfig = {
skip: ['lodash', '@types/*'],
};

expect(shouldSkipPackage('lodash', config)).toBe(true);
expect(shouldSkipPackage('@types/node', config)).toBe(true);
expect(shouldSkipPackage('chalk', config)).toBe(false);
expect(shouldSkipPackage({ packageName: 'lodash', config })).toBe(true);
expect(
shouldSkipPackage({
packageName: '@types/node',
config,
})
).toBe(true);
expect(shouldSkipPackage({ packageName: 'chalk', config })).toBe(false);
});

it('should treat patterns without regex chars as exact matches', () => {
const config: PatchPulseConfig = {
skip: ['lodash', 'test-package'],
};

expect(shouldSkipPackage('lodash', config)).toBe(true);
expect(shouldSkipPackage('test-package', config)).toBe(true);
expect(shouldSkipPackage('test-package-extra', config)).toBe(false);
expect(shouldSkipPackage({ packageName: 'lodash', config })).toBe(true);
expect(
shouldSkipPackage({
packageName: 'test-package',
config,
})
).toBe(true);
expect(
shouldSkipPackage({
packageName: 'test-package-extra',
config,
})
).toBe(false);
});
});

it('should handle no defined config skip parameter', () => {
expect(shouldSkipPackage({ packageName: 'lodash', config: {} })).toBe(
false
);
});
});
Loading