Repository: JoshuaKGoldberg/eslint-plugin-package-json
Pull request #1097 (2025-06-09 21:53:22)

feat: remove support for node 18
<!-- 👋 Hi, thanks for sending a PR to eslint-plugin-package-json! 🗂
Please fill out all fields below and make sure each item is true and [x] checked.
Otherwise we may not be able to review your PR. -->

## PR Checklist

- [x] Addresses an existing open issue: fixes #1074
- [x] That issue was marked as [`status: accepting prs`](https://github.com/JoshuaKGoldberg/eslint-plugin-package-json/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22)
- [x] Steps in [CONTRIBUTING.md](https://github.com/JoshuaKGoldberg/eslint-plugin-package-json/blob/main/.github/CONTRIBUTING.md) were taken

## Overview

This change removes official support for node v18.



Patch:
diff --git a/.github/actions/prepare/action.yml b/.github/actions/prepare/action.yml
index 188ba5a..c771f83 100644
--- a/.github/actions/prepare/action.yml
+++ b/.github/actions/prepare/action.yml
@@ -8,7 +8,7 @@ runs:
     - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
       with:
         cache: pnpm
-        node-version: 18
+        node-version: 20
     - run: pnpm install --frozen-lockfile
       shell: bash
   using: composite
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 418503f..3a357d7 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -74,7 +74,7 @@ jobs:
     steps:
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
       - uses: ./.github/actions/prepare
-      - run: pnpm tsc
+      - run: pnpm typecheck
 
 name: CI
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a079dc8..97079dd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Changelog
 
+# [0.34.0](https://github.com/JoshuaKGoldberg/eslint-plugin-package-json/compare/v0.33.2...v0.34.0) (2025-06-10)
+
+### Features
+
+- **valid-package-definition:** add ignoreProperties option ([#1077](https://github.com/JoshuaKGoldberg/eslint-plugin-package-json/issues/1077)) ([530e343](https://github.com/JoshuaKGoldberg/eslint-plugin-package-json/commit/530e3430f87f21c9fadd4d511097d20e26f7190f)), closes [#1076](https://github.com/JoshuaKGoldberg/eslint-plugin-package-json/issues/1076)
+
 ## [0.33.2](https://github.com/JoshuaKGoldberg/eslint-plugin-package-json/compare/v0.33.1...v0.33.2) (2025-06-01)
 
 ### Bug Fixes
diff --git a/docs/rules/valid-package-def.md b/docs/rules/valid-package-def.md
index 9f31427..e358e89 100644
--- a/docs/rules/valid-package-def.md
+++ b/docs/rules/valid-package-def.md
@@ -45,6 +45,27 @@ Examples of **correct** code for this rule:
 }
 ```
 
+## Options
+
+### `ignoreProperties`
+
+Pass an array of top-level package properties to ignore.
+When provided, any errors related to the properties won't cause the rule to report a violation.
+This can be useful if you're using any of the more granular `valid-*` rules alongside this one.
+That way you're not double-reporting violations.
+
+Example of excluding the bin property:
+
+```json
+{
+	"package-json/valid-bin": "error",
+	"package-json/validate-package-definition": [
+		"error",
+		{ "ignoreProperties": ["bin"] }
+	]
+}
+```
+
 ## When Not To Use It
 
 npm may complain, but it works perfectly with many package files that do not violate spec.
diff --git a/docs/rules/valid-package-definition.md b/docs/rules/valid-package-definition.md
index 7a89a5b..1e6d9c3 100644
--- a/docs/rules/valid-package-definition.md
+++ b/docs/rules/valid-package-definition.md
@@ -45,6 +45,31 @@ Examples of **correct** code for this rule:
 }
 ```
 
+## Options
+
+### `ignoreProperties`
+
+Pass an array of top-level package properties to ignore.
+When provided, any errors related to the properties won't cause the rule to report a violation.
+This can be useful if you're using any of the more granular `valid-*` rules alongside this one.
+That way you're not double-reporting violations.
+
+Example of excluding the bin property:
+
+```json
+{
+	"package-json/valid-bin": "error",
+	"package-json/validate-package-definition": [
+		"error",
+		{ "ignoreProperties": ["bin"] }
+	]
+}
+```
+
+For now, the `recommended` config sets `ignoreProperties` to ignore those that are covered by the newer, more granular `valid-*` rules.
+Once all `valid-*` rules are implemented, `valid-package-definition` will be removed from `recommended`.
+See [#850 💬 Discussion: Which require- and valid- rules should be in the recommended config?](https://github.com/JoshuaKGoldberg/eslint-plugin-package-json/issues/850) for the list of rules that will be added.
+
 ## When Not To Use It
 
 npm may complain, but it works perfectly with many package files that do not violate spec.
diff --git a/package.json b/package.json
index a9a63ba..1bb1739 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "eslint-plugin-package-json",
-	"version": "0.33.2",
+	"version": "0.34.0",
 	"description": "Rules for consistent, readable, and valid package.json files. 🗂️",
 	"homepage": "https://github.com/JoshuaKGoldberg/eslint-plugin-package-json#readme",
 	"bugs": {
@@ -43,7 +43,7 @@
 		"lint:spelling": "cspell \"**\" \".github/**/*\"",
 		"prepare": "husky",
 		"test": "vitest",
-		"tsc": "tsc"
+		"typecheck": "tsc"
 	},
 	"lint-staged": {
 		"*": "prettier --ignore-unknown --write"
@@ -53,7 +53,7 @@
 		"detect-indent": "6.1.0",
 		"detect-newline": "3.1.0",
 		"eslint-fix-utils": "^0.2.0",
-		"package-json-validator": "^0.10.0",
+		"package-json-validator": "~0.13.1",
 		"semver": "^7.5.4",
 		"sort-object-keys": "^1.1.3",
 		"sort-package-json": "^3.0.0",
@@ -107,7 +107,7 @@
 	},
 	"packageManager": "pnpm@10.11.0",
 	"engines": {
-		"node": ">=18"
+		"node": "^=20.19.0 || >=22.12.0"
 	},
 	"publishConfig": {
 		"provenance": true
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8d9ba19..45aba54 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -21,8 +21,8 @@ importers:
         specifier: ^0.2.0
         version: 0.2.1(@types/estree@1.0.7)(eslint@9.28.0(jiti@2.4.2))
       package-json-validator:
-        specifier: ^0.10.0
-        version: 0.10.0
+        specifier: ~0.13.1
+        version: 0.13.1
       semver:
         specifier: ^7.5.4
         version: 7.7.2
@@ -3180,8 +3180,8 @@ packages:
   package-json-from-dist@1.0.1:
     resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
 
-  package-json-validator@0.10.0:
-    resolution: {integrity: sha512-zaPt4x0ZIxA4KYWpPMkbOEhkEDfQdtkCCC1xhnbnYrQV+Kry3zMAxENujgdT6aPA5BJ+FfpncKoNULWc/qjloQ==}
+  package-json-validator@0.13.1:
+    resolution: {integrity: sha512-8wRiZoasU7/w9eTnW2faWuVYwyEASLgGlVJXvSohOBKrLH+Es57JZ6hXLtCf62vLXK4saYGzLR02QC23FKU8Gw==}
     engines: {node: '>=18'}
     hasBin: true
 
@@ -7413,7 +7413,7 @@ snapshots:
 
   package-json-from-dist@1.0.1: {}
 
-  package-json-validator@0.10.0:
+  package-json-validator@0.13.1:
     dependencies:
       yargs: 17.7.2
 
diff --git a/src/plugin.ts b/src/plugin.ts
index 16a8869..85c7bcd 100644
--- a/src/plugin.ts
+++ b/src/plugin.ts
@@ -1,3 +1,4 @@
+import { Linter } from "eslint";
 import * as parserJsonc from "jsonc-eslint-parser";
 import { createRequire } from "node:module";
 
@@ -54,11 +55,34 @@ const rules: Record<string, PackageJsonRuleModule> = {
 	},
 };
 
-const recommendedRules = Object.fromEntries(
-	Object.entries(rules)
-		.filter(([, rule]) => rule.meta.docs?.recommended)
-		.map(([name]) => ["package-json/" + name, "error" as const]),
-);
+const baseRecommendedRules = {
+	...Object.fromEntries(
+		Object.entries(rules)
+			.filter(([, rule]) => rule.meta.docs?.recommended)
+			.map(([name]) => ["package-json/" + name, "error" as const]),
+	),
+} satisfies Linter.RulesRecord;
+
+const recommendedRules = {
+	...baseRecommendedRules,
+	// As we add more `valid-*` rules, we should prevent this legacy rule from
+	// also reporting the same errors.
+	"package-json/valid-package-definition": [
+		"error",
+		{
+			// Create a list of properties to ignore based on the valid-* rules
+			// we currently have. Once we've fully covered what `valid-package-definition`
+			// checks, we can remove it from the `recommended` config entirely.
+			ignoreProperties: Object.entries(baseRecommendedRules)
+				.filter(
+					([name]) =>
+						name.startsWith("package-json/valid-") &&
+						name !== "package-json/valid-package-definition",
+				)
+				.map(([name]) => name.replace("package-json/valid-", "")),
+		},
+	],
+} satisfies Linter.RulesRecord;
 
 export const plugin = {
 	configs: {
diff --git a/src/rules/valid-package-definition.ts b/src/rules/valid-package-definition.ts
index b449662..9e2335f 100644
--- a/src/rules/valid-package-definition.ts
+++ b/src/rules/valid-package-definition.ts
@@ -2,6 +2,11 @@ import { validate } from "package-json-validator";
 
 import { createRule } from "../createRule.js";
 
+interface Option {
+	ignoreProperties?: string[];
+}
+type Options = [Option?];
+
 // package-json-validator does not correctly recognize shorthand for repositories and alternate dependency statements, so we discard those values.
 // it also enforces a stricter code for npm than is really appropriate,
 // so we disable some other errors here.
@@ -14,21 +19,31 @@ const ignoredErrors = [
 const isUsableError = (errorText: string) =>
 	ignoredErrors.every((pattern) => !pattern.test(errorText));
 
-export const rule = createRule({
+export const rule = createRule<Options>({
 	create(context) {
+		const ignoreProperties = context.options[0]?.ignoreProperties ?? [];
+
 		return {
 			"Program:exit"() {
 				const validation = validate(context.sourceCode.text);
 
-				validation.errors?.filter(isUsableError).forEach((message) => {
-					if (message) {
+				const usableErrors =
+					validation.errors?.filter((error) => {
+						return (
+							isUsableError(error.message) &&
+							!ignoreProperties.includes(error.field)
+						);
+					}) ?? [];
+
+				for (const error of usableErrors) {
+					if (error.message) {
 						context.report({
 							// eslint-disable-next-line eslint-plugin/prefer-message-ids
-							message,
+							message: error.message,
 							node: context.sourceCode.ast,
 						});
 					}
-				});
+				}
 			},
 		};
 	},
@@ -41,7 +56,20 @@ export const rule = createRule({
 				"Enforce that package.json has all properties required by the npm spec",
 			recommended: true,
 		},
-		schema: [],
+		schema: [
+			{
+				additionalProperties: false,
+				properties: {
+					ignoreProperties: {
+						items: {
+							type: "string",
+						},
+						type: "array",
+					},
+				},
+				type: "object",
+			},
+		],
 		type: "problem",
 	},
 });


Test patch:
diff --git a/src/tests/rules/valid-package-definition.test.ts b/src/tests/rules/valid-package-definition.test.ts
index da8b9dd..0d13342 100644
--- a/src/tests/rules/valid-package-definition.test.ts
+++ b/src/tests/rules/valid-package-definition.test.ts
@@ -78,5 +78,14 @@ ruleTester.run("valid-package-definition", rule, {
 }`,
 			filename: "package.json",
 		},
+		{
+			code: `{ "thee-silver": "mt-zion" }`,
+			filename: "packages/nested/package.json",
+			options: [
+				{
+					ignoreProperties: ["name", "version"],
+				},
+			],
+		},
 	],
 });