Skip to content

Commit

Permalink
Merge pull request #171 from ext/feature/exports-types-order
Browse files Browse the repository at this point in the history
feat: ensure typescript `types` comes first in `exports`
  • Loading branch information
ext committed May 2, 2023
2 parents ac42469 + 1e76b9b commit cd30241
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 0 deletions.
6 changes: 6 additions & 0 deletions README.md
Expand Up @@ -60,6 +60,12 @@ Verifies the presence of files specified in:
- `bin`
- `man`

## TypeScript `types` in `exports`

Requires `types` to be the first condition in `exports`.

**Why?** For TypeScript to properly detect `types` it need to come before `require` or `import`, else it will fall back to detecting by filename.

## Disallowed dependencies

Disallows certain packages from being included as `dependencies` (use `devDependencies` or `peerDependencies` instead).
Expand Down
2 changes: 2 additions & 0 deletions src/package-json.ts
Expand Up @@ -3,6 +3,7 @@ import { Message } from "./message";
import { Result } from "./result";
import { nonempty, present, typeArray, typeString, ValidationError, validUrl } from "./validators";
import { isDisallowedDependency } from "./rules/disallowed-dependency";
import { exportsTypesOrder } from "./rules/exports-types-order";
import { outdatedEngines } from "./rules/outdated-engines";
import { verifyEngineConstraint } from "./rules/verify-engine-constraint";
import { typesNodeMatchingEngine } from "./rules/types-node-matching-engine";
Expand Down Expand Up @@ -88,6 +89,7 @@ export async function verifyPackageJson(
): Promise<Result[]> {
const messages: Message[] = [
...(await verifyEngineConstraint(pkg)),
...exportsTypesOrder(pkg),
...verifyFields(pkg, options),
...verifyDependencies(pkg, options),
...outdatedEngines(pkg),
Expand Down
105 changes: 105 additions & 0 deletions src/rules/exports-types-order.spec.ts
@@ -0,0 +1,105 @@
import PackageJson from "../types/package-json";
import { exportsTypesOrder } from "./exports-types-order";

let pkg: PackageJson;

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

it("should report error when types isn't first", () => {
expect.assertions(1);
pkg.exports = {
require: "./index.cjs",
import: "./index.mjs",
types: "./index.d.ts",
};
expect(Array.from(exportsTypesOrder(pkg))).toMatchInlineSnapshot(`
[
{
"column": 1,
"line": 1,
"message": ""types" must be the first condition in "exports"",
"ruleId": "exports-types-order",
"severity": 2,
},
]
`);
});

it("should not report error when types is first", () => {
expect.assertions(1);
pkg.exports = {
types: "./index.d.ts",
require: "./index.cjs",
import: "./index.mjs",
};
expect(Array.from(exportsTypesOrder(pkg))).toMatchInlineSnapshot(`[]`);
});

it("should handle nested objects", () => {
expect.assertions(1);
pkg.exports = {
"./foo": {
require: "./foo.cjs",
import: "./foo.mjs",
types: "./foo.d.ts",
},
"./bar": {
deeply: {
nested: {
require: "./bar.cjs",
import: "./bar.mjs",
types: "./bar.d.ts",
},
},
},
};
expect(Array.from(exportsTypesOrder(pkg))).toMatchInlineSnapshot(`
[
{
"column": 1,
"line": 1,
"message": ""types" must be the first condition in "exports["./foo"]"",
"ruleId": "exports-types-order",
"severity": 2,
},
{
"column": 1,
"line": 1,
"message": ""types" must be the first condition in "exports["./bar"]["deeply"]["nested"]"",
"ruleId": "exports-types-order",
"severity": 2,
},
]
`);
});

it("should handle when exports are missing", () => {
expect.assertions(1);
expect(Array.from(exportsTypesOrder(pkg))).toMatchInlineSnapshot(`[]`);
});

it("should handle when exports is string", () => {
expect.assertions(1);
pkg.exports = "./index.js";
expect(Array.from(exportsTypesOrder(pkg))).toMatchInlineSnapshot(`[]`);
});

it("should handle when exports is empty", () => {
expect.assertions(1);
pkg.exports = {};
expect(Array.from(exportsTypesOrder(pkg))).toMatchInlineSnapshot(`[]`);
});

it("should handle when exports does not include types", () => {
expect.assertions(1);
pkg.exports = {
require: "./index.cjs",
import: "./index.mjs",
};
expect(Array.from(exportsTypesOrder(pkg))).toMatchInlineSnapshot(`[]`);
});
41 changes: 41 additions & 0 deletions src/rules/exports-types-order.ts
@@ -0,0 +1,41 @@
import { Severity } from "@html-validate/stylish";
import { Message } from "../message";
import PackageJson, { PackageJsonExports } from "../types/package-json";

const ruleId = "exports-types-order";
const severity = Severity.ERROR;

function* validateOrder(
value: string | PackageJsonExports | null,
path: string[]
): Generator<Message> {
if (!value || typeof value === "string") {
return;
}

const keys = Object.keys(value);
if (keys.length === 0) {
return;
}

if (keys.includes("types") && keys[0] !== "types") {
const property = path.map((it) => `["${it}"]`).join("");
yield {
ruleId,
severity,
message: `"types" must be the first condition in "exports${property}"`,
line: 1,
column: 1,
};
}

for (const key of keys) {
yield* validateOrder(value[key], [...path, key]);
}
}

export function* exportsTypesOrder(pkg: PackageJson): Generator<Message> {
if (pkg.exports) {
yield* validateOrder(pkg.exports, []);
}
}

0 comments on commit cd30241

Please sign in to comment.