Skip to content

Commit

Permalink
feat: add --allow-dependency to ignore disallowed dependency
Browse files Browse the repository at this point in the history
  • Loading branch information
ext committed Apr 7, 2024
1 parent 5cb951f commit 5b7939e
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 21 deletions.
2 changes: 2 additions & 0 deletions README.md
Expand Up @@ -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.
Expand Down
12 changes: 0 additions & 12 deletions src/__snapshots__/package-json.spec.ts.snap
Expand Up @@ -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,
},
]
`;
11 changes: 10 additions & 1 deletion src/index.ts
Expand Up @@ -24,6 +24,7 @@ interface ParsedArgs {
pkgfile: string;
tarball?: string;
ignore_missing_fields?: boolean;
allow_dependency: string[];
allow_types_dependencies?: boolean;
}

Expand Down Expand Up @@ -99,16 +100,23 @@ async function run(): Promise<void> {
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",
help: "ignore errors for missing fields (but still checks for empty and valid)",
});

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);
Expand Down Expand Up @@ -144,6 +152,7 @@ async function run(): Promise<void> {
setupBlacklist(pkg.name);

const options: VerifyOptions = {
allowedDependencies,
allowTypesDependencies: args.allow_types_dependencies,
ignoreMissingFields: args.ignore_missing_fields,
};
Expand Down
55 changes: 52 additions & 3 deletions src/package-json.spec.ts
Expand Up @@ -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 () => {
Expand All @@ -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";
Expand All @@ -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);
});
});
Expand All @@ -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);
});
});
Expand Down
25 changes: 22 additions & 3 deletions src/package-json.ts
Expand Up @@ -9,6 +9,7 @@ import { verifyEngineConstraint } from "./rules/verify-engine-constraint";
import { typesNodeMatchingEngine } from "./rules/types-node-matching-engine";

export interface VerifyPackageJsonOptions {
allowedDependencies: Set<string>;
allowTypesDependencies?: boolean;
ignoreMissingFields?: boolean;
}
Expand Down Expand Up @@ -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,
});
Expand All @@ -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<Result[]> {
const messages: Message[] = [
...(await verifyEngineConstraint(pkg)),
Expand Down
2 changes: 1 addition & 1 deletion src/verify.ts
Expand Up @@ -10,7 +10,7 @@ export async function verify(
pkg: PackageJson,
pkgPath: string,
tarball: TarballMeta,
options: VerifyOptions = {}
options: VerifyOptions
): Promise<Result[]> {
return [
...(await verifyTarball(pkg, tarball)),
Expand Down
2 changes: 1 addition & 1 deletion tests/integration.spec.ts
Expand Up @@ -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();
});

0 comments on commit 5b7939e

Please sign in to comment.