diff --git a/.verdaccio/config.yml b/.verdaccio/config.yml index 3e73d2132..0fabb539f 100644 --- a/.verdaccio/config.yml +++ b/.verdaccio/config.yml @@ -35,3 +35,7 @@ log: publish: allow_offline: true # set offline to true to allow publish offline + +middlewares: + audit: + enabled: true # needed to run npm audit in e2e test folder diff --git a/e2e/plugin-js-packages-e2e/eslint.config.js b/e2e/plugin-js-packages-e2e/eslint.config.js new file mode 100644 index 000000000..2656b27cb --- /dev/null +++ b/e2e/plugin-js-packages-e2e/eslint.config.js @@ -0,0 +1,12 @@ +import tseslint from 'typescript-eslint'; +import baseConfig from '../../eslint.config.js'; + +export default tseslint.config(...baseConfig, { + files: ['**/*.ts'], + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, +}); diff --git a/e2e/plugin-js-packages-e2e/mocks/fixtures/npm-repo/code-pushup.config.ts b/e2e/plugin-js-packages-e2e/mocks/fixtures/npm-repo/code-pushup.config.ts new file mode 100644 index 000000000..aba57ced5 --- /dev/null +++ b/e2e/plugin-js-packages-e2e/mocks/fixtures/npm-repo/code-pushup.config.ts @@ -0,0 +1,16 @@ +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import jsPackagesPlugin from '@code-pushup/js-packages-plugin'; +import type { CoreConfig } from '@code-pushup/models'; + +const thisConfigFolder = fileURLToPath(dirname(import.meta.url)); + +export default { + persist: { outputDir: thisConfigFolder, format: ['json'] }, + plugins: [ + await jsPackagesPlugin({ + packageManager: 'npm', + packageJsonPath: join(thisConfigFolder, 'package.json'), + }), + ], +} satisfies CoreConfig; diff --git a/e2e/plugin-js-packages-e2e/mocks/fixtures/npm-repo/package-lock.json b/e2e/plugin-js-packages-e2e/mocks/fixtures/npm-repo/package-lock.json new file mode 100644 index 000000000..2e0aca30f --- /dev/null +++ b/e2e/plugin-js-packages-e2e/mocks/fixtures/npm-repo/package-lock.json @@ -0,0 +1,191 @@ +{ + "name": "npm-repo", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "express": "3.0.0" + } + }, + "node_modules/bytes": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-0.1.0.tgz", + "integrity": "sha512-zTSmfpu7b+Mll4T9ZjTYUO3Q6+m+F3ZEQ515ZECaAFhmmHiRl/UcdcAsuFyVklbMRo9GWyRyqTsB6C6ahjGnVA==" + }, + "node_modules/commander": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", + "integrity": "sha512-0fLycpl1UMTGX257hRsu/arL/cUbcvQM4zMKwvLvzXtfdezIV4yotPS2dYtknF+NmEfWSoCEF6+hj9XLm/6hEw==", + "engines": { + "node": ">= 0.4.x" + } + }, + "node_modules/connect": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-2.6.0.tgz", + "integrity": "sha512-DKQC8Lf4WordoUU+GKP2DkTx3j+G2S+pXCOeZMQ/K0MCoShSuE9HB4ZvZQOQCqnAqUImOdcR9l+p0wvaxN7Riw==", + "deprecated": "connect 2.x series is deprecated", + "dependencies": { + "bytes": "0.1.0", + "cookie": "0.0.4", + "crc": "0.2.0", + "debug": "*", + "formidable": "1.0.11", + "fresh": "0.1.0", + "pause": "0.0.1", + "qs": "0.5.1", + "send": "0.0.4" + }, + "engines": { + "node": ">= 0.5.0" + } + }, + "node_modules/connect/node_modules/send": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/send/-/send-0.0.4.tgz", + "integrity": "sha512-weKMWbrKdW7kqeHbk1IWf+u25CqLx1xrqhDrRUV02yW5BNzUp783GRxgxziFWH3QGrQPMvR5/DTUN9RuO2u9ew==", + "dependencies": { + "debug": "*", + "fresh": "0.1.0", + "mime": "1.2.6", + "range-parser": "0.0.4" + } + }, + "node_modules/cookie": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.0.4.tgz", + "integrity": "sha512-K4/8ihPVK55g3atBFCLcDWzHnrqZBawwjQnRGZ9A4Erg/uOmZY8b9n/tssKt4odxq3eK0HTQT6NVgtKvLSoKEg==", + "engines": { + "node": "*" + } + }, + "node_modules/crc": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-0.2.0.tgz", + "integrity": "sha512-LFlOXOW6KT46bjpUevoixE6UQVdm9wMwCrR4JHxg4LJ+9COF7efwTdVMRXrSlNXYmUQgtAcHsWa0VgKBiQZmMQ==", + "engines": { + "node": "*" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/express": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/express/-/express-3.0.0.tgz", + "integrity": "sha512-77v5ENowsy0mmT/bY0Z5iXID3JZUtWrgVnVjewaznQLFYQdvel74XiM/hhttrKYEcLoxKsG/HjYFk1rk5Wecqg==", + "dependencies": { + "commander": "0.6.1", + "connect": "2.6.0", + "cookie": "0.0.4", + "crc": "0.2.0", + "debug": "*", + "fresh": "0.1.0", + "methods": "0.0.1", + "mkdirp": "0.3.3", + "range-parser": "0.0.4", + "send": "0.1.0" + }, + "bin": { + "express": "bin/express" + }, + "engines": { + "node": "*" + } + }, + "node_modules/formidable": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.0.11.tgz", + "integrity": "sha512-ZG3xz6afuCmpLGNtTI/W8HDKWisPv/iZgtEvfB1nF3vJHDJ2M4hpU/HDLJQYnTVqErpaLphweqOMULwP/Ls6cg==", + "deprecated": "Please upgrade to latest, formidable@v2 or formidable@v3! Check these notes: https://bit.ly/2ZEqIau", + "engines": { + "node": "*" + } + }, + "node_modules/fresh": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.1.0.tgz", + "integrity": "sha512-ROG9M8tikYOuOJsvRBggh10WiQ/JebnldAwuCaQyFoiAUIE9XrYVnpznIjOQGZfCMzxzEBYHQr/LHJp3tcndzQ==", + "engines": { + "node": "*" + } + }, + "node_modules/methods": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/methods/-/methods-0.0.1.tgz", + "integrity": "sha512-pB8oFfci/xcfUgM6DTxc7lbTKifPPgs3mZUOsEgaH+1TTWpmcmv3sHl+5sUHIj2X2W8aPYa2+nJealRHK+Lo6A==", + "license": "MIT" + }, + "node_modules/mime": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.6.tgz", + "integrity": "sha512-S4yfg1ehMduQ5F3NeTUUWJesnut4RvymaRSatO4etOm68yZE98oCg2GtgG0coGYx03GCv240sezMvRwFk8DUKw==", + "engines": { + "node": "*" + } + }, + "node_modules/mkdirp": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.3.tgz", + "integrity": "sha512-Oamd41MnZw/yuxtarGf3MFbHzFqQY4S17DcN+rATh2t5MKuCtG7vVVRG+RUT6g9+hr47DIVucIHGOUlwmJRvDA==", + "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, + "node_modules/qs": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-0.5.1.tgz", + "integrity": "sha512-1NhhAEZMTI+2tQrOAGFlS1HFmKCcI9mvsysUbfqvvz6ObXwxCvPuAqzD+5LYBbEfjrdSOakWzaZx4wFPnND+xA==", + "engines": { + "node": "*" + } + }, + "node_modules/range-parser": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-0.0.4.tgz", + "integrity": "sha512-okJVEq9DbZyg+5lD8pr6ooQmeA0uu8DYIyAU7VK1WUUK7hctI1yw2ZHhKiKjB6RXaDrYRmTR4SsIHkyiQpaLMA==", + "engines": { + "node": "*" + } + }, + "node_modules/send": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.1.0.tgz", + "integrity": "sha512-D/GaJQQYx7ICNq9Te5V4wHetfDQdFk3HJ4oBfDUBNW7XQmLbJ8sQDm/wFvVUUpKN8tluOnO1dFdM8KODn6D79w==", + "dependencies": { + "debug": "*", + "fresh": "0.1.0", + "mime": "1.2.6", + "range-parser": "0.0.4" + } + } + } +} diff --git a/e2e/plugin-js-packages-e2e/mocks/fixtures/npm-repo/package.json b/e2e/plugin-js-packages-e2e/mocks/fixtures/npm-repo/package.json new file mode 100644 index 000000000..c36917449 --- /dev/null +++ b/e2e/plugin-js-packages-e2e/mocks/fixtures/npm-repo/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "express": "3.0.0" + } +} diff --git a/e2e/plugin-js-packages-e2e/project.json b/e2e/plugin-js-packages-e2e/project.json new file mode 100644 index 000000000..edbd1594d --- /dev/null +++ b/e2e/plugin-js-packages-e2e/project.json @@ -0,0 +1,23 @@ +{ + "name": "plugin-js-packages-e2e", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "projectType": "application", + "sourceRoot": "e2e/plugin-js-packages-e2e/src", + "implicitDependencies": ["cli", "plugin-js-packages"], + "targets": { + "lint": { + "executor": "@nx/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["e2e/plugin-eslint-e2e/**/*.ts"] + } + }, + "e2e": { + "executor": "@nx/vite:test", + "options": { + "configFile": "e2e/plugin-eslint-e2e/vite.config.e2e.ts" + } + } + }, + "tags": ["scope:plugin", "type:e2e"] +} diff --git a/e2e/plugin-js-packages-e2e/tests/plugin-js-packages.e2e.test.ts b/e2e/plugin-js-packages-e2e/tests/plugin-js-packages.e2e.test.ts new file mode 100644 index 000000000..5e884aaa5 --- /dev/null +++ b/e2e/plugin-js-packages-e2e/tests/plugin-js-packages.e2e.test.ts @@ -0,0 +1,108 @@ +import { cp } from 'node:fs/promises'; +import path from 'node:path'; +import { afterAll, beforeAll, expect, it } from 'vitest'; +import { + type AuditReport, + type Report, + reportSchema, +} from '@code-pushup/models'; +import { nxTargetProject } from '@code-pushup/test-nx-utils'; +import { + E2E_ENVIRONMENTS_DIR, + TEST_OUTPUT_DIR, + teardownTestFolder, +} from '@code-pushup/test-utils'; +import { executeProcess, readJsonFile } from '@code-pushup/utils'; + +describe('plugin-js-packages', () => { + const fixturesDir = path.join( + 'e2e', + 'plugin-js-packages-e2e', + 'mocks', + 'fixtures', + ); + const fixturesNPMDir = path.join(fixturesDir, 'npm-repo'); + + const envRoot = path.join( + E2E_ENVIRONMENTS_DIR, + nxTargetProject(), + TEST_OUTPUT_DIR, + ); + const npmRepoDir = path.join(envRoot, 'npm-repo'); + + beforeAll(async () => { + await cp(fixturesNPMDir, npmRepoDir, { recursive: true }); + }); + + afterAll(async () => { + await teardownTestFolder(npmRepoDir); + }); + + it('should run JS packages plugin for NPM and create report.json', async () => { + const { code } = await executeProcess({ + command: 'npx', + args: [ + '@code-pushup/cli', + 'collect', + '--verbose', + '--no-progress', + `--config=${path.join( + TEST_OUTPUT_DIR, + 'npm-repo', + 'code-pushup.config.ts', + )}`, + ], + cwd: path.join(E2E_ENVIRONMENTS_DIR, nxTargetProject()), + }); + + expect(code).toBe(0); + + const report = await readJsonFile( + path.join(npmRepoDir, 'report.json'), + ); + + const plugin = report.plugins[0]!; + const npmAuditProd = plugin.audits.find( + ({ slug }) => slug === 'npm-audit-prod', + )!; + const npmOutdatedProd = plugin.audits.find( + ({ slug }) => slug === 'npm-outdated-prod', + )!; + + expect(plugin.packageName).toBe('@code-pushup/js-packages-plugin'); + expect(plugin.audits).toHaveLength(4); + + expect(npmAuditProd).toEqual( + expect.objectContaining({ + value: expect.any(Number), + }), + ); + expect(npmAuditProd.value).toBeGreaterThanOrEqual(7); // there are 7 vulnerabilities (6 high, 1 low) in prod dependency at the time + expect(npmAuditProd.displayValue).toMatch(/\d vulnerabilities/); + expect(npmAuditProd.details?.issues!.length).toBeGreaterThanOrEqual(7); + + const expressConnectIssue = npmAuditProd.details!.issues![0]!; + expect(expressConnectIssue?.severity).toBe('error'); + expect(expressConnectIssue?.message).toContain('express'); + expect(expressConnectIssue?.message).toContain('2.30.2'); + expect(expressConnectIssue?.message).toContain( + 'methodOverride Middleware Reflected Cross-Site Scripting in connect', + ); + + expect(npmOutdatedProd.score).toBe(0); + expect(npmOutdatedProd.value).toBe(1); // there is 1 outdated prod dependency at the time + expect(npmOutdatedProd.displayValue).toBe( + '1 major outdated package version', + ); + expect(npmOutdatedProd.details?.issues).toHaveLength(1); + + const expressOutdatedIssue = npmOutdatedProd.details!.issues![0]!; + expect(expressOutdatedIssue.severity).toBe('error'); + expect(expressOutdatedIssue?.message).toContain('express'); + expect(expressOutdatedIssue?.message).toContain( + 'requires a **major** update from **3.0.0** to', + ); + + expect(() => reportSchema.parse(report)).not.toThrow(); + }); +}); diff --git a/e2e/plugin-js-packages-e2e/tsconfig.json b/e2e/plugin-js-packages-e2e/tsconfig.json new file mode 100644 index 000000000..f5a2f890a --- /dev/null +++ b/e2e/plugin-js-packages-e2e/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "ESNext", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "types": ["vitest"] + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.test.json" + } + ] +} diff --git a/e2e/plugin-js-packages-e2e/tsconfig.test.json b/e2e/plugin-js-packages-e2e/tsconfig.test.json new file mode 100644 index 000000000..34f35e30f --- /dev/null +++ b/e2e/plugin-js-packages-e2e/tsconfig.test.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": ["vitest/globals", "vitest/importMeta", "vite/client", "node"], + "target": "ES2020" + }, + "exclude": ["__test-env__/**"], + "include": [ + "vite.config.e2e.ts", + "tests/**/*.e2e.test.ts", + "tests/**/*.d.ts", + "mocks/**/*.ts" + ] +} diff --git a/e2e/plugin-js-packages-e2e/vite.config.e2e.ts b/e2e/plugin-js-packages-e2e/vite.config.e2e.ts new file mode 100644 index 000000000..8b8475ed1 --- /dev/null +++ b/e2e/plugin-js-packages-e2e/vite.config.e2e.ts @@ -0,0 +1,21 @@ +/// +import { defineConfig } from 'vite'; +import { tsconfigPathAliases } from '../../tools/vitest-tsconfig-path-aliases.js'; + +export default defineConfig({ + cacheDir: '../../node_modules/.vite/plugin-js-packages-e2e', + test: { + reporters: ['basic'], + testTimeout: 120_000, + globals: true, + alias: tsconfigPathAliases(), + pool: 'threads', + poolOptions: { threads: { singleThread: true } }, + cache: { + dir: '../../node_modules/.vitest', + }, + environment: 'node', + include: ['tests/**/*.e2e.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + setupFiles: ['../../testing/test-setup/src/lib/reset.mocks.ts'], + }, +}); diff --git a/packages/plugin-js-packages/README.md b/packages/plugin-js-packages/README.md index 0bc12de9f..41f78b27b 100644 --- a/packages/plugin-js-packages/README.md +++ b/packages/plugin-js-packages/README.md @@ -113,7 +113,7 @@ The plugin accepts the following parameters: - `packageManager`: The package manager you are using. Supported values: `npm`, `yarn-classic` (v1), `yarn-modern` (v2+), `pnpm`. - (optional) `checks`: Array of checks to be run. Supported commands: `audit`, `outdated`. Both are configured by default. - (optional) `dependencyGroups`: Array of dependency groups to be checked. `prod` and `dev` are configured by default. `optional` are opt-in. -- (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. +- (optional) `packageJsonPath`: File path to `package.json`. Root `package.json` at CWD is used by default. - (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`. ### Audits and group diff --git a/packages/plugin-js-packages/src/lib/config.ts b/packages/plugin-js-packages/src/lib/config.ts index 8a8f4a75a..75d6c247b 100644 --- a/packages/plugin-js-packages/src/lib/config.ts +++ b/packages/plugin-js-packages/src/lib/config.ts @@ -18,16 +18,14 @@ const packageManagerIdSchema = z.enum([ export type PackageManagerId = z.infer; const packageJsonPathSchema = z - .union([ - z.array(z.string()).min(1), - z.object({ autoSearch: z.literal(true) }), - ]) + .string() + .regex(/package\.json$/, 'File path must end with package.json') .describe( - 'File paths to package.json. Looks only at root package.json by default', + 'File path to package.json, tries to use root package.json at CWD by default', ) - .default(['package.json']); + .default('package.json'); -export type PackageJsonPaths = z.infer; +export type PackageJsonPath = z.infer; export const packageAuditLevels = [ 'critical', @@ -75,7 +73,7 @@ export const jsPackagesPluginConfigSchema = z.object({ }) .default(defaultAuditLevelMapping) .transform(fillAuditLevelMapping), - packageJsonPaths: packageJsonPathSchema, + packageJsonPath: packageJsonPathSchema, }); export type JSPackagesPluginConfig = z.input< diff --git a/packages/plugin-js-packages/src/lib/config.unit.test.ts b/packages/plugin-js-packages/src/lib/config.unit.test.ts index e6e871734..0b80823af 100644 --- a/packages/plugin-js-packages/src/lib/config.unit.test.ts +++ b/packages/plugin-js-packages/src/lib/config.unit.test.ts @@ -15,7 +15,7 @@ describe('jsPackagesPluginConfigSchema', () => { checks: ['audit'], packageManager: 'yarn-classic', dependencyGroups: ['prod'], - packageJsonPaths: ['./ui-app/package.json', './ui-e2e/package.json'], + packageJsonPath: './ui-app/package.json', } satisfies JSPackagesPluginConfig), ).not.toThrow(); }); @@ -36,7 +36,7 @@ describe('jsPackagesPluginConfigSchema', () => { checks: ['audit', 'outdated'], packageManager: 'npm', dependencyGroups: ['prod', 'dev'], - packageJsonPaths: ['package.json'], + packageJsonPath: 'package.json', auditLevelMapping: { critical: 'error', high: 'error', @@ -47,15 +47,6 @@ describe('jsPackagesPluginConfigSchema', () => { }); }); - it('should accept auto search for package.json files', () => { - expect(() => - jsPackagesPluginConfigSchema.parse({ - packageManager: 'yarn-classic', - packageJsonPaths: { autoSearch: true }, - } satisfies JSPackagesPluginConfig), - ).not.toThrow(); - }); - it('should throw for no passed commands', () => { expect(() => jsPackagesPluginConfigSchema.parse({ diff --git a/packages/plugin-js-packages/src/lib/package-managers/npm/outdated-result.ts b/packages/plugin-js-packages/src/lib/package-managers/npm/outdated-result.ts index ebb6973b7..c064b0bd0 100644 --- a/packages/plugin-js-packages/src/lib/package-managers/npm/outdated-result.ts +++ b/packages/plugin-js-packages/src/lib/package-managers/npm/outdated-result.ts @@ -1,21 +1,16 @@ import { objectToEntries } from '@code-pushup/utils'; import type { OutdatedResult } from '../../runner/outdated/types.js'; -import type { NpmNormalizedOverview, NpmOutdatedResultJson } from './types.js'; +import type { NpmOutdatedResultJson } from './types.js'; export function npmToOutdatedResult(output: string): OutdatedResult { const npmOutdated = JSON.parse(output) as NpmOutdatedResultJson; - // current might be missing in some cases + // "current" might be missing in some cases, usually it is missing if the dependency is not installed, fallback to "wanted" should avoid this problem // https://stackoverflow.com/questions/42267101/npm-outdated-command-shows-missing-in-current-version - return objectToEntries(npmOutdated) - .filter( - (entry): entry is [string, NpmNormalizedOverview] => - entry[1].current != null, - ) - .map(([name, overview]) => ({ - name, - current: overview.current, - latest: overview.latest, - type: overview.type, - ...(overview.homepage != null && { url: overview.homepage }), - })); + return objectToEntries(npmOutdated).map(([name, overview]) => ({ + name, + current: overview.current || overview.wanted, + latest: overview.latest, + type: overview.type, + ...(overview.homepage != null && { url: overview.homepage }), + })); } diff --git a/packages/plugin-js-packages/src/lib/package-managers/npm/outdated-result.unit.test.ts b/packages/plugin-js-packages/src/lib/package-managers/npm/outdated-result.unit.test.ts index 0810e08de..d67a40103 100644 --- a/packages/plugin-js-packages/src/lib/package-managers/npm/outdated-result.unit.test.ts +++ b/packages/plugin-js-packages/src/lib/package-managers/npm/outdated-result.unit.test.ts @@ -39,7 +39,7 @@ describe('npmToOutdatedResult', () => { ]); }); - it('should skip dependencies without current version', () => { + it('should not skip dependencies without current version', () => { expect( npmToOutdatedResult( JSON.stringify({ @@ -50,7 +50,14 @@ describe('npmToOutdatedResult', () => { }, }), ), - ).toEqual([]); + ).toEqual([ + { + current: undefined, + latest: '5.3.0', + name: 'typescript', + type: 'dependencies', + }, + ]); }); it('should transform no dependencies to empty array', () => { diff --git a/packages/plugin-js-packages/src/lib/package-managers/npm/types.ts b/packages/plugin-js-packages/src/lib/package-managers/npm/types.ts index e0525ef23..f7775532e 100644 --- a/packages/plugin-js-packages/src/lib/package-managers/npm/types.ts +++ b/packages/plugin-js-packages/src/lib/package-managers/npm/types.ts @@ -36,13 +36,10 @@ export type NpmAuditResultJson = { // Subset of NPM outdated JSON type export type NpmVersionOverview = { current?: string; + wanted: string; latest: string; type: DependencyGroupLong; homepage?: string; }; -export type NpmNormalizedOverview = Omit & { - current: string; -}; - export type NpmOutdatedResultJson = Record; diff --git a/packages/plugin-js-packages/src/lib/runner/index.ts b/packages/plugin-js-packages/src/lib/runner/index.ts index f8bca562d..98fc1047f 100644 --- a/packages/plugin-js-packages/src/lib/runner/index.ts +++ b/packages/plugin-js-packages/src/lib/runner/index.ts @@ -16,7 +16,7 @@ import { type AuditSeverity, type DependencyGroup, type FinalJSPackagesPluginConfig, - type PackageJsonPaths, + type PackageJsonPath, type PackageManagerId, dependencyGroups, } from '../config.js'; @@ -25,7 +25,7 @@ import { packageManagers } from '../package-managers/package-managers.js'; import { auditResultToAuditOutput } from './audit/transform.js'; import type { AuditResult } from './audit/types.js'; import { outdatedResultToAuditOutput } from './outdated/transform.js'; -import { findAllPackageJson, getTotalDependencies } from './utils.js'; +import { getTotalDependencies } from './utils.js'; export async function createRunnerConfig( scriptPath: string, @@ -55,16 +55,21 @@ export async function executeRunner({ packageManager, checks, auditLevelMapping, - packageJsonPaths, + packageJsonPath, dependencyGroups: depGroups, } = await readJsonFile(runnerConfigPath); const auditResults = checks.includes('audit') - ? await processAudit(packageManager, depGroups, auditLevelMapping) + ? await processAudit( + packageManager, + depGroups, + auditLevelMapping, + packageJsonPath, + ) : []; const outdatedResults = checks.includes('outdated') - ? await processOutdated(packageManager, depGroups, packageJsonPaths) + ? await processOutdated(packageManager, depGroups, packageJsonPath) : []; const checkResults = [...auditResults, ...outdatedResults]; @@ -75,21 +80,17 @@ export async function executeRunner({ async function processOutdated( id: PackageManagerId, depGroups: DependencyGroup[], - packageJsonPaths: PackageJsonPaths, + packageJsonPath: PackageJsonPath, ) { const pm = packageManagers[id]; const { stdout } = await executeProcess({ command: pm.command, args: pm.outdated.commandArgs, - cwd: process.cwd(), + cwd: packageJsonPath ? path.dirname(packageJsonPath) : process.cwd(), ignoreExitCode: true, // outdated returns exit code 1 when outdated dependencies are found }); - // Locate all package.json files in the repository if not provided - const finalPaths = Array.isArray(packageJsonPaths) - ? packageJsonPaths - : await findAllPackageJson(); - const depTotals = await getTotalDependencies(finalPaths); + const depTotals = await getTotalDependencies(packageJsonPath); const normalizedResult = pm.outdated.unifyResult(stdout); return depGroups.map(depGroup => @@ -106,6 +107,7 @@ async function processAudit( id: PackageManagerId, depGroups: DependencyGroup[], auditLevelMapping: AuditSeverity, + packageJsonPath: PackageJsonPath, ) { const pm = packageManagers[id]; const supportedAuditDepGroups = @@ -120,7 +122,7 @@ async function processAudit( const { stdout } = await executeProcess({ command: pm.command, args: pm.audit.getCommandArgs(depGroup), - cwd: process.cwd(), + cwd: packageJsonPath ? path.dirname(packageJsonPath) : process.cwd(), ignoreExitCode: pm.audit.ignoreExitCode, }); return [depGroup, pm.audit.unifyResult(stdout)]; diff --git a/packages/plugin-js-packages/src/lib/runner/runner.integration.test.ts b/packages/plugin-js-packages/src/lib/runner/runner.integration.test.ts index bff5912e3..c61af31fc 100644 --- a/packages/plugin-js-packages/src/lib/runner/runner.integration.test.ts +++ b/packages/plugin-js-packages/src/lib/runner/runner.integration.test.ts @@ -12,7 +12,7 @@ describe('createRunnerConfig', () => { checks: ['audit'], auditLevelMapping: defaultAuditLevelMapping, dependencyGroups: ['prod', 'dev'], - packageJsonPaths: ['package.json'], + packageJsonPath: 'package.json', }); expect(runnerConfig).toStrictEqual({ command: 'node', @@ -32,7 +32,7 @@ describe('createRunnerConfig', () => { checks: ['outdated'], dependencyGroups: ['prod', 'dev'], auditLevelMapping: { ...defaultAuditLevelMapping, moderate: 'error' }, - packageJsonPaths: ['package.json'], + packageJsonPath: 'package.json', }; const { configFile } = await createRunnerConfig( 'executeRunner.ts', diff --git a/packages/plugin-js-packages/src/lib/runner/utils.ts b/packages/plugin-js-packages/src/lib/runner/utils.ts index db0f318e6..2d615f171 100644 --- a/packages/plugin-js-packages/src/lib/runner/utils.ts +++ b/packages/plugin-js-packages/src/lib/runner/utils.ts @@ -1,13 +1,10 @@ -import path from 'node:path'; import { - crawlFileSystem, objectFromEntries, objectToKeys, readJsonFile, } from '@code-pushup/utils'; import type { AuditResult, Vulnerability } from './audit/types.js'; import { - type DependencyGroupLong, type DependencyTotals, type PackageJson, dependencyGroupLong, @@ -54,41 +51,18 @@ export function filterAuditResult( }; } -// TODO: use .gitignore -export async function findAllPackageJson(): Promise { - return ( - await crawlFileSystem({ - directory: '.', - pattern: /(^|[\\/])package\.json$/, - }) - ).filter( - filePath => - !filePath.startsWith(`node_modules${path.sep}`) && - !filePath.includes(`${path.sep}node_modules${path.sep}`) && - !filePath.startsWith(`.nx${path.sep}`), - ); -} - export async function getTotalDependencies( - packageJsonPaths: string[], + packageJsonPath: string, ): Promise { - const parsedDeps = await Promise.all( - packageJsonPaths.map(readJsonFile), - ); + const parsedDeps = await readJsonFile(packageJsonPath); - const mergedDeps = parsedDeps.reduce>( - (acc, depMapper) => - objectFromEntries( - dependencyGroupLong.map(group => { - const deps = depMapper[group]; - return [ - group, - [...acc[group], ...(deps == null ? [] : objectToKeys(deps))], - ]; - }), - ), - { dependencies: [], devDependencies: [], optionalDependencies: [] }, + const mergedDeps = objectFromEntries( + dependencyGroupLong.map(group => { + const deps = parsedDeps[group]; + return [group, deps == null ? [] : objectToKeys(deps)]; + }), ); + return objectFromEntries( objectToKeys(mergedDeps).map(deps => [ deps, diff --git a/packages/plugin-js-packages/src/lib/runner/utils.unit.test.ts b/packages/plugin-js-packages/src/lib/runner/utils.unit.test.ts index 321606a30..f695a5ff7 100644 --- a/packages/plugin-js-packages/src/lib/runner/utils.unit.test.ts +++ b/packages/plugin-js-packages/src/lib/runner/utils.unit.test.ts @@ -4,34 +4,7 @@ import { describe, expect, it } from 'vitest'; import { MEMFS_VOLUME } from '@code-pushup/test-utils'; import type { AuditResult, Vulnerability } from './audit/types.js'; import type { DependencyTotals, PackageJson } from './outdated/types.js'; -import { - filterAuditResult, - findAllPackageJson, - getTotalDependencies, -} from './utils.js'; - -describe('findAllPackageJson', () => { - beforeEach(() => { - vol.fromJSON( - { - 'package.json': '', - [path.join('ui', 'package.json')]: '', - [path.join('ui', 'ng-package.json')]: '', // non-exact file match should be excluded - [path.join('.nx', 'cache', 'ui', 'package.json')]: '', // nx cache should be excluded - [path.join('node_modules', 'eslint', 'package.json')]: '', // root node_modules should be excluded - [path.join('ui', 'node_modules', 'eslint', 'package.json')]: '', // project node_modules should be excluded - }, - MEMFS_VOLUME, - ); - }); - - it('should return all valid package.json files (exclude .nx, node_modules)', async () => { - await expect(findAllPackageJson()).resolves.toEqual([ - 'package.json', - path.join('ui', 'package.json'), - ]); - }); -}); +import { filterAuditResult, getTotalDependencies } from './utils.js'; describe('getTotalDependencies', () => { beforeEach(() => { @@ -64,7 +37,7 @@ describe('getTotalDependencies', () => { it('should return correct number of dependencies', async () => { await expect( - getTotalDependencies([path.join(MEMFS_VOLUME, 'package.json')]), + getTotalDependencies(path.join(MEMFS_VOLUME, 'package.json')), ).resolves.toStrictEqual({ dependencies: 1, devDependencies: 3, @@ -72,15 +45,12 @@ describe('getTotalDependencies', () => { } satisfies DependencyTotals); }); - it('should merge dependencies for multiple package.json files', async () => { + it('should return dependencies for nested package.json file', async () => { await expect( - getTotalDependencies([ - path.join(MEMFS_VOLUME, 'package.json'), - path.join(MEMFS_VOLUME, 'ui', 'package.json'), - ]), + getTotalDependencies(path.join(MEMFS_VOLUME, 'ui', 'package.json')), ).resolves.toStrictEqual({ dependencies: 2, - devDependencies: 4, + devDependencies: 1, optionalDependencies: 1, } satisfies DependencyTotals); });