Skip to content

Commit 03e5a14

Browse files
committed
feat: add option to include/exclude dev deps
1 parent 5c25bae commit 03e5a14

7 files changed

Lines changed: 272 additions & 42 deletions

File tree

action.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ inputs:
4444
description: 'Detect modules which have community suggested alternatives'
4545
required: false
4646
default: true
47+
include-dev-deps:
48+
description: 'Include dev dependencies in the analysis'
49+
required: false
50+
default: 'true'
4751
working-directory:
4852
description: 'Working directory to scan for package lock file'
4953
required: false

build/main.js

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24033,11 +24033,23 @@ var supportedLockfiles = [
2403324033
"yarn.lock",
2403424034
"bun.lock"
2403524035
];
24036-
function computeDependencyVersions(lockFile) {
24036+
function computeDependencyVersions(lockFile, includeDevDeps) {
2403724037
const result = /* @__PURE__ */ new Map();
24038-
for (const pkg of lockFile.packages) {
24039-
if (!pkg.name || !pkg.version) continue;
24040-
addVersion(result, pkg.name, pkg.version);
24038+
if (!includeDevDeps) {
24039+
const visitorFn = (node) => {
24040+
if (!node.name || !node.version) return;
24041+
addVersion(result, node.name, node.version);
24042+
};
24043+
traverse(lockFile.root, {
24044+
dependency: visitorFn,
24045+
optionalDependency: visitorFn,
24046+
peerDependency: visitorFn
24047+
});
24048+
} else {
24049+
for (const pkg of lockFile.packages) {
24050+
if (!pkg.name || !pkg.version) continue;
24051+
addVersion(result, pkg.name, pkg.version);
24052+
}
2404124053
}
2404224054
return result;
2404324055
}
@@ -24452,7 +24464,7 @@ function getLsCommand(lockfilePath, packageName) {
2445224464
}
2445324465
return void 0;
2445424466
}
24455-
function computeParentPaths(lockfile, duplicateDependencyNames, dependencyMap) {
24467+
function computeParentPaths(lockfile, duplicateDependencyNames, dependencyMap, includeDevDeps) {
2445624468
const parentPaths = /* @__PURE__ */ new Map();
2445724469
const visitorFn = (node, _parent, path2) => {
2445824470
if (!duplicateDependencyNames.has(node.name) || !path2) {
@@ -24471,13 +24483,13 @@ function computeParentPaths(lockfile, duplicateDependencyNames, dependencyMap) {
2447124483
};
2447224484
const visitor = {
2447324485
dependency: visitorFn,
24474-
devDependency: visitorFn,
24486+
...includeDevDeps ? { devDependency: visitorFn } : {},
2447524487
optionalDependency: visitorFn
2447624488
};
2447724489
traverse(lockfile.root, visitor);
2447824490
return parentPaths;
2447924491
}
24480-
function scanForDuplicates(messages, threshold, dependencyMap, lockfilePath, lockfile) {
24492+
function scanForDuplicates(messages, threshold, dependencyMap, lockfilePath, lockfile, includeDevDeps) {
2448124493
const duplicateRows = [];
2448224494
const duplicateDependencyNames = /* @__PURE__ */ new Set();
2448324495
for (const [packageName, currentVersionSet] of dependencyMap) {
@@ -24491,7 +24503,8 @@ function scanForDuplicates(messages, threshold, dependencyMap, lockfilePath, loc
2449124503
const parentPaths = computeParentPaths(
2449224504
lockfile,
2449324505
duplicateDependencyNames,
24494-
dependencyMap
24506+
dependencyMap,
24507+
includeDevDeps
2449524508
);
2449624509
for (const name of duplicateDependencyNames) {
2449724510
const versionSet = dependencyMap.get(name);
@@ -24820,6 +24833,7 @@ async function run() {
2482024833
const token = getInput("github-token", { required: true });
2482124834
const prNumber = parseInt(getInput("pr-number", { required: true }), 10);
2482224835
const detectReplacements = getBooleanInput("detect-replacements");
24836+
const includeDevDeps = getBooleanInput("include-dev-deps");
2482324837
const dependencyThreshold = parseInt(
2482424838
getInput("dependency-threshold") || "10",
2482524839
10
@@ -24906,8 +24920,11 @@ async function run() {
2490624920
setFailed(`Failed to parse base lockfile: ${err}`);
2490724921
return;
2490824922
}
24909-
const currentDeps = computeDependencyVersions(parsedCurrentLock);
24910-
const baseDeps = computeDependencyVersions(parsedBaseLock);
24923+
const currentDeps = computeDependencyVersions(
24924+
parsedCurrentLock,
24925+
includeDevDeps
24926+
);
24927+
const baseDeps = computeDependencyVersions(parsedBaseLock, includeDevDeps);
2491124928
info(`Dependency threshold set to ${dependencyThreshold}`);
2491224929
info(`Size threshold set to ${formatBytes(sizeThreshold)}`);
2491324930
info(`Duplicate threshold set to ${duplicateThreshold}`);
@@ -24924,7 +24941,8 @@ async function run() {
2492424941
duplicateThreshold,
2492524942
currentDeps,
2492624943
lockfilePath,
24927-
parsedCurrentLock
24944+
parsedCurrentLock,
24945+
includeDevDeps
2492824946
);
2492924947
await scanForDependencySize(
2493024948
messages,
@@ -24964,15 +24982,14 @@ async function run() {
2496424982
);
2496524983
return;
2496624984
}
24967-
const baseDependencies = getDependenciesFromPackageJson(basePackageJson, [
24968-
"optional",
24969-
"peer",
24970-
"dev",
24971-
"prod"
24972-
]);
24985+
const depTypes = includeDevDeps ? ["optional", "peer", "dev", "prod"] : ["optional", "peer", "prod"];
24986+
const baseDependencies = getDependenciesFromPackageJson(
24987+
basePackageJson,
24988+
depTypes
24989+
);
2497324990
const currentDependencies = getDependenciesFromPackageJson(
2497424991
currentPackageJson,
24975-
["optional", "peer", "dev", "prod"]
24992+
depTypes
2497624993
);
2497724994
scanForReplacements(messages, baseDependencies, currentDependencies);
2497824995
}

src/checks/duplicates.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ function getLsCommand(
2222
function computeParentPaths(
2323
lockfile: ParsedLockFile,
2424
duplicateDependencyNames: Set<string>,
25-
dependencyMap: Map<string, Set<string>>
25+
dependencyMap: Map<string, Set<string>>,
26+
includeDevDeps: boolean
2627
): Map<string, string[]> {
2728
const parentPaths = new Map<string, string[]>();
2829

@@ -43,7 +44,7 @@ function computeParentPaths(
4344
};
4445
const visitor = {
4546
dependency: visitorFn,
46-
devDependency: visitorFn,
47+
...(includeDevDeps ? {devDependency: visitorFn} : {}),
4748
optionalDependency: visitorFn
4849
};
4950

@@ -57,7 +58,8 @@ export function scanForDuplicates(
5758
threshold: number,
5859
dependencyMap: Map<string, Set<string>>,
5960
lockfilePath: string,
60-
lockfile: ParsedLockFile
61+
lockfile: ParsedLockFile,
62+
includeDevDeps: boolean
6163
): void {
6264
const duplicateRows: string[] = [];
6365
const duplicateDependencyNames = new Set<string>();
@@ -75,7 +77,8 @@ export function scanForDuplicates(
7577
const parentPaths = computeParentPaths(
7678
lockfile,
7779
duplicateDependencyNames,
78-
dependencyMap
80+
dependencyMap,
81+
includeDevDeps
7982
);
8083

8184
for (const name of duplicateDependencyNames) {

src/lockfile.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {ParsedLockFile} from 'lockparse';
1+
import {type ParsedLockFile, traverse, type VisitorFn} from 'lockparse';
22
import {existsSync} from 'node:fs';
33
import {join} from 'node:path';
44

@@ -12,13 +12,26 @@ export const supportedLockfiles = [
1212
] as const;
1313

1414
export function computeDependencyVersions(
15-
lockFile: ParsedLockFile
15+
lockFile: ParsedLockFile,
16+
includeDevDeps: boolean
1617
): VersionsSet {
1718
const result: VersionsSet = new Map();
1819

19-
for (const pkg of lockFile.packages) {
20-
if (!pkg.name || !pkg.version) continue;
21-
addVersion(result, pkg.name, pkg.version);
20+
if (!includeDevDeps) {
21+
const visitorFn: VisitorFn = (node) => {
22+
if (!node.name || !node.version) return;
23+
addVersion(result, node.name, node.version);
24+
};
25+
traverse(lockFile.root, {
26+
dependency: visitorFn,
27+
optionalDependency: visitorFn,
28+
peerDependency: visitorFn
29+
});
30+
} else {
31+
for (const pkg of lockFile.packages) {
32+
if (!pkg.name || !pkg.version) continue;
33+
addVersion(result, pkg.name, pkg.version);
34+
}
2235
}
2336

2437
return result;

src/main.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {join} from 'node:path';
66
import {parse as parseLockfile, type ParsedLockFile} from 'lockparse';
77
import {detectLockfile, computeDependencyVersions} from './lockfile.js';
88
import {getFileFromRef, getBaseRef, tryGetJSONFromRef} from './git.js';
9-
import {getDependenciesFromPackageJson} from './npm.js';
9+
import {getDependenciesFromPackageJson, type DependencyType} from './npm.js';
1010
import {getPacksFromPattern} from './packs.js';
1111
import {scanForReplacements} from './checks/replacements.js';
1212
import {scanForDuplicates} from './checks/duplicates.js';
@@ -34,6 +34,7 @@ async function run(): Promise<void> {
3434
const token = core.getInput('github-token', {required: true});
3535
const prNumber = parseInt(core.getInput('pr-number', {required: true}), 10);
3636
const detectReplacements = core.getBooleanInput('detect-replacements');
37+
const includeDevDeps = core.getBooleanInput('include-dev-deps');
3738
const dependencyThreshold = parseInt(
3839
core.getInput('dependency-threshold') || '10',
3940
10
@@ -129,8 +130,11 @@ async function run(): Promise<void> {
129130
return;
130131
}
131132

132-
const currentDeps = computeDependencyVersions(parsedCurrentLock);
133-
const baseDeps = computeDependencyVersions(parsedBaseLock);
133+
const currentDeps = computeDependencyVersions(
134+
parsedCurrentLock,
135+
includeDevDeps
136+
);
137+
const baseDeps = computeDependencyVersions(parsedBaseLock, includeDevDeps);
134138

135139
core.info(`Dependency threshold set to ${dependencyThreshold}`);
136140
core.info(`Size threshold set to ${formatBytes(sizeThreshold)}`);
@@ -150,7 +154,8 @@ async function run(): Promise<void> {
150154
duplicateThreshold,
151155
currentDeps,
152156
lockfilePath,
153-
parsedCurrentLock
157+
parsedCurrentLock,
158+
includeDevDeps
154159
);
155160

156161
await scanForDependencySize(
@@ -198,15 +203,19 @@ async function run(): Promise<void> {
198203
return;
199204
}
200205

201-
const baseDependencies = getDependenciesFromPackageJson(basePackageJson, [
206+
const depTypes: DependencyType[] = [
202207
'optional',
203208
'peer',
204-
'dev',
209+
...(includeDevDeps ? (['dev'] as const) : []),
205210
'prod'
206-
]);
211+
];
212+
const baseDependencies = getDependenciesFromPackageJson(
213+
basePackageJson,
214+
depTypes
215+
);
207216
const currentDependencies = getDependenciesFromPackageJson(
208217
currentPackageJson,
209-
['optional', 'peer', 'dev', 'prod']
218+
depTypes
210219
);
211220

212221
scanForReplacements(messages, baseDependencies, currentDependencies);

test/checks/duplicates_test.ts

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ describe('scanForDuplicates', () => {
2929
threshold,
3030
dependencyMap,
3131
lockfilePath,
32-
lockfile
32+
lockfile,
33+
true
3334
);
3435

3536
expect(messages).toHaveLength(0);
@@ -85,7 +86,8 @@ describe('scanForDuplicates', () => {
8586
threshold,
8687
dependencyMap,
8788
lockfilePath,
88-
lockfile
89+
lockfile,
90+
true
8991
);
9092

9193
expect(messages).toMatchSnapshot();
@@ -141,12 +143,67 @@ describe('scanForDuplicates', () => {
141143
threshold,
142144
dependencyMap,
143145
lockfilePath,
144-
lockfile
146+
lockfile,
147+
true
145148
);
146149

147150
expect(messages).toHaveLength(0);
148151
});
149152

153+
it('should exclude dev dependency duplicates when includeDevDeps is false', () => {
154+
const messages: string[] = [];
155+
const threshold = 1;
156+
const packageA: ParsedDependency = {
157+
name: 'package-a',
158+
version: '1.0.0',
159+
dependencies: [],
160+
devDependencies: [],
161+
optionalDependencies: [],
162+
peerDependencies: []
163+
};
164+
const packageAAlt: ParsedDependency = {
165+
name: 'package-a',
166+
version: '1.1.0',
167+
dependencies: [],
168+
devDependencies: [],
169+
optionalDependencies: [],
170+
peerDependencies: []
171+
};
172+
const dependencyMap = new Map<string, Set<string>>([
173+
['package-a', new Set(['1.0.0', '1.1.0'])]
174+
]);
175+
const lockfilePath = 'package-lock.json';
176+
const lockfile: ParsedLockFile = {
177+
type: 'npm',
178+
packages: [packageA, packageAAlt],
179+
root: {
180+
name: 'root-package',
181+
version: '1.0.0',
182+
dependencies: [packageA],
183+
devDependencies: [packageAAlt],
184+
optionalDependencies: [],
185+
peerDependencies: []
186+
}
187+
};
188+
189+
scanForDuplicates(
190+
messages,
191+
threshold,
192+
dependencyMap,
193+
lockfilePath,
194+
lockfile,
195+
false
196+
);
197+
198+
// With includeDevDeps=false, the devDependency path for packageAAlt should not be traversed
199+
// The duplicate is still reported (since dependencyMap has both versions),
200+
// but parent paths for the dev-only version won't be found
201+
expect(messages).toHaveLength(1);
202+
expect(messages[0]).toContain('package-a');
203+
// The prod version should have a parent path, the dev version should not
204+
expect(messages[0]).toContain('package-a@1.0.0');
205+
});
206+
150207
it('should truncate long parent paths in the report', () => {
151208
const messages: string[] = [];
152209
const threshold = 1;
@@ -204,7 +261,8 @@ describe('scanForDuplicates', () => {
204261
threshold,
205262
dependencyMap,
206263
lockfilePath,
207-
lockfile
264+
lockfile,
265+
true
208266
);
209267

210268
expect(messages).toMatchSnapshot();

0 commit comments

Comments
 (0)