Skip to content

Commit 137392e

Browse files
committed
refactor(plugin-js-packages): option to provide package json path
1 parent ef5c2ee commit 137392e

File tree

10 files changed

+71
-113
lines changed

10 files changed

+71
-113
lines changed

.verdaccio/config.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,7 @@ log:
3535

3636
publish:
3737
allow_offline: true # set offline to true to allow publish offline
38+
39+
middlewares:
40+
audit:
41+
enabled: true # needed to run npm audit in e2e test folder
Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
1+
import { dirname, join } from 'node:path';
2+
import { fileURLToPath } from 'node:url';
13
import jsPackagesPlugin from '@code-pushup/js-packages-plugin';
24
import type { CoreConfig } from '@code-pushup/models';
35

6+
const thisConfigFolder = fileURLToPath(dirname(import.meta.url));
7+
48
export default {
5-
persist: { outputDir: './' },
6-
plugins: [await jsPackagesPlugin({ packageManager: 'npm' })],
9+
persist: { outputDir: thisConfigFolder, format: ['json'] },
10+
plugins: [
11+
await jsPackagesPlugin({
12+
packageManager: 'npm',
13+
packageJsonPath: join(thisConfigFolder, 'package.json'),
14+
}),
15+
],
716
} satisfies CoreConfig;

e2e/plugin-js-packages-e2e/tests/plugin-js-packages.e2e.test.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ import {
77
reportSchema,
88
} from '@code-pushup/models';
99
import { nxTargetProject } from '@code-pushup/test-nx-utils';
10-
import { teardownTestFolder } from '@code-pushup/test-setup';
11-
import { E2E_ENVIRONMENTS_DIR, TEST_OUTPUT_DIR } from '@code-pushup/test-utils';
10+
import {
11+
E2E_ENVIRONMENTS_DIR,
12+
TEST_OUTPUT_DIR,
13+
teardownTestFolder,
14+
} from '@code-pushup/test-utils';
1215
import { executeProcess, readJsonFile } from '@code-pushup/utils';
1316

