Skip to content

Commit

Permalink
feat: add --ignore-node-version to disable outdated-engines node …
Browse files Browse the repository at this point in the history
…version error
  • Loading branch information
ext committed May 19, 2024
1 parent ddc872f commit 9869e06
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 12 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ optional arguments:
--ignore-missing-fields
ignore errors for missing fields (but still checks for
empty and valid)
--ignore-node-version [MAJOR]
ignore error for outdated node version (restricted to MAJOR version if given)
```

Use `--tarball` and `--pkgfile` to specify custom locations.
Expand Down Expand Up @@ -257,6 +259,8 @@ While stable Linux distributions (e.g. Debian stable) and enterprise environment
Users stuck at older versions will not be able to update to the latest set of node packages but if you are using an environment with unsupported versions you are unlikely to want to update node packages.
It is also very likely that the package doesn't actually run on such old version anyway because of a missing feature or a dependency requiring a later version.

This rule can be ignored with `--ignore-node-version`.

## Verify engine constraints

Requires `engines.node` to be satisfied by all transitive dependencies.
Expand Down
10 changes: 10 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ interface ParsedArgs {
pkgfile: string;
tarball?: string;
ignore_missing_fields?: boolean;
ignore_node_version: boolean | number;
allow_dependency: string[];
allow_types_dependencies?: boolean;
}
Expand Down Expand Up @@ -114,6 +115,14 @@ async function run(): Promise<void> {
action: "store_true",
help: "ignore errors for missing fields (but still checks for empty and valid)",
});
parser.add_argument("--ignore-node-version", {
nargs: "?",
metavar: "MAJOR",
type: "int",
default: false,
const: true,
help: "ignore error for outdated node version (restricted to MAJOR version if given)",
});

const args = parser.parse_args() as ParsedArgs;
const allowedDependencies = new Set(args.allow_dependency.map((it) => it.split(",")).flat());
Expand Down Expand Up @@ -155,6 +164,7 @@ async function run(): Promise<void> {
allowedDependencies,
allowTypesDependencies: args.allow_types_dependencies,
ignoreMissingFields: args.ignore_missing_fields,
ignoreNodeVersion: args.ignore_node_version,
};

const results = await verify(pkg, pkgPath, tarball, options);
Expand Down
6 changes: 6 additions & 0 deletions src/node-versions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ interface NodeVersionDescriptor {
eol?: string;
}

/* https://github.com/nodejs/Release/blob/main/schedule.json */
export const nodeVersions: Array<[string, NodeVersionDescriptor]> = [
["0.10.x", { eol: "2016-10-31" }],
["0.12.x", { eol: "2016-12-31" }],
Expand All @@ -20,4 +21,9 @@ export const nodeVersions: Array<[string, NodeVersionDescriptor]> = [
["15.x.x", { eol: "2021-06-01" }],
["16.x.x", { eol: "2023-09-11" }],
["17.x.x", { eol: "2022-06-01" }],
["18.x.x", {}],
["19.x.x", { eol: "2023-06-01" }],
["20.x.x", {}],
["21.x.x", {}],
["22.x.x", {}],
];
3 changes: 3 additions & 0 deletions src/package-json.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ it("should not return error if explicitly allowed by user", async () => {
};
const results = await verifyPackageJson(pkg, "package.json", {
allowedDependencies: new Set(["eslint"]),
ignoreNodeVersion: false,
});
expect(results).toHaveLength(0);
});
Expand Down Expand Up @@ -151,6 +152,7 @@ describe("@types", () => {
const results = await verifyPackageJson(pkg, "package.json", {
allowedDependencies: new Set(),
allowTypesDependencies: true,
ignoreNodeVersion: false,
});
expect(results).toHaveLength(0);
});
Expand All @@ -170,6 +172,7 @@ describe("present", () => {
const results = await verifyPackageJson(pkg, "package.json", {
allowedDependencies: new Set(),
ignoreMissingFields: true,
ignoreNodeVersion: false,
});
expect(results).toHaveLength(0);
});
Expand Down
6 changes: 4 additions & 2 deletions src/package-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface VerifyPackageJsonOptions {
allowedDependencies: Set<string>;
allowTypesDependencies?: boolean;
ignoreMissingFields?: boolean;
ignoreNodeVersion: boolean | number;
}

type validator = (key: string, value: unknown) => void;
Expand Down Expand Up @@ -129,15 +130,16 @@ function verifyDependencies(pkg: PackageJson, options: VerifyPackageJsonOptions)
export async function verifyPackageJson(
pkg: PackageJson,
filePath: string,
options: VerifyPackageJsonOptions = { allowedDependencies: new Set() },
options: VerifyPackageJsonOptions = { allowedDependencies: new Set(), ignoreNodeVersion: false },
): Promise<Result[]> {
const { ignoreNodeVersion } = options;
const messages: Message[] = [
...(await deprecatedDependency(pkg)),
...(await verifyEngineConstraint(pkg)),
...exportsTypesOrder(pkg),
...verifyFields(pkg, options),
...verifyDependencies(pkg, options),
...outdatedEngines(pkg),
...outdatedEngines(pkg, ignoreNodeVersion),
...typesNodeMatchingEngine(pkg),
];

Expand Down
61 changes: 56 additions & 5 deletions src/rules/outdated-engines.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ describe("should return error when unsupported version satisfies engines.node",
${">= 13.x"} | ${"Node 13"}
${">= 14.x"} | ${"Node 14"}
${">= 15.x"} | ${"Node 15"}
${">= 16.x"} | ${"Node 16"}
${">= 17.x"} | ${"Node 17"}
`("$description", ({ range, description }) => {
expect.assertions(1);
pkg.engines = {
Expand All @@ -38,7 +40,7 @@ describe("should return error when unsupported version satisfies engines.node",
const message = new RegExp(
String.raw`engines\.node is satisfied by ${description} \(EOL since \d{4}-.*\)`,
);
expect(Array.from(outdatedEngines(pkg))).toEqual([
expect(Array.from(outdatedEngines(pkg, false))).toEqual([
{
ruleId: "outdated-engines",
severity: Severity.ERROR,
Expand All @@ -50,12 +52,29 @@ describe("should return error when unsupported version satisfies engines.node",
});
});

describe("should allow supported version (including odd versions in-between)", () => {
it.each`
range | description
${">= 18.x"} | ${"Node 18"}
${">= 19.x"} | ${"Node 19"}
${">= 20.x"} | ${"Node 20"}
${">= 21.x"} | ${"Node 21"}
${">= 22.x"} | ${"Node 22"}
`("$description", ({ range }) => {
expect.assertions(1);
pkg.engines = {
node: range,
};
expect(Array.from(outdatedEngines(pkg, false))).toEqual([]);
});
});

it("should return error engines.node is not a valid semver range", () => {
expect.assertions(1);
pkg.engines = {
node: "foobar",
};
expect(Array.from(outdatedEngines(pkg))).toMatchInlineSnapshot(`
expect(Array.from(outdatedEngines(pkg, false))).toMatchInlineSnapshot(`
[
{
"column": 1,
Expand All @@ -71,7 +90,7 @@ it("should return error engines.node is not a valid semver range", () => {
it("should return error engines.node is missing", () => {
expect.assertions(1);
pkg.engines = {};
expect(Array.from(outdatedEngines(pkg))).toMatchInlineSnapshot(`
expect(Array.from(outdatedEngines(pkg, false))).toMatchInlineSnapshot(`
[
{
"column": 1,
Expand All @@ -87,7 +106,7 @@ it("should return error engines.node is missing", () => {
it("should return error engines is missing", () => {
expect.assertions(1);
delete pkg.engines;
expect(Array.from(outdatedEngines(pkg))).toMatchInlineSnapshot(`
expect(Array.from(outdatedEngines(pkg, false))).toMatchInlineSnapshot(`
[
{
"column": 1,
Expand All @@ -105,5 +124,37 @@ it("should not return error when engines.node only supports active versions", ()
pkg.engines = {
node: ">= 18",
};
expect(Array.from(outdatedEngines(pkg))).toMatchInlineSnapshot(`[]`);
expect(Array.from(outdatedEngines(pkg, false))).toMatchInlineSnapshot(`[]`);
});

it("should ignore outdated node version when ignoreNodeVersion is true", () => {
expect.assertions(1);
pkg.engines = {
node: ">= 16",
};
expect(Array.from(outdatedEngines(pkg, true))).toEqual([]);
});

it("should ignore outdated node version when ignoreNodeVersion is specific major", () => {
expect.assertions(1);
pkg.engines = {
node: ">= 16",
};
expect(Array.from(outdatedEngines(pkg, 16))).toEqual([]);
});

it("should yield error when ignoreNodeVersion does not match declared engines.node range", () => {
expect.assertions(1);
pkg.engines = {
node: ">= 18",
};
expect(Array.from(outdatedEngines(pkg, 16))).toEqual([
{
ruleId: "outdated-engines",
severity: 2,
message: '--ignore-node-version=16 used but engines.node=">= 18" does not match v16.x',
line: 1,
column: 1,
},
]);
});
41 changes: 37 additions & 4 deletions src/rules/outdated-engines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import { type PackageJson } from "../types";
const ruleId = "outdated-engines";
const severity = Severity.ERROR;

export function* outdatedEngines(pkg: PackageJson): Generator<Message> {
export function* outdatedEngines(
pkg: PackageJson,
ignoreNodeVersion: boolean | number,
): Generator<Message> {
if (!pkg.engines?.node) {
yield {
ruleId,
Expand All @@ -31,17 +34,32 @@ export function* outdatedEngines(pkg: PackageJson): Generator<Message> {
return;
}

if (ignoreNodeVersion === true) {
return;
}

for (const [version, descriptor] of nodeVersions) {
/* assume the list of versions are sorted: when a version not EOL is found
* we stop processing the list, e.g. `>= 18` is OK while Node 18 is not EOL
* even if Node 19 is EOL. */
if (!descriptor.eol) {
continue;
break;
}

const expanded = version.replace(/[xX*]/g, "999");
const parsed = semver.parse(expanded);
if (!semver.satisfies(expanded, range)) {
continue;
}
const nodeRelease = (parsed?.major ?? 0) || `0.${String(parsed?.minor ?? "")}`;

/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- parsing hardcoded values and covered by unit tests */
const parsed = semver.parse(expanded)!;

const { major, minor } = parsed;
if (ignoreNodeVersion === major) {
return;
}

const nodeRelease = major > 0 ? major : `0.${String(minor)}`;
const message = `engines.node is satisfied by Node ${String(nodeRelease)} (EOL since ${
descriptor.eol
})`;
Expand All @@ -54,4 +72,19 @@ export function* outdatedEngines(pkg: PackageJson): Generator<Message> {
};
return;
}

/* if we reached this far there was no error silenced by ignoreNodeVersion so
* we yield a new error informing that the ignore is no longer needed */
if (typeof ignoreNodeVersion === "number") {
const option = String(ignoreNodeVersion);
const version = `v${String(ignoreNodeVersion)}.x`;
const message = `--ignore-node-version=${option} used but engines.node="${range}" does not match ${version}`;
yield {
ruleId,
severity,
message,
line: 1,
column: 1,
};
}
}
5 changes: 4 additions & 1 deletion tests/integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ 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, { allowedDependencies: new Set() });
const result = await verify(pkg, pkgPath, tarball, {
allowedDependencies: new Set(),
ignoreNodeVersion: false,
});
expect(result).toMatchSnapshot();
});

0 comments on commit 9869e06

Please sign in to comment.