From 5b7939ed388777029cf2cfcb100dbcfaa3ae6b91 Mon Sep 17 00:00:00 2001 From: David Sveningsson Date: Mon, 8 Apr 2024 01:15:22 +0200 Subject: [PATCH] feat: add `--allow-dependency` to ignore disallowed dependency --- README.md | 2 + src/__snapshots__/package-json.spec.ts.snap | 12 ----- src/index.ts | 11 ++++- src/package-json.spec.ts | 55 +++++++++++++++++++-- src/package-json.ts | 25 ++++++++-- src/verify.ts | 2 +- tests/integration.spec.ts | 2 +- 7 files changed, 88 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 1ab61dfd..a022ad6b 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,8 @@ Examples of disallowed packages: By default `@types/*` is disallowed but this can be disabled with `--allow-types-dependencies`. +If needed, `--allow-dependency` can be used to ignore one or more dependencies. + ### ESLint If your `package.json` contains the `"eslint"` keyword the ESLint packages can be included as dependencies, e.g. if you publish a sharable config including a plugin you must include `"eslint"` as a keyword. diff --git a/src/__snapshots__/package-json.spec.ts.snap b/src/__snapshots__/package-json.spec.ts.snap index 6262ec5d..286bc9d5 100644 --- a/src/__snapshots__/package-json.spec.ts.snap +++ b/src/__snapshots__/package-json.spec.ts.snap @@ -251,15 +251,3 @@ exports[`should return engines.node supports eol version 1`] = ` }, ] `; - -exports[`should return error if dependency is disallowed 1`] = ` -[ - { - "column": 1, - "line": 1, - "message": "eslint should be a devDependency", - "ruleId": "disallowed-dependency", - "severity": 2, - }, -] -`; diff --git a/src/index.ts b/src/index.ts index 861b48a4..ebc9b1e2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,6 +24,7 @@ interface ParsedArgs { pkgfile: string; tarball?: string; ignore_missing_fields?: boolean; + allow_dependency: string[]; allow_types_dependencies?: boolean; } @@ -99,9 +100,15 @@ async function run(): Promise { parser.add_argument("-t", "--tarball", { help: "specify tarball location" }); parser.add_argument("-p", "--pkgfile", { help: "specify package.json location" }); parser.add_argument("--cache", { help: "specify cache directory" }); + parser.add_argument("--allow-dependency", { + action: "append", + default: [], + metavar: "DEPENDENCY", + help: "explicitly allow given dependency (can be given multiple times or as a comma-separated list)", + }); parser.add_argument("--allow-types-dependencies", { action: "store_true", - help: "allow dependencies to `@types/*`", + help: "allow production dependencies to `@types/*`", }); parser.add_argument("--ignore-missing-fields", { action: "store_true", @@ -109,6 +116,7 @@ async function run(): Promise { }); const args = parser.parse_args() as ParsedArgs; + const allowedDependencies = new Set(args.allow_dependency.map((it) => it.split(",")).flat()); if (args.cache) { await setCacheDirecory(args.cache); @@ -144,6 +152,7 @@ async function run(): Promise { setupBlacklist(pkg.name); const options: VerifyOptions = { + allowedDependencies, allowTypesDependencies: args.allow_types_dependencies, ignoreMissingFields: args.ignore_missing_fields, }; diff --git a/src/package-json.spec.ts b/src/package-json.spec.ts index 074f40c0..f601bdc7 100644 --- a/src/package-json.spec.ts +++ b/src/package-json.spec.ts @@ -48,7 +48,38 @@ it("should return error if dependency is disallowed", async () => { const results = await verifyPackageJson(pkg, "package.json"); expect(results).toHaveLength(1); expect(results[0].filePath).toBe("package.json"); - expect(results[0].messages).toMatchSnapshot(); + expect(results[0].messages).toMatchInlineSnapshot(` + [ + { + "column": 1, + "line": 1, + "message": ""eslint" should be a devDependency", + "ruleId": "disallowed-dependency", + "severity": 2, + }, + ] + `); +}); + +it("should return error if aliased dependency is disallowed", async () => { + expect.assertions(3); + pkg.dependencies = { + aliased: "npm:eslint@1.2.3", + }; + const results = await verifyPackageJson(pkg, "package.json"); + expect(results).toHaveLength(1); + expect(results[0].filePath).toBe("package.json"); + expect(results[0].messages).toMatchInlineSnapshot(` + [ + { + "column": 1, + "line": 1, + "message": """aliased" ("npm:eslint")" should be a devDependency", + "ruleId": "disallowed-dependency", + "severity": 2, + }, + ] + `); }); it("should not return error if dependency is allowed", async () => { @@ -60,6 +91,18 @@ it("should not return error if dependency is allowed", async () => { expect(results).toEqual([]); }); +it("should not return error if explicitly allowed by user", async () => { + expect.assertions(1); + pkg.dependencies = { + eslint: "1.2.3", + aliased: "npm:eslint@1.2.3", + }; + const results = await verifyPackageJson(pkg, "package.json", { + allowedDependencies: new Set(["eslint"]), + }); + expect(results).toHaveLength(0); +}); + it("should return engines.node supports eol version", async () => { expect.assertions(3); pkg.engines.node = ">= 8"; @@ -84,7 +127,10 @@ describe("@types", () => { it("should not return error if @types is allowed", async () => { expect.assertions(1); - const results = await verifyPackageJson(pkg, "package.json", { allowTypesDependencies: true }); + const results = await verifyPackageJson(pkg, "package.json", { + allowedDependencies: new Set(), + allowTypesDependencies: true, + }); expect(results).toHaveLength(0); }); }); @@ -100,7 +146,10 @@ describe("present", () => { it("should ignore error on missing fields if ignoreMissingFields is set", async () => { expect.assertions(1); delete pkg.description; - const results = await verifyPackageJson(pkg, "package.json", { ignoreMissingFields: true }); + const results = await verifyPackageJson(pkg, "package.json", { + allowedDependencies: new Set(), + ignoreMissingFields: true, + }); expect(results).toHaveLength(0); }); }); diff --git a/src/package-json.ts b/src/package-json.ts index a33e8ddf..8d523a31 100644 --- a/src/package-json.ts +++ b/src/package-json.ts @@ -9,6 +9,7 @@ import { verifyEngineConstraint } from "./rules/verify-engine-constraint"; import { typesNodeMatchingEngine } from "./rules/types-node-matching-engine"; export interface VerifyPackageJsonOptions { + allowedDependencies: Set; allowTypesDependencies?: boolean; ignoreMissingFields?: boolean; } @@ -59,20 +60,38 @@ function verifyFields(pkg: PackageJson, options: VerifyPackageJsonOptions): Mess return messages; } +function getActualDependency(key: string, version: string): string { + /* handle npm: prefix */ + if (version.startsWith("npm:")) { + const [name] = version.slice("npm:".length).split("@", 2); + return name; + } + + return key; +} + function verifyDependencies(pkg: PackageJson, options: VerifyPackageJsonOptions): Message[] { const messages: Message[] = []; - for (const dependency of Object.keys(pkg.dependencies ?? {})) { + for (const [key, version] of Object.entries(pkg.dependencies ?? {})) { + const dependency = getActualDependency(key, version); + + /* skip dependencies explicitly allowed by the user */ + if (options.allowedDependencies.has(dependency)) { + continue; + } + /* skip @types/* if explicitly allowed by user */ if (options.allowTypesDependencies && dependency.match(/^@types\//)) { continue; } if (isDisallowedDependency(pkg, dependency)) { + const name = key === dependency ? dependency : `"${key}" ("npm:${dependency}")`; messages.push({ ruleId: "disallowed-dependency", severity: 2, - message: `${dependency} should be a devDependency`, + message: `"${name}" should be a devDependency`, line: 1, column: 1, }); @@ -85,7 +104,7 @@ function verifyDependencies(pkg: PackageJson, options: VerifyPackageJsonOptions) export async function verifyPackageJson( pkg: PackageJson, filePath: string, - options: VerifyPackageJsonOptions = {} + options: VerifyPackageJsonOptions = { allowedDependencies: new Set() } ): Promise { const messages: Message[] = [ ...(await verifyEngineConstraint(pkg)), diff --git a/src/verify.ts b/src/verify.ts index 0231a6a2..af13eff4 100644 --- a/src/verify.ts +++ b/src/verify.ts @@ -10,7 +10,7 @@ export async function verify( pkg: PackageJson, pkgPath: string, tarball: TarballMeta, - options: VerifyOptions = {} + options: VerifyOptions ): Promise { return [ ...(await verifyTarball(pkg, tarball)), diff --git a/tests/integration.spec.ts b/tests/integration.spec.ts index 5e514a5f..52204387 100644 --- a/tests/integration.spec.ts +++ b/tests/integration.spec.ts @@ -25,6 +25,6 @@ it.each(fixtures)("%s", async (fixture) => { const pkgPath = path.relative(ROOT_DIRECTORY, path.join(dir, "package.json")); const pkg: PackageJson = JSON.parse(await fs.readFile(pkgPath, "utf-8")); const tarball = { filePath: await npmPack(pkg, fixture) }; - const result = await verify(pkg, pkgPath, tarball); + const result = await verify(pkg, pkgPath, tarball, { allowedDependencies: new Set() }); expect(result).toMatchSnapshot(); });