1417
describe('plugin-js-packages', () => {
@@ -37,13 +40,19 @@ describe('plugin-js-packages', () => {
3740

3841
it('should run JS packages plugin for NPM and create report.json', async () => {
3942
const { code } = await executeProcess({
40-
command: 'node',
43+
command: 'npx',
4144
args: [
42-
'../../node_modules/@code-pushup/cli/src/index.js',
45+
'@code-pushup/cli',
4346
'collect',
47+
'--verbose',
4448
'--no-progress',
49+
`--config=${path.join(
50+
TEST_OUTPUT_DIR,
51+
'npm-repo',
52+
'code-pushup.config.ts',
53+
)}`,
4554
],
46-
cwd: npmRepoDir,
55+
cwd: path.join(E2E_ENVIRONMENTS_DIR, nxTargetProject()),
4756
});
4857

4958
expect(code).toBe(0);
@@ -89,8 +98,9 @@ describe('plugin-js-packages', () => {
8998

9099
const expressOutdatedIssue = npmOutdatedProd.details!.issues![0]!;
91100
expect(expressOutdatedIssue.severity).toBe('error');
92-
expect(expressOutdatedIssue.message).toMatch(
93-
/^Package \[`express`]\(http:\/\/expressjs\.com\/?\) requires a \*\*major\*\* update from \*\*3.0.0\*\* to \*\*\d+\.\d+\.\d+\*\*\.$/,
101+
expect(expressOutdatedIssue?.message).toContain('express');
102+
expect(expressOutdatedIssue?.message).toContain(
103+
'requires a **major** update from **3.0.0** to',
94104
);
95105

96106
expect(() => reportSchema.parse(report)).not.toThrow();

packages/plugin-js-packages/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ The plugin accepts the following parameters:
113113
- `packageManager`: The package manager you are using. Supported values: `npm`, `yarn-classic` (v1), `yarn-modern` (v2+), `pnpm`.
114114
- (optional) `checks`: Array of checks to be run. Supported commands: `audit`, `outdated`. Both are configured by default.
115115
- (optional) `dependencyGroups`: Array of dependency groups to be checked. `prod` and `dev` are configured by default. `optional` are opt-in.
116-
- (optional) `packageJsonPaths`: File path(s) to `package.json`. Root `package.json` is used by default. Multiple `package.json` paths may be passed. If `{ autoSearch: true }` is provided, all `package.json` files in the repository are searched.
116+
- (optional) `packageJsonPath`: File path to `package.json`. Root `package.json` at CWD is used by default.
117117
- (optional) `auditLevelMapping`: If you wish to set a custom level of issue severity based on audit vulnerability level, you may do so here. Any omitted values will be filled in by defaults. Audit levels are: `critical`, `high`, `moderate`, `low` and `info`. Issue severities are: `error`, `warn` and `info`. By default the mapping is as follows: `critical` and `high``error`; `moderate` and `low``warning`; `info``info`.
118118

119119
### Audits and group

packages/plugin-js-packages/src/lib/config.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,14 @@ const packageManagerIdSchema = z.enum([
1818
export type PackageManagerId = z.infer<typeof packageManagerIdSchema>;
1919

2020
const packageJsonPathSchema = z
21-
.union([
22-
z.array(z.string()).min(1),
23-
z.object({ autoSearch: z.literal(true) }),
24-
])
21+
.string()
22+
.regex(/package\.json$/, 'File path must end with package.json')
2523
.describe(
26-
'File paths to package.json. Looks only at root package.json by default',
24+
'File path to package.json, tries to use root package.json at CWD by default',
2725
)
28-
.default(['package.json']);
26+
.default('package.json');
2927

30-
export type PackageJsonPaths = z.infer<typeof packageJsonPathSchema>;
28+
export type PackageJsonPath = z.infer<typeof packageJsonPathSchema>;
3129

3230
export const packageAuditLevels = [
3331
'critical',
@@ -75,7 +73,7 @@ export const jsPackagesPluginConfigSchema = z.object({
7573
})
7674
.default(defaultAuditLevelMapping)
7775
.transform(fillAuditLevelMapping),
78-
packageJsonPaths: packageJsonPathSchema,
76+
packageJsonPath: packageJsonPathSchema,
7977
});
8078

8179
export type JSPackagesPluginConfig = z.input<

packages/plugin-js-packages/src/lib/config.unit.test.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ describe('jsPackagesPluginConfigSchema', () => {
1515
checks: ['audit'],
1616
packageManager: 'yarn-classic',
1717
dependencyGroups: ['prod'],
18-
packageJsonPaths: ['./ui-app/package.json', './ui-e2e/package.json'],
18+
packageJsonPath: './ui-app/package.json',
1919
} satisfies JSPackagesPluginConfig),
2020
).not.toThrow();
2121
});
@@ -36,7 +36,7 @@ describe('jsPackagesPluginConfigSchema', () => {
3636
checks: ['audit', 'outdated'],
3737
packageManager: 'npm',
3838
dependencyGroups: ['prod', 'dev'],
39-
packageJsonPaths: ['package.json'],
39+
packageJsonPath: 'package.json',
4040
auditLevelMapping: {
4141
critical: 'error',
4242
high: 'error',
@@ -47,15 +47,6 @@ describe('jsPackagesPluginConfigSchema', () => {
4747
});
4848
});
4949

50-
it('should accept auto search for package.json files', () => {
51-
expect(() =>
52-
jsPackagesPluginConfigSchema.parse({
53-
packageManager: 'yarn-classic',
54-
packageJsonPaths: { autoSearch: true },
55-
} satisfies JSPackagesPluginConfig),
56-
).not.toThrow();
57-
});
58-
5950
it('should throw for no passed commands', () => {
6051
expect(() =>
6152
jsPackagesPluginConfigSchema.parse({

packages/plugin-js-packages/src/lib/runner/index.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
type AuditSeverity,
1717
type DependencyGroup,
1818
type FinalJSPackagesPluginConfig,
19-
type PackageJsonPaths,
19+
type PackageJsonPath,
2020
type PackageManagerId,
2121
dependencyGroups,
2222
} from '../config.js';
@@ -25,7 +25,7 @@ import { packageManagers } from '../package-managers/package-managers.js';
2525
import { auditResultToAuditOutput } from './audit/transform.js';
2626
import type { AuditResult } from './audit/types.js';
2727
import { outdatedResultToAuditOutput } from './outdated/transform.js';
28-
import { findAllPackageJson, getTotalDependencies } from './utils.js';
28+
import { getTotalDependencies } from './utils.js';
2929

3030
export async function createRunnerConfig(
3131
scriptPath: string,
@@ -55,16 +55,21 @@ export async function executeRunner({
5555
packageManager,
5656
checks,
5757
auditLevelMapping,
58-
packageJsonPaths,
58+
packageJsonPath,
5959
dependencyGroups: depGroups,
6060
} = await readJsonFile<FinalJSPackagesPluginConfig>(runnerConfigPath);
6161

6262
const auditResults = checks.includes('audit')
63-
? await processAudit(packageManager, depGroups, auditLevelMapping)
63+
? await processAudit(
64+
packageManager,
65+
depGroups,
66+
auditLevelMapping,
67+
packageJsonPath,
68+
)
6469
: [];
6570

6671
const outdatedResults = checks.includes('outdated')
67-
? await processOutdated(packageManager, depGroups, packageJsonPaths)
72+
? await processOutdated(packageManager, depGroups, packageJsonPath)
6873
: [];
6974
const checkResults = [...auditResults, ...outdatedResults];
7075

@@ -75,21 +80,17 @@ export async function executeRunner({
7580
async function processOutdated(
7681
id: PackageManagerId,
7782
depGroups: DependencyGroup[],
78-
packageJsonPaths: PackageJsonPaths,
83+
packageJsonPath: PackageJsonPath,
7984
) {
8085
const pm = packageManagers[id];
8186
const { stdout } = await executeProcess({
8287
command: pm.command,
8388
args: pm.outdated.commandArgs,
84-
cwd: process.cwd(),
89+
cwd: packageJsonPath ? path.dirname(packageJsonPath) : process.cwd(),
8590
ignoreExitCode: true, // outdated returns exit code 1 when outdated dependencies are found
8691
});
8792

88-
// Locate all package.json files in the repository if not provided
89-
const finalPaths = Array.isArray(packageJsonPaths)
90-
? packageJsonPaths
91-
: await findAllPackageJson();
92-
const depTotals = await getTotalDependencies(finalPaths);
93+
const depTotals = await getTotalDependencies(packageJsonPath);
9394

9495
const normalizedResult = pm.outdated.unifyResult(stdout);
9596
return depGroups.map(depGroup =>
@@ -106,6 +107,7 @@ async function processAudit(
106107
id: PackageManagerId,
107108
depGroups: DependencyGroup[],
108109
auditLevelMapping: AuditSeverity,
110+
packageJsonPath: PackageJsonPath,
109111
) {
110112
const pm = packageManagers[id];
111113
const supportedAuditDepGroups =
@@ -120,7 +122,7 @@ async function processAudit(
120122
const { stdout } = await executeProcess({
121123
command: pm.command,
122124
args: pm.audit.getCommandArgs(depGroup),
123-
cwd: process.cwd(),
125+
cwd: packageJsonPath ? path.dirname(packageJsonPath) : process.cwd(),
124126
ignoreExitCode: pm.audit.ignoreExitCode,
125127
});
126128
return [depGroup, pm.audit.unifyResult(stdout)];

packages/plugin-js-packages/src/lib/runner/runner.integration.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ describe('createRunnerConfig', () => {
1212
checks: ['audit'],
1313
auditLevelMapping: defaultAuditLevelMapping,
1414
dependencyGroups: ['prod', 'dev'],
15-
packageJsonPaths: ['package.json'],
15+
packageJsonPath: 'package.json',
1616
});
1717
expect(runnerConfig).toStrictEqual<RunnerConfig>({
1818
command: 'node',
@@ -32,7 +32,7 @@ describe('createRunnerConfig', () => {
3232
checks: ['outdated'],
3333
dependencyGroups: ['prod', 'dev'],
3434
auditLevelMapping: { ...defaultAuditLevelMapping, moderate: 'error' },
35-
packageJsonPaths: ['package.json'],
35+
packageJsonPath: 'package.json',
3636
};
3737
const { configFile } = await createRunnerConfig(
3838
'executeRunner.ts',

packages/plugin-js-packages/src/lib/runner/utils.ts

Lines changed: 8 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
1-
import path from 'node:path';
21
import {
3-
crawlFileSystem,
42
objectFromEntries,
53
objectToKeys,
64
readJsonFile,
75
} from '@code-pushup/utils';
86
import type { AuditResult, Vulnerability } from './audit/types.js';
97
import {
10-
type DependencyGroupLong,
118
type DependencyTotals,
129
type PackageJson,
1310
dependencyGroupLong,
@@ -54,41 +51,18 @@ export function filterAuditResult(
5451
};
5552
}
5653

57-
// TODO: use .gitignore
58-
export async function findAllPackageJson(): Promise<string[]> {
59-
return (
60-
await crawlFileSystem({
61-
directory: '.',
62-
pattern: /(^|[\\/])package\.json$/,
63-
})
64-
).filter(
65-
filePath =>
66-
!filePath.startsWith(`node_modules${path.sep}`) &&
67-
!filePath.includes(`${path.sep}node_modules${path.sep}`) &&
68-
!filePath.startsWith(`.nx${path.sep}`),
69-
);
70-
}
71-
7254
export async function getTotalDependencies(
73-
packageJsonPaths: string[],
55+
packageJsonPath: string,
7456
): Promise<DependencyTotals> {
75-
const parsedDeps = await Promise.all(
76-
packageJsonPaths.map(readJsonFile<PackageJson>),
77-
);
57+
const parsedDeps = await readJsonFile<PackageJson>(packageJsonPath);
7858

79-
const mergedDeps = parsedDeps.reduce<Record<DependencyGroupLong, string[]>>(
80-
(acc, depMapper) =>
81-
objectFromEntries(
82-
dependencyGroupLong.map(group => {
83-
const deps = depMapper[group];
84-
return [
85-
group,
86-
[...acc[group], ...(deps == null ? [] : objectToKeys(deps))],
87-
];
88-
}),
89-
),
90-
{ dependencies: [], devDependencies: [], optionalDependencies: [] },
59+
const mergedDeps = objectFromEntries(
60+
dependencyGroupLong.map(group => {
61+
const deps = parsedDeps[group];
62+
return [group, deps == null ? [] : objectToKeys(deps)];
63+
}),
9164
);
65+
9266
return objectFromEntries(
9367
objectToKeys(mergedDeps).map(deps => [
9468
deps,

packages/plugin-js-packages/src/lib/runner/utils.unit.test.ts

Lines changed: 5 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,7 @@ import { describe, expect, it } from 'vitest';
44
import { MEMFS_VOLUME } from '@code-pushup/test-utils';
55
import type { AuditResult, Vulnerability } from './audit/types.js';
66
import type { DependencyTotals, PackageJson } from './outdated/types.js';
7-
import {
8-
filterAuditResult,
9-
findAllPackageJson,
10-
getTotalDependencies,
11-
} from './utils.js';
12-
13-
describe('findAllPackageJson', () => {
14-
beforeEach(() => {
15-
vol.fromJSON(
16-
{
17-
'package.json': '',
18-
[path.join('ui', 'package.json')]: '',
19-
[path.join('ui', 'ng-package.json')]: '', // non-exact file match should be excluded
20-
[path.join('.nx', 'cache', 'ui', 'package.json')]: '', // nx cache should be excluded
21-
[path.join('node_modules', 'eslint', 'package.json')]: '', // root node_modules should be excluded
22-
[path.join('ui', 'node_modules', 'eslint', 'package.json')]: '', // project node_modules should be excluded
23-
},
24-
MEMFS_VOLUME,
25-
);
26-
});
27-
28-
it('should return all valid package.json files (exclude .nx, node_modules)', async () => {
29-
await expect(findAllPackageJson()).resolves.toEqual([
30-
'package.json',
31-
path.join('ui', 'package.json'),
32-
]);
33-
});
34-
});
7+
import { filterAuditResult, getTotalDependencies } from './utils.js';
358

369
describe('getTotalDependencies', () => {
3710
beforeEach(() => {
@@ -64,23 +37,20 @@ describe('getTotalDependencies', () => {
6437

6538
it('should return correct number of dependencies', async () => {
6639
await expect(
67-
getTotalDependencies([path.join(MEMFS_VOLUME, 'package.json')]),
40+
getTotalDependencies(path.join(MEMFS_VOLUME, 'package.json')),
6841
).resolves.toStrictEqual({
6942
dependencies: 1,
7043
devDependencies: 3,
7144
optionalDependencies: 0,
7245
} satisfies DependencyTotals);
7346
});
7447

75-
it('should merge dependencies for multiple package.json files', async () => {
48+
it('should return dependencies for nested package.json file', async () => {
7649
await expect(
77-
getTotalDependencies([
78-
path.join(MEMFS_VOLUME, 'package.json'),
79-
path.join(MEMFS_VOLUME, 'ui', 'package.json'),
80-
]),
50+
getTotalDependencies(path.join(MEMFS_VOLUME, 'ui', 'package.json')),
8151
).resolves.toStrictEqual({
8252
dependencies: 2,
83-
devDependencies: 4,
53+
devDependencies: 1,
8454
optionalDependencies: 1,
8555
} satisfies DependencyTotals);
8656
});

0 commit comments

Comments
 (0)