From 5fd881a2bf4050bc29907228d4830b217003c948 Mon Sep 17 00:00:00 2001 From: Kelly Selden Date: Tue, 23 Apr 2024 12:21:30 -0700 Subject: [PATCH] add ensure-workspaces rule --- README.md | 1 + docs/rules/ensure-workspaces.md | 58 +++++++++++++ lib/rules/ensure-workspaces.js | 68 +++++++++++++++ package-lock.json | 87 +++++++++---------- package.json | 1 + .../fixtures/workspaces/foo/bar/package.json | 1 + tests/fixtures/workspaces/foo/baz/.gitkeep | 0 tests/lib/rules/ensure-workspaces.js | 75 ++++++++++++++++ 8 files changed, 247 insertions(+), 44 deletions(-) create mode 100644 docs/rules/ensure-workspaces.md create mode 100644 lib/rules/ensure-workspaces.js create mode 100644 tests/fixtures/workspaces/foo/bar/package.json create mode 100644 tests/fixtures/workspaces/foo/baz/.gitkeep create mode 100644 tests/lib/rules/ensure-workspaces.js diff --git a/README.md b/README.md index c03abe8..a9422c5 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ eslint . --ext js,json |:--------|:------------| | [json-files/ensure-repository-directory](./docs/rules/ensure-repository-directory.md) | ensure repository/directory in package.json | | [json-files/ensure-volta-extends](./docs/rules/ensure-volta-extends.md) | ensure volta-extends in package.json | +| [json-files/ensure-workspaces](./docs/rules/ensure-workspaces.md) | ensure workspace globs in package.json resolve to directories | | [json-files/eol-last](./docs/rules/eol-last.md) | require or disallow newline at the end of package.json | | [json-files/no-branch-in-dependencies](./docs/rules/no-branch-in-dependencies.md) | prevent branches in package.json dependencies | | [json-files/require-engines](./docs/rules/require-engines.md) | require the engines field in package.json | diff --git a/docs/rules/ensure-workspaces.md b/docs/rules/ensure-workspaces.md new file mode 100644 index 0000000..411cb80 --- /dev/null +++ b/docs/rules/ensure-workspaces.md @@ -0,0 +1,58 @@ +# Ensure workspace globs in package.json resolve to directories (ensure-workspaces) + +Check that the monorepo workspace globs find dirs with package.json files. + + +## Rule Details + +This rule aims to ensure the workspace globs find dirs with package.json files. + +Examples of **incorrect** code for this rule: + +```json +{ + "workspace": [ + "packages/*/missing-dir" + ] +} +``` + +Examples of **correct** code for this rule: + +```json +{ + "workspace": [ + "packages/*/dir-with-package-json" + ] +} +``` + +```json +{ + "workspace": [ + "packages/dir-with-package-json" + ] +} +``` + +```json +{ + "workspace": { + "packages": [ + "packages/*/dir-with-package-json" + ] + } +} +``` + +### Options + + + +## When Not To Use It + +If workspace globs are placeholders for future packages. + +## Further Reading + +https://docs.npmjs.com/cli/v10/configuring-npm/package-json#workspaces diff --git a/lib/rules/ensure-workspaces.js b/lib/rules/ensure-workspaces.js new file mode 100644 index 0000000..7188e84 --- /dev/null +++ b/lib/rules/ensure-workspaces.js @@ -0,0 +1,68 @@ +'use strict'; + +const path = require('path'); +const fg = require('fast-glob'); + +module.exports = { + meta: { + docs: { + description: 'ensure workspace globs in package.json resolve to directories' + }, + schema: [ + ] + }, + + create(context) { + let filename = context.getFilename(); + if (path.basename(filename) !== 'package.json') { + return {}; + } + + return { + AssignmentExpression(node) { + let json = node.right; + let property = json.properties.find(p => p.key.value === 'workspaces'); + if (!property) { + return; + } + + let workspaces = property.value; + if (workspaces.type === 'ObjectExpression') { + let property = workspaces.properties.find(p => p.key.value === 'packages'); + if (!property) { + return; + } + + workspaces = property.value; + } + + if (workspaces.type !== 'ArrayExpression') { + return; + } + + for (let node of workspaces.elements) { + if (node.type !== 'Literal') { + continue; + } + + if (typeof node.value !== 'string') { + continue; + } + + let glob = path.join(node.value, 'package.json'); + + let entries = fg.sync(glob, { + cwd: path.dirname(filename) + }); + + if (!entries.length) { + context.report({ + node, + message: 'workspace path/glob does not match any workspaces with a package.json.' + }); + } + } + } + }; + } +}; diff --git a/package-lock.json b/package-lock.json index 4f32d85..1830363 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "ajv": "^8.2.0", "better-ajv-errors": "^1.2.0", + "fast-glob": "^3.3.2", "requireindex": "^1.2.0", "semver": "^7.0.0", "sort-package-json": "^1.22.1" @@ -1168,19 +1169,18 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz", - "integrity": "sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.0", + "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.2", - "picomatch": "^2.2.1" + "micromatch": "^4.0.4" }, "engines": { - "node": ">=8" + "node": ">=8.6.0" } }, "node_modules/fast-json-stable-stringify": { @@ -1654,15 +1654,15 @@ } }, "node_modules/micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "dependencies": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" + "braces": "^3.0.2", + "picomatch": "^2.3.1" }, "engines": { - "node": ">=8" + "node": ">=8.6" } }, "node_modules/minimatch": { @@ -1957,9 +1957,9 @@ } }, "node_modules/picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "engines": { "node": ">=8.6" }, @@ -3136,6 +3136,7 @@ "eslint-plugin-mocha": "^10.2.0", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prefer-let": "^3.0.1", + "fast-glob": "^3.3.2", "mocha": "^10.2.0", "mocha-helpers": "^9.0.0", "renovate-config-standard": "2.1.2", @@ -3965,16 +3966,15 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-glob": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz", - "integrity": "sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "requires": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.0", + "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.2", - "picomatch": "^2.2.1" + "micromatch": "^4.0.4" } }, "fast-json-stable-stringify": { @@ -4321,12 +4321,12 @@ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" }, "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" + "braces": "^3.0.2", + "picomatch": "^2.3.1" } }, "minimatch": { @@ -4550,9 +4550,9 @@ "dev": true }, "picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" }, "prelude-ls": { "version": "1.2.1", @@ -5037,16 +5037,15 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-glob": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz", - "integrity": "sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "requires": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.0", + "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.2", - "picomatch": "^2.2.1" + "micromatch": "^4.0.4" } }, "fast-json-stable-stringify": { @@ -5393,12 +5392,12 @@ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" }, "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" + "braces": "^3.0.2", + "picomatch": "^2.3.1" } }, "minimatch": { @@ -5622,9 +5621,9 @@ "dev": true }, "picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" }, "prelude-ls": { "version": "1.2.1", diff --git a/package.json b/package.json index 9950e9e..380e82c 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "dependencies": { "ajv": "^8.2.0", "better-ajv-errors": "^1.2.0", + "fast-glob": "^3.3.2", "requireindex": "^1.2.0", "semver": "^7.0.0", "sort-package-json": "^1.22.1" diff --git a/tests/fixtures/workspaces/foo/bar/package.json b/tests/fixtures/workspaces/foo/bar/package.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/tests/fixtures/workspaces/foo/bar/package.json @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/workspaces/foo/baz/.gitkeep b/tests/fixtures/workspaces/foo/baz/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/lib/rules/ensure-workspaces.js b/tests/lib/rules/ensure-workspaces.js new file mode 100644 index 0000000..baaf605 --- /dev/null +++ b/tests/lib/rules/ensure-workspaces.js @@ -0,0 +1,75 @@ +'use strict'; + +const { RuleTester } = require('eslint'); +const rule = require('../../../lib/rules/ensure-workspaces'); +const preprocess = require('../../helpers/preprocess'); + +new RuleTester().run('ensure-workspaces', rule, preprocess({ + valid: [ + { + code: '{ "workspaces": ["tests/fixtures/workspaces/*/bar"] }' + }, + { + code: '{}', + filename: 'package.json' + }, + { + code: '{ "workspaces": "literal" }', + filename: 'package.json' + }, + { + code: '{ "workspaces": [{}] }', + filename: 'package.json' + }, + { + code: '{ "workspaces": [0] }', + filename: 'package.json' + }, + { + code: '{ "workspaces": ["tests/fixtures/workspaces/*/bar"] }', + filename: 'package.json' + }, + { + code: '{ "workspaces": ["tests/fixtures/workspaces/*/bar"] }', + filename: 'package.json' + }, + { + code: '{ "workspaces": {} }', + filename: 'package.json' + }, + { + code: '{ "workspaces": { "packages": "literal" } }', + filename: 'package.json' + }, + { + code: '{ "workspaces": { "packages": [{}] } }', + filename: 'package.json' + }, + { + code: '{ "workspaces": { "packages": [0] } }', + filename: 'package.json' + }, + { + code: '{ "workspaces": { "packages": ["tests/fixtures/workspaces/*/bar"] } }', + filename: 'package.json' + } + ], + invalid: [ + { + code: '{ "workspaces": ["tests/fixtures/workspaces/*/baz"] }', + filename: 'package.json', + errors: [{ + message: 'workspace path/glob does not match any workspaces with a package.json.', + type: 'Literal' + }] + }, + { + code: '{ "workspaces": { "packages": ["tests/fixtures/workspaces/*/baz"] } }', + filename: 'package.json', + errors: [{ + message: 'workspace path/glob does not match any workspaces with a package.json.', + type: 'Literal' + }] + } + ] +}));