Skip to content

Commit

Permalink
feat: check for unsupported node versions
Browse files Browse the repository at this point in the history
  • Loading branch information
ext committed Nov 21, 2020
1 parent 21d339c commit c788c00
Show file tree
Hide file tree
Showing 14 changed files with 227 additions and 5 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,15 @@ Verifies the following fields:
It also enforces all urls to be `https`, even the repository url.
While `git` is technically valid most users cannot clone the repository anonomously.
Shortcuts are not permitted either because it saves basically nothing, makes tooling more difficult to write and wont work for smaller hosting services.

## Unsupported node versions

Requires `engines.node` to be up-to-date and only supporting LTS and active versions.

**Why?** Newer versions contains more builtin functions and features replacing the need for polyfills and many one-liner packages.

As an example `mkdirp` can be replaced with `fs.mkdir(p, { recursive: true })` starting with Node 10.

While stable Linux distributions (e.g. Debian stable) and enterprise environment might not use the most recent versions they often try to stay away from EOL versions.
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.
9 changes: 7 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"@html-validate/stylish": "^1.0.0",
"argparse": "^2.0.1",
"find-up": "^5.0.0",
"semver": "^7.3.2",
"tar": "^6.0.5",
"tmp": "^0.2.1"
},
Expand All @@ -75,6 +76,7 @@
"@types/glob": "7.1.3",
"@types/jest": "26.0.15",
"@types/node": "11.15.37",
"@types/semver": "^7.3.4",
"@types/tar": "4.0.4",
"@types/tmp": "0.2.0",
"@typescript-eslint/eslint-plugin": "4.8.1",
Expand Down
12 changes: 12 additions & 0 deletions src/__snapshots__/package-json.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,18 @@ Array [
]
`;

exports[`should return engines.node supports eol version 1`] = `
Array [
Object {
"column": 1,
"line": 1,
"message": "engines.node is satisfied by Node 8 (EOL since 2019-12-31)",
"ruleId": "outdated-engines",
"severity": 2,
},
]
`;

exports[`should return error if dependency is disallowed 1`] = `
Array [
Object {
Expand Down
12 changes: 12 additions & 0 deletions src/package-json.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ beforeEach(() => {
license: "UNLICENSED",
author: "Fred Flintstone <fred.flintstone@example.net>",
repository: "https://git.example.net/test-case.git",
engines: {
node: ">= 10",
},
};
});

Expand Down Expand Up @@ -50,6 +53,15 @@ it("should not return error if dependency is allowed", async () => {
expect(results).toEqual([]);
});

it("should return engines.node supports eol version", async () => {
expect.assertions(3);
pkg.engines.node = ">= 8";
const results = await verifyPackageJson(pkg, "package.json");
expect(results).toHaveLength(1);
expect(results[0].filePath).toEqual("package.json");
expect(results[0].messages).toMatchSnapshot();
});

describe("@types", () => {
beforeEach(() => {
pkg.dependencies = {
Expand Down
7 changes: 6 additions & 1 deletion src/package-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Message } from "./message";
import { Result } from "./result";
import { nonempty, present, typeArray, typeString, validUrl } from "./validators";
import { isDisallowedDependency } from "./rules/disallowed-dependency";
import { outdatedEngines } from "./rules/outdated-engines";

export interface VerifyPackageJsonOptions {
allowTypesDependencies?: boolean;
Expand Down Expand Up @@ -75,7 +76,11 @@ export async function verifyPackageJson(
filePath: string,
options: VerifyPackageJsonOptions = {}
): Promise<Result[]> {
const messages: Message[] = [...verifyFields(pkg, options), ...verifyDependencies(pkg, options)];
const messages: Message[] = [
...verifyFields(pkg, options),
...verifyDependencies(pkg, options),
...outdatedEngines(pkg),
];

if (messages.length === 0) {
return [];
Expand Down
95 changes: 95 additions & 0 deletions src/rules/outdated-engines.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Severity } from "@html-validate/stylish/dist/severity";
import PackageJson from "../types/package-json";
import { outdatedEngines } from "./outdated-engines";

let pkg: PackageJson;
const ruleId = "outdated-engines";
const severity = Severity.ERROR;

beforeEach(() => {
pkg = {
name: "mock-package",
version: "1.2.3",
engines: {},
};
});

describe("should return error when unsupported version satisfies engines.node", () => {
it.each`
range | description | date
${">= 0.10.x"} | ${"Node 0.10"} | ${"2016-10-31"}
${">= 0.12.x"} | ${"Node 0.12"} | ${"2016-12-31"}
${">= 4.x"} | ${"Node 4"} | ${"2018-04-30"}
${">= 5.x"} | ${"Node 5"} | ${"2016-06-30"}
${">= 6.x"} | ${"Node 6"} | ${"2019-04-30"}
${">= 7.x"} | ${"Node 7"} | ${"2017-06-30"}
${">= 8.x"} | ${"Node 8"} | ${"2019-12-31"}
${">= 9.x"} | ${"Node 9"} | ${"2018-06-30"}
`("$description", ({ range, description, date }) => {
expect.assertions(1);
pkg.engines.node = range;
expect(Array.from(outdatedEngines(pkg))).toEqual([
{
ruleId: "outdated-engines",
severity: Severity.ERROR,
message: `engines.node is satisfied by ${description} (EOL since ${date})`,
line: 1,
column: 1,
},
]);
});
});

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(`
Array [
Object {
"column": 1,
"line": 1,
"message": "engines.node \\"foobar\\" is not a valid semver range",
"ruleId": "outdated-engines",
"severity": 2,
},
]
`);
});

it("should return error engines.node is missing", () => {
expect.assertions(1);
delete pkg.engines.node;
expect(Array.from(outdatedEngines(pkg))).toMatchInlineSnapshot(`
Array [
Object {
"column": 1,
"line": 1,
"message": "Missing engines.node field",
"ruleId": "outdated-engines",
"severity": 2,
},
]
`);
});

it("should return error engines is missing", () => {
expect.assertions(1);
delete pkg.engines;
expect(Array.from(outdatedEngines(pkg))).toMatchInlineSnapshot(`
Array [
Object {
"column": 1,
"line": 1,
"message": "Missing engines.node field",
"ruleId": "outdated-engines",
"severity": 2,
},
]
`);
});

it("should not return error when engines.node only supports active versions", () => {
expect.assertions(1);
pkg.engines.node = ">= 10";
expect(Array.from(outdatedEngines(pkg))).toMatchInlineSnapshot(`Array []`);
});
63 changes: 63 additions & 0 deletions src/rules/outdated-engines.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import semver from "semver";
import { Severity } from "@html-validate/stylish/dist/severity";
import { Message } from "../message";
import PackageJson from "../types/package-json";

const ruleId = "outdated-engines";
const severity = Severity.ERROR;

interface EOLDescriptor {
date: string;
}

const EOL: [string, EOLDescriptor][] = [
["0.10.99", { date: "2016-10-31" }],
["0.12.99", { date: "2016-12-31" }],
["4.99.99", { date: "2018-04-30" }],
["5.99.99", { date: "2016-06-30" }],
["6.99.99", { date: "2019-04-30" }],
["7.99.99", { date: "2017-06-30" }],
["8.99.99", { date: "2019-12-31" }],
["9.99.99", { date: "2018-06-30" }],
];

export function* outdatedEngines(pkg: PackageJson): Generator<Message> {
if (!pkg.engines || !pkg.engines.node) {
yield {
ruleId,
severity,
message: "Missing engines.node field",
line: 1,
column: 1,
};
return;
}

const range = pkg.engines.node;
if (!semver.validRange(range)) {
yield {
ruleId,
severity,
message: `engines.node "${range}" is not a valid semver range`,
line: 1,
column: 1,
};
return;
}

for (const [version, descriptor] of EOL) {
const parsed = semver.parse(version);
if (semver.satisfies(version, range)) {
yield {
ruleId,
severity,
message: `engines.node is satisfied by Node ${
parsed.major || `0.${parsed.minor}`
} (EOL since ${descriptor.date})`,
line: 1,
column: 1,
};
return;
}
}
}
1 change: 1 addition & 0 deletions src/types/package-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export default interface PackageJson {
peerDependencies?: Record<string, string>;
bundledDependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
engines?: Record<string, string>;

[key: string]:
| string
Expand Down
3 changes: 3 additions & 0 deletions tests/fixtures/disallowed-files/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,8 @@
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"engines": {
"node": ">= 10"
},
"license": "ISC"
}
5 changes: 4 additions & 1 deletion tests/fixtures/empty-fields/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,8 @@
"author": "",
"repository": {},
"main": "",
"license": ""
"license": "",
"engines": {
"node": ">= 10"
}
}
3 changes: 3 additions & 0 deletions tests/fixtures/missing-binary/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,8 @@
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"engines": {
"node": ">= 10"
},
"license": "ISC"
}
5 changes: 4 additions & 1 deletion tests/fixtures/missing-fields/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"name": "missing-fields",
"version": "1.0.0",
"files": []
"files": [],
"engines": {
"node": ">= 10"
}
}
3 changes: 3 additions & 0 deletions tests/fixtures/missing-main/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,8 @@
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"engines": {
"node": ">= 10"
},
"license": "ISC"
}

0 comments on commit c788c00

Please sign in to comment.