diff --git a/biome.json b/biome.json index 6edc6f97..1bb13347 100644 --- a/biome.json +++ b/biome.json @@ -7,7 +7,8 @@ }, "files": { "ignoreUnknown": false, - "ignore": ["dist", "build"] + "ignore": ["dist", "build"], + "maxSize": 1000000000000 }, "formatter": { "enabled": true, diff --git a/packages/core/package.json b/packages/core/package.json index 6d859d82..ec7066cf 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -13,9 +13,7 @@ "types": "./dist/types/index.d.ts" } }, - "files": [ - "dist" - ], + "files": ["dist"], "scripts": { "build": "tsup-node", "dev": "tsc --watch", diff --git a/packages/core/src/dependency-finder/find-dependencies.ts b/packages/core/src/dependency-finder/find-dependencies.ts index a8ffc590..8bdb04fb 100644 --- a/packages/core/src/dependency-finder/find-dependencies.ts +++ b/packages/core/src/dependency-finder/find-dependencies.ts @@ -52,7 +52,7 @@ function findExternalDependencies({ switch (packageManager) { case "npm": case "yarn": - return findNpmDependencies(projectRoot, production, verbose); + return findNpmDependencies(projectRoot, production); case "pnpm": return findPnpmDependencies(projectRoot, production, verbose); case "yarn-classic": diff --git a/packages/core/src/dependency-finder/npm.ts b/packages/core/src/dependency-finder/npm.ts index b15727fa..f8798d5c 100644 --- a/packages/core/src/dependency-finder/npm.ts +++ b/packages/core/src/dependency-finder/npm.ts @@ -1,73 +1,38 @@ import type { DependenciesResult } from "@license-auditor/data"; -import { ExecCommandException } from "../exceptions/index.js"; -import { execCommand } from "./exec-command.js"; +import Arborist, { type Node } from "@npmcli/arborist"; -export const findNpmDepsCommand = "npm ls --all -p"; -export const findNpmProdDepsCommand = "npm ls --all -p --omit=dev"; +const flattenDependenciesTree = ( + tree: Node, + production?: boolean, +): string[] => { + const dependencies: string[] = []; + for (const [, node] of tree.children) { + if (!(production && node.dev)) { + const packagePath = node.path; + if (!packagePath) { + throw new Error("Package found by arborist is missing a path"); + } + dependencies.push(packagePath); + } + dependencies.push(...flattenDependenciesTree(node, production)); + } + + return dependencies; +}; export async function findNpmDependencies( projectRoot: string, production?: boolean | undefined, - verbose?: boolean | undefined, ): Promise { - const { output, warning } = await (async () => { - try { - return { - output: await execCommand( - production ? findNpmProdDepsCommand : findNpmDepsCommand, - projectRoot, - verbose, - ), - }; - } catch (error) { - if (error instanceof ExecCommandException) { - if (/missing:.+required by/.test(error.stderr)) { - return { - output: error.stdout, - warning: `Results incomplete because of an error. This is most likely caused by peer dependencies. Try turning legacy-peer-deps off and resolve peer dependency conflicts. Original error:\n${error.stderr}`, - }; - } - if (/ELSPROBLEMS/.test(error.stderr)) { - throw new ExecCommandException( - [ - "", - "Unable to resolve project dependencies.", - error.message, - "", - "Potential causes:", - " - Incompatible or inconsistent version specifications in package.json.", - " - Conflicts in peer dependencies.", - " - Cached or outdated data in node_modules or npm cache.", - "", - "Suggested actions:", - " 1. Inspect and resolve version conflicts in package.json.", - " 2. Check and address peer dependency conflicts.", - " 3. Clear the node_modules folder and reinstall dependencies with a clean state:", - " remove node_modules and run npm install", - " 4. Clear the npm cache to ensure no outdated or corrupted data is used:", - " npm cache clean --force", - ].join("\n"), - { - stdout: error.stdout, - stderr: error.stderr, - originalError: error.originalError, - }, - ); - } - } - throw error; - } - })(); - - // Remove the first line, as npm always prints the project root first - const lines = output.split("\n").slice(1); + try { + const arborist = new Arborist({ path: projectRoot }); + const tree = await arborist.loadActual(); - const dependencies = lines - .filter((line) => line.trim() !== "") - .map((line) => line.trim()); + const dependencies: string[] = flattenDependenciesTree(tree, production); - if (warning) { - return { dependencies, warning }; + return { dependencies }; + } catch (error) { + console.log("arborist error", error); + throw error; } - return { dependencies }; } diff --git a/packages/core/src/license-finder/find-license-in-license-file.ts b/packages/core/src/license-finder/find-license-in-license-file.ts index 29f5f9ce..a8a88488 100644 --- a/packages/core/src/license-finder/find-license-in-license-file.ts +++ b/packages/core/src/license-finder/find-license-in-license-file.ts @@ -19,11 +19,16 @@ export function retrieveLicenseFromLicenseFileContent(content: string): { // threshold selected empirically based on our tests const foundLicense = licenseMap.get(detectedLicense.licenseId); if (!foundLicense) { - throw new Error(`License detected but not found in license map: ${detectedLicense.licenseId}`); + throw new Error( + `License detected but not found in license map: ${detectedLicense.licenseId}`, + ); } return { - licenses: addLicenseSource([LicenseSchema.parse(foundLicense)], LICENSE_SOURCE.licenseFileContent), + licenses: addLicenseSource( + [LicenseSchema.parse(foundLicense)], + LICENSE_SOURCE.licenseFileContent, + ), }; } diff --git a/test/test/npm.test.ts b/test/test/npm.test.ts index 4041bded..b2ac5729 100644 --- a/test/test/npm.test.ts +++ b/test/test/npm.test.ts @@ -83,7 +83,7 @@ describe("license-auditor", () => { }); expect(errorCode).toBe(0); - expect(output).toContain("Results incomplete because of an error."); + expect(output).toContain("170 licenses are compliant"); }); describe("parse license files", () => { @@ -792,8 +792,8 @@ describe("license-auditor", () => { cwd: testDirectory, }); - expect(errorCode).toBe(1); - expect(output).toContain("Unable to resolve project dependencies."); + expect(errorCode).toBe(0); + expect(output).toContain("7 licenses are compliant"); }, ); }); diff --git a/test/test/snapshot.test.ts b/test/test/snapshot.test.ts index 1e56f695..9e6007c7 100644 --- a/test/test/snapshot.test.ts +++ b/test/test/snapshot.test.ts @@ -10,7 +10,7 @@ import { getCliPath } from "../utils/get-cli-path"; import { readJsonFile } from "../utils/read-json-file"; import { runCliCommand } from "../utils/run-cli-command"; import "../utils/path-serializer"; -import { matchSnapshotRecursive } from '../utils/matchSnapshotRecursive'; +import { matchSnapshotRecursive } from "../utils/match-snapshot-recursive"; describe("snapshot testing", () => { monorepoFixture("monorepo project", async ({ testDirectory, expect }) => { @@ -25,8 +25,29 @@ describe("snapshot testing", () => { const jsonOutput = await readJsonFile( path.join(testDirectory, "license-auditor.results.json"), ); - matchSnapshotRecursive('./__snapshots__/monorepo.json', jsonOutput, false); + matchSnapshotRecursive("./__snapshots__/monorepo.json", jsonOutput, false); }); + monorepoFixture( + "monorepo project production only", + async ({ testDirectory, expect }) => { + const { errorCode } = await runCliCommand({ + command: "npx", + args: [getCliPath(), "--verbose", "--json", "--production"], + cwd: testDirectory, + }); + + expect(errorCode).toBe(0); + + const jsonOutput = await readJsonFile( + path.join(testDirectory, "license-auditor.results.json"), + ); + matchSnapshotRecursive( + "./__snapshots__/monorepo-production.json", + jsonOutput, + false, + ); + }, + ); pnpmFixture("pnpm project", async ({ testDirectory, expect }) => { const { errorCode } = await runCliCommand({ @@ -40,9 +61,31 @@ describe("snapshot testing", () => { const jsonOutput = await readJsonFile( path.join(testDirectory, "license-auditor.results.json"), ); - matchSnapshotRecursive('./__snapshots__/pnpm.json', jsonOutput, false); + matchSnapshotRecursive("./__snapshots__/pnpm.json", jsonOutput, false); }); + pnpmFixture( + "pnpm project production only", + async ({ testDirectory, expect }) => { + const { errorCode } = await runCliCommand({ + command: "npx", + args: [getCliPath(), "--verbose", "--json", "--production"], + cwd: testDirectory, + }); + + expect(errorCode).toBe(0); + + const jsonOutput = await readJsonFile( + path.join(testDirectory, "license-auditor.results.json"), + ); + matchSnapshotRecursive( + "./__snapshots__/pnpm-production.json", + jsonOutput, + false, + ); + }, + ); + defaultTest("npm project", async ({ testDirectory, expect }) => { const { errorCode } = await runCliCommand({ command: "npx", @@ -55,9 +98,31 @@ describe("snapshot testing", () => { const jsonOutput = await readJsonFile( path.join(testDirectory, "license-auditor.results.json"), ); - matchSnapshotRecursive('./__snapshots__/npm.json', jsonOutput, false); + matchSnapshotRecursive("./__snapshots__/npm.json", jsonOutput, false); }); + defaultTest( + "npm project production only", + async ({ testDirectory, expect }) => { + const { errorCode } = await runCliCommand({ + command: "npx", + args: [getCliPath(), "--verbose", "--json", "--production"], + cwd: testDirectory, + }); + + expect(errorCode).toBe(0); + + const jsonOutput = await readJsonFile( + path.join(testDirectory, "license-auditor.results.json"), + ); + matchSnapshotRecursive( + "./__snapshots__/npm-production.json", + jsonOutput, + false, + ); + }, + ); + yarnFixture("yarn project", async ({ testDirectory, expect }) => { const { errorCode } = await runCliCommand({ command: "npx", @@ -70,6 +135,28 @@ describe("snapshot testing", () => { const jsonOutput = await readJsonFile( path.join(testDirectory, "license-auditor.results.json"), ); - matchSnapshotRecursive('./__snapshots__/yarn.json', jsonOutput, false); + matchSnapshotRecursive("./__snapshots__/yarn.json", jsonOutput, false); }); + + yarnFixture( + "yarn project production only", + async ({ testDirectory, expect }) => { + const { errorCode } = await runCliCommand({ + command: "npx", + args: [getCliPath(), "--verbose", "--json", "--production"], + cwd: testDirectory, + }); + + expect(errorCode).toBe(0); + + const jsonOutput = await readJsonFile( + path.join(testDirectory, "license-auditor.results.json"), + ); + matchSnapshotRecursive( + "./__snapshots__/yarn-production.json", + jsonOutput, + false, + ); + }, + ); }); diff --git a/test/utils/matchSnapshotRecursive.test.ts b/test/utils/match-snapshot-recursive.test.ts similarity index 64% rename from test/utils/matchSnapshotRecursive.test.ts rename to test/utils/match-snapshot-recursive.test.ts index cce0b335..62cb4136 100644 --- a/test/utils/matchSnapshotRecursive.test.ts +++ b/test/utils/match-snapshot-recursive.test.ts @@ -1,108 +1,98 @@ -import { describe, test, expect } from 'vitest'; -import { findDiffRecursive } from './matchSnapshotRecursive'; +import { describe, expect, test } from "vitest"; +import { findDiffRecursive } from "./match-snapshot-recursive"; describe("matchSnapshotRecursive", () => { test("works", async () => { const source = { whitelist: [ { - "packageName": "express@4.21.1", - "packagePath": "/node_modules/express", - "status": "whitelist", - "licenses": [ + packageName: "express@4.21.1", + packagePath: "/node_modules/express", + status: "whitelist", + licenses: [ { - "reference": "https://spdx.org/licenses/MIT.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/MIT.json", - "name": "MIT License", - "licenseId": "MIT", - "seeAlso": [ - "https://opensource.org/license/mit/" - ], - "isOsiApproved": true, - "isFsfLibre": true, - "source": "package.json-license" + reference: "https://spdx.org/licenses/MIT.html", + isDeprecatedLicenseId: false, + detailsUrl: "https://spdx.org/licenses/MIT.json", + name: "MIT License", + licenseId: "MIT", + seeAlso: ["https://opensource.org/license/mit/"], + isOsiApproved: true, + isFsfLibre: true, + source: "package.json-license", }, { - "reference": "https://spdx.org/licenses/MIT.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/MIT.json", - "name": "MIT License", - "licenseId": "MIT", - "seeAlso": [ - "https://opensource.org/license/mit/" - ], - "isOsiApproved": true, - "source": "license-file-content" - } + reference: "https://spdx.org/licenses/MIT.html", + isDeprecatedLicenseId: false, + detailsUrl: "https://spdx.org/licenses/MIT.json", + name: "MIT License", + licenseId: "MIT", + seeAlso: ["https://opensource.org/license/mit/"], + isOsiApproved: true, + source: "license-file-content", + }, ], - "licensePath": [ + licensePath: [ "/node_modules/express/package.json", - "/node_modules/express/LICENSE" + "/node_modules/express/LICENSE", ], - "verificationStatus": "ok" + verificationStatus: "ok", }, { - "packageName": "some-package@1.0.0", - } + packageName: "some-package@1.0.0", + }, ], }; const compare = { whitelist: [ { - "packageName": "some-package@1.0.0", + packageName: "some-package@1.0.0", }, { - "packageName": "express@4.21.1", - "packagePath": "/node_modules/express", - "status": "whitelist", - "licenses": [ + packageName: "express@4.21.1", + packagePath: "/node_modules/express", + status: "whitelist", + licenses: [ { - "reference": "https://spdx.org/licenses/MIT.html", - "isDeprecatedLicenseId": false, - "detailsUrl": "https://spdx.org/licenses/MIT.json", - "name": "MIT License", - "licenseId": "MIT", - "seeAlso": [ - "https://opensource.org/license/mit/" - ], - "isOsiApproved": true, - "source": "license-file-content" + reference: "https://spdx.org/licenses/MIT.html", + isDeprecatedLicenseId: false, + detailsUrl: "https://spdx.org/licenses/MIT.json", + name: "MIT License", + licenseId: "MIT", + seeAlso: ["https://opensource.org/license/mit/"], + isOsiApproved: true, + source: "license-file-content", }, { - "reference": "https://spdx.org/licenses/MIT.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/MIT.json", - "name": "MIT License", - "licenseId": "MIT", - "seeAlso": [ - "https://opensource.org/license/mit/" - ], - "isOsiApproved": true, - "isFsfLibre": true, - "source": "package.json-license" + reference: "https://spdx.org/licenses/MIT.html", + isDeprecatedLicenseId: true, + detailsUrl: "https://spdx.org/licenses/MIT.json", + name: "MIT License", + licenseId: "MIT", + seeAlso: ["https://opensource.org/license/mit/"], + isOsiApproved: true, + isFsfLibre: true, + source: "package.json-license", }, { - "reference": "https://spdx.org/licenses/bad-license.html", - "isDeprecatedLicenseId": true, - "detailsUrl": "https://spdx.org/licenses/bad-license.json", - "name": "bad-license License", - "licenseId": "bad-license", - "seeAlso": [ - "https://opensource.org/license/bad-license/" - ], - "isOsiApproved": true, - "isFsfLibre": true, - "source": "license-file-content" - } + reference: "https://spdx.org/licenses/bad-license.html", + isDeprecatedLicenseId: true, + detailsUrl: "https://spdx.org/licenses/bad-license.json", + name: "bad-license License", + licenseId: "bad-license", + seeAlso: ["https://opensource.org/license/bad-license/"], + isOsiApproved: true, + isFsfLibre: true, + source: "license-file-content", + }, ], - "licensePath": [ + licensePath: [ "/node_modules/express/package.json", - "/node_modules/express/LICENSE" + "/node_modules/express/LICENSE", ], - "verificationStatus": "ok" + verificationStatus: "ok", }, - ] + ], }; const { jsView, sortedView } = findDiffRecursive(source, compare); @@ -223,7 +213,7 @@ describe("matchSnapshotRecursive", () => { packageName: 'some-package@1.0.0', }, ], -}`) +}`); expect(sortedView.target).toBe(`{ whitelist: [ { diff --git a/test/utils/matchSnapshotRecursive.ts b/test/utils/match-snapshot-recursive.ts similarity index 61% rename from test/utils/matchSnapshotRecursive.ts rename to test/utils/match-snapshot-recursive.ts index 970751a3..a4f42950 100644 --- a/test/utils/matchSnapshotRecursive.ts +++ b/test/utils/match-snapshot-recursive.ts @@ -1,32 +1,41 @@ -import * as path from 'node:path'; -import createDiff, { diffJsView, diffSortedView, diffChangesViewConsole } from 'differrer'; -import { expect } from 'vitest'; -import * as fs from 'node:fs'; -import { replaceTestDirectory } from './path-serializer'; +import * as fs from "node:fs"; +import * as path from "node:path"; +import createDiff, { + diffJsView, + diffSortedView, + diffChangesViewConsole, +} from "differrer"; +import { expect } from "vitest"; +import { replaceTestDirectory } from "./path-serializer"; +// biome-ignore lint: this actually is of "any" type const getArrayElementId = (item: any) => { - if (typeof item !== 'object') { + if (typeof item !== "object") { return item; } - if (Object.prototype.hasOwnProperty.call(item, 'packageName')) { + if (Object.prototype.hasOwnProperty.call(item, "packageName")) { return item.packageName; } - if (Object.prototype.hasOwnProperty.call(item, 'licenseId') && Object.prototype.hasOwnProperty.call(item, 'source')) { + if ( + Object.prototype.hasOwnProperty.call(item, "licenseId") && + Object.prototype.hasOwnProperty.call(item, "source") + ) { return `${item.licenseId}-${item.source}`; } -} +}; const diff = createDiff({ getArrayElementId, sortArrayItems: true, getValue: (value, path) => { - if (typeof value === 'string') { + if (typeof value === "string") { return replaceTestDirectory(value); } return value; }, }); +// biome-ignore lint: this actually is of "any" type export const findDiffRecursive = (source: any, compare: any) => { const diffResult = diff(source, compare); @@ -34,7 +43,6 @@ export const findDiffRecursive = (source: any, compare: any) => { const sortedView = diffSortedView(diffResult); const diffChangesView = diffChangesViewConsole(diffResult); - return { diffResult, jsView, @@ -43,7 +51,12 @@ export const findDiffRecursive = (source: any, compare: any) => { }; }; -export const matchSnapshotRecursive = (snapshotPath: string, value: any, updateSnapshot?: boolean) => { +export const matchSnapshotRecursive = ( + snapshotPath: string, + // biome-ignore lint: this actually is of "any" type + value: any, + updateSnapshot?: boolean, +) => { const testState = expect.getState(); const currentDirectory = path.dirname(testState.testPath); const snapshotFilePath = path.join(currentDirectory, snapshotPath); @@ -54,7 +67,7 @@ export const matchSnapshotRecursive = (snapshotPath: string, value: any, updateS return; } - const snapshot = JSON.parse(fs.readFileSync(snapshotFilePath, 'utf-8')); + const snapshot = JSON.parse(fs.readFileSync(snapshotFilePath, "utf-8")); const { sortedView } = findDiffRecursive(snapshot, value); diff --git a/test/utils/path-serializer.ts b/test/utils/path-serializer.ts index b19b5f06..9ac4f72a 100644 --- a/test/utils/path-serializer.ts +++ b/test/utils/path-serializer.ts @@ -1,10 +1,11 @@ import { expect } from "vitest"; import { TEST_TEMP_DIRECTORY } from "../global-setup"; -export const replaceTestDirectory = (value: string) => value.replace( - new RegExp(`${TEST_TEMP_DIRECTORY}/testProject-[a-zA-Z0-9]+`, "g"), - "", -); +export const replaceTestDirectory = (value: string) => + value.replace( + new RegExp(`${TEST_TEMP_DIRECTORY}/testProject-[a-zA-Z0-9]+`, "g"), + "", + ); export const pathSerializer = { test: (val: unknown): boolean => {