routie dev init since i didn't adhere to any proper guidance up until now

This commit is contained in:
2026-04-29 22:27:29 -06:00
commit e1dabb71e2
15301 changed files with 3562618 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2025-PRESENT Anthony Fu <https://github.com/antfu>
Copyright (c) 2025-PRESENT 三咲智子 Kevin Deng <https://github.com/sxzz>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+130
View File
@@ -0,0 +1,130 @@
# eslint-plugin-pnpm
[![npm version][npm-version-src]][npm-version-href]
[![npm downloads][npm-downloads-src]][npm-downloads-href]
[![bundle][bundle-src]][bundle-href]
[![JSDocs][jsdocs-src]][jsdocs-href]
[![License][license-src]][license-href]
ESLint plugin to enforce and auto-fix pnpm catalogs.
This plugin consists of two set of rules that applies to `package.json` and `pnpm-workspace.yaml` respectively.
- [`json-` rules](./src/rules/json) applies to `package.json` and requires [`jsonc-eslint-parser`](https://github.com/ota-meshi/jsonc-eslint-parser) to be used as parser.
- [`yaml-` rules](./src/rules/yaml) applies to `pnpm-workspace.yaml` and requires [`yaml-eslint-parser`](https://github.com/ota-meshi/yaml-eslint-parser) to be used as parser.
- YAML support is still experimental as it might have race conditions with other plugins.
## Setup
```bash
pnpm add -D eslint-plugin-pnpm
```
### Basic Usage
```js
// eslint.config.mjs
import { configs } from 'eslint-plugin-pnpm'
export default [
{
ignores: ['**/node_modules/**', '**/dist/**'],
},
...configs.json,
...configs.yaml,
]
```
### Manual Configuration
```js
// eslint.config.mjs
import pluginPnpm from 'eslint-plugin-pnpm'
import * as jsoncParser from 'jsonc-eslint-parser'
import * as yamlParser from 'yaml-eslint-parser'
export default [
{
ignores: ['**/node_modules/**', '**/dist/**'],
},
{
name: 'pnpm/package.json',
files: [
'package.json',
'**/package.json',
],
languageOptions: {
parser: jsoncParser,
},
plugins: {
pnpm: pluginPnpm,
},
rules: {
'pnpm/json-enforce-catalog': 'error',
'pnpm/json-valid-catalog': 'error',
'pnpm/json-prefer-workspace-settings': 'error',
},
},
{
name: 'pnpm/pnpm-workspace-yaml',
files: ['pnpm-workspace.yaml'],
languageOptions: {
parser: yamlParser,
},
plugins: {
pnpm: pluginPnpm,
},
rules: {
'pnpm/yaml-no-unused-catalog-item': 'error',
'pnpm/yaml-no-duplicate-catalog-item': 'error',
'pnpm/yaml-valid-packages': 'error',
},
},
]
```
## Rules
### JSON Rules (`package.json`)
- [`json-enforce-catalog`](./src/rules/json/json-enforce-catalog.ts) - Enforce catalog usage for dependencies
- [`json-valid-catalog`](./src/rules/json/json-valid-catalog.ts) - Validate catalog references in dependencies
- [`json-prefer-workspace-settings`](./src/rules/json/json-prefer-workspace-settings.ts) - Prefer workspace protocol for local dependencies
### YAML Rules (`pnpm-workspace.yaml`)
- [`yaml-no-unused-catalog-item`](./src/rules/yaml/yaml-no-unused-catalog-item.ts) - Disallow unused catalog items
- [`yaml-no-duplicate-catalog-item`](./src/rules/yaml/yaml-no-duplicate-catalog-item.ts) - Disallow duplicate catalog items
- [`yaml-valid-packages`](./src/rules/yaml/yaml-valid-packages.ts) - Ensure package patterns match directories with package.json
- [`yaml-enforce-settings`](./src/rules/yaml/yaml-enforce-settings.ts) - Enforce settings in `pnpm-workspace.yaml`
## Settings
| Name | Description | Type | Default |
| --------------------- | ----------------------------------------------------------- | ------- | ------- |
| `ensureWorkspaceFile` | Whether to create `pnpm-workspace.yaml` if it doesn't exist | boolean | false |
## Sponsors
<p align="center">
<a href="https://cdn.jsdelivr.net/gh/antfu/static/sponsors.svg">
<img src='https://cdn.jsdelivr.net/gh/antfu/static/sponsors.svg' alt="antfu's sponsors"/>
</a>
</p>
## License
[MIT](./LICENSE) License © [Anthony Fu](https://github.com/antfu)
<!-- Badges -->
[npm-version-src]: https://img.shields.io/npm/v/eslint-plugin-pnpm?style=flat&colorA=080f12&colorB=1fa669
[npm-version-href]: https://npmjs.com/package/eslint-plugin-pnpm
[npm-downloads-src]: https://img.shields.io/npm/dm/eslint-plugin-pnpm?style=flat&colorA=080f12&colorB=1fa669
[npm-downloads-href]: https://npmjs.com/package/eslint-plugin-pnpm
[bundle-src]: https://img.shields.io/bundlephobia/minzip/eslint-plugin-pnpm?style=flat&colorA=080f12&colorB=1fa669&label=minzip
[bundle-href]: https://bundlephobia.com/result?p=eslint-plugin-pnpm
[license-src]: https://img.shields.io/github/license/antfu/pnpm-workspace-utils.svg?style=flat&colorA=080f12&colorB=1fa669
[license-href]: https://github.com/antfu/pnpm-workspace-utils/blob/main/LICENSE.md
[jsdocs-src]: https://img.shields.io/badge/jsdocs-reference-080f12?style=flat&colorA=080f12&colorB=1fa669
[jsdocs-href]: https://www.jsdocs.io/package/eslint-plugin-pnpm
+11
View File
@@ -0,0 +1,11 @@
import { ESLint, Linter } from "eslint";
//#region src/index.d.ts
declare const plugin: ESLint.Plugin;
declare const configs: {
recommended: Linter.Config[];
json: Linter.Config[];
yaml: Linter.Config[];
};
//#endregion
export { configs, plugin as default, plugin };
+868
View File
@@ -0,0 +1,868 @@
import * as jsoncParser from "jsonc-eslint-parser";
import * as yamlParser from "yaml-eslint-parser";
import fs, { existsSync, readFileSync } from "node:fs";
import { up } from "empathic/find";
import { basename, dirname, join, normalize, resolve } from "pathe";
import { parsePnpmWorkspaceYaml } from "pnpm-workspace-yaml";
import yaml from "yaml";
import { globSync } from "tinyglobby";
//#region package.json
var name = "eslint-plugin-pnpm";
var version = "1.6.0";
//#endregion
//#region src/utils/create.ts
const blobUrl = "https://github.com/antfu/pnpm-workspace-utils/tree/main/packages/eslint-plugin-pnpm/src/rules/";
/**
* Creates reusable function to create rules with default options and docs URLs.
*
* @param urlCreator Creates a documentation URL for a given rule name.
* @returns Function to create a rule with the docs URL format.
*/
function RuleCreator(urlCreator) {
return function createNamedRule({ name: name$1, meta, ...rule }) {
return createRule({
name: name$1,
meta: {
...meta,
docs: {
...meta.docs,
url: urlCreator(name$1)
}
},
...rule
});
};
}
/**
* Creates a well-typed TSESLint custom ESLint rule without a docs URL.
*
* @returns Well-typed TSESLint custom ESLint rule.
* @remarks It is generally better to provide a docs URL function to RuleCreator.
*/
function createRule({ name: name$1, create, defaultOptions, meta }) {
return {
name: name$1,
create: ((context) => {
return create(context, context.options.map((options, index) => {
return {
...defaultOptions?.[index] || {},
...options || {}
};
}));
}),
defaultOptions,
meta
};
}
const createEslintRule = RuleCreator((ruleName) => `${blobUrl}${ruleName.startsWith("json-") ? `json/${ruleName}` : `yaml/${ruleName}`}.test.ts`);
//#endregion
//#region src/utils/iterate.ts
function getPackageJsonRootNode(context) {
if (!context.filename.endsWith("package.json")) return;
const root = context.sourceCode.ast.body[0];
if (root.expression.type === "JSONObjectExpression") return root.expression;
}
function* iterateDependencies(context, fields) {
const root = getPackageJsonRootNode(context);
if (!root) return;
for (const fieldName of fields) {
const path = fieldName.split(".");
let node = root;
for (let i = 0; i < path.length; i++) {
const item = node.properties.find((property) => property.key.type === "JSONLiteral" && property.key.value === path[i]);
if (!item?.value || item.value.type !== "JSONObjectExpression") {
node = void 0;
break;
}
node = item.value;
}
if (!node || node === root) continue;
for (const property of node.properties) {
if (property.value.type !== "JSONLiteral" || property.key.type !== "JSONLiteral") continue;
if (typeof property.value.value !== "string") continue;
yield {
packageName: String(property.key.value),
specifier: String(property.value.value),
property
};
}
}
}
//#endregion
//#region src/utils/_read.ts
function findPnpmWorkspace(sourceFile) {
return up("pnpm-workspace.yaml", { cwd: dirname(sourceFile) });
}
function createPnpmWorkspace(sourceFile) {
const file = up("package.json", { cwd: dirname(sourceFile) });
if (!file) throw new Error("Could not find package.json to create pnpm-workspace.yaml");
const workspacePath = resolve(dirname(file), "pnpm-workspace.yaml");
fs.writeFileSync(workspacePath, "");
return workspacePath;
}
function readPnpmWorkspace(filepath) {
const workspace = parsePnpmWorkspaceYaml(fs.readFileSync(filepath, "utf-8"));
let queueTimer;
const queue = [];
const write = () => {
fs.writeFileSync(filepath, workspace.toString());
};
const hasQueue = () => queueTimer != null;
const queueChange = (fn, order) => {
if (order === "pre") queue.unshift(fn);
else queue.push(fn);
if (queueTimer != null) clearTimeout(queueTimer);
queueTimer = setTimeout(() => {
queueTimer = void 0;
const clone = [...queue];
queue.length = 0;
for (const fn$1 of clone) fn$1(workspace);
if (workspace.hasChanged()) write();
}, 1e3);
};
return {
filepath,
lastRead: Date.now(),
...workspace,
hasQueue,
queueChange
};
}
//#endregion
//#region src/utils/workspace.ts
const WORKSPACE_CACHE_TIME = 1e4;
const workspaces = {};
function getPnpmWorkspace(context) {
const sourcePath = context.filename;
const ensureWorkspaceFile = context.settings.pnpm?.ensureWorkspaceFile;
let workspacePath = findPnpmWorkspace(sourcePath);
if (!workspacePath) if (ensureWorkspaceFile) workspacePath = createPnpmWorkspace(sourcePath);
else throw new Error("pnpm-workspace.yaml not found");
let workspace = workspaces[workspacePath];
if (workspace && !workspace.hasQueue() && Date.now() - workspace.lastRead > WORKSPACE_CACHE_TIME) {
workspaces[workspacePath] = void 0;
workspace = void 0;
}
if (!workspace) {
workspace = readPnpmWorkspace(workspacePath);
workspaces[workspacePath] = workspace;
}
return workspace;
}
//#endregion
//#region src/rules/json/json-enforce-catalog.ts
const RULE_NAME$6 = "json-enforce-catalog";
const DEFAULT_FIELDS$1 = ["dependencies", "devDependencies"];
var json_enforce_catalog_default = createEslintRule({
name: RULE_NAME$6,
meta: {
type: "layout",
docs: { description: "Enforce using \"catalog:\" in `package.json`" },
fixable: "code",
schema: [{
type: "object",
properties: {
allowedProtocols: {
type: "array",
description: "Allowed protocols in specifier to not be converted to catalog",
items: { type: "string" }
},
autofix: {
type: "boolean",
description: "Whether to autofix the linting error",
default: true
},
defaultCatalog: {
type: "string",
description: "Default catalog to use when moving version to catalog with autofix"
},
reuseExistingCatalog: {
type: "boolean",
description: "Whether to reuse existing catalog when moving version to catalog with autofix",
default: true
},
conflicts: {
type: "string",
description: "Strategy to handle conflicts when adding packages to catalogs",
enum: [
"new-catalog",
"overrides",
"error"
],
default: "new-catalog"
},
fields: {
type: "array",
description: "Fields to check for catalog",
items: { type: "string" },
default: DEFAULT_FIELDS$1
},
ignores: {
type: "array",
description: "Ignore certain packages that require version specification",
items: { type: "string" },
default: []
}
},
additionalProperties: false
}],
messages: { expectCatalog: "Expect to use catalog instead of plain specifier, got \"{{specifier}}\" for package \"{{packageName}}\"." }
},
defaultOptions: [{}],
create(context, [options]) {
const { allowedProtocols = [
"workspace",
"link",
"file"
], defaultCatalog = "default", autofix = true, reuseExistingCatalog = true, conflicts = "new-catalog", fields = DEFAULT_FIELDS$1, ignores = [] } = options || {};
for (const { packageName, specifier, property } of iterateDependencies(context, fields)) {
if (specifier.startsWith("catalog:") || ignores.includes(packageName)) continue;
if (allowedProtocols?.some((p) => specifier.startsWith(p))) continue;
const workspace = getPnpmWorkspace(context);
if (!workspace) return {};
let targetCatalog = reuseExistingCatalog ? workspace.getPackageCatalogs(packageName)[0] || defaultCatalog : defaultCatalog;
const resolvedConflicts = workspace.hasSpecifierConflicts(targetCatalog, packageName, specifier);
let shouldFix = autofix;
if (conflicts === "error") {
if (resolvedConflicts.conflicts) shouldFix = false;
}
if (conflicts === "new-catalog" && resolvedConflicts.conflicts) targetCatalog = resolvedConflicts.newCatalogName;
context.report({
node: property.value,
messageId: "expectCatalog",
data: {
specifier,
packageName
},
fix: shouldFix ? (fixer) => {
workspace.queueChange(() => {
workspace.setPackage(targetCatalog, packageName, specifier);
});
return fixer.replaceText(property.value, targetCatalog === "default" ? JSON.stringify("catalog:") : JSON.stringify(`catalog:${targetCatalog}`));
} : void 0
});
}
return {};
}
});
//#endregion
//#region src/rules/json/json-prefer-workspace-settings.ts
const RULE_NAME$5 = "json-prefer-workspace-settings";
var json_prefer_workspace_settings_default = createEslintRule({
name: RULE_NAME$5,
meta: {
type: "layout",
docs: { description: "Prefer having pnpm settings in `pnpm-workspace.yaml` instead of `package.json`. This requires pnpm v10.6+, see https://github.com/orgs/pnpm/discussions/9037." },
fixable: "code",
schema: [{
type: "object",
properties: { autofix: {
type: "boolean",
description: "Whether to autofix the linting error",
default: true
} },
additionalProperties: false
}],
messages: { unexpectedPnpmSettings: "Unexpected pnpm settings in package.json, should move to pnpm-workspace.yaml" }
},
defaultOptions: [{}],
create(context, [options = {}]) {
const { autofix = true } = options || {};
const root = getPackageJsonRootNode(context);
if (!root) return {};
const pnpmNode = root.properties.find((property) => property.key.type === "JSONLiteral" && property.key.value === "pnpm");
if (!pnpmNode) return {};
const workspace = getPnpmWorkspace(context);
if (!workspace) return {};
context.report({
node: pnpmNode,
messageId: "unexpectedPnpmSettings",
fix: autofix ? (fixer) => {
const pnpmSettings = JSON.parse(context.sourceCode.text).pnpm;
const flatValueParis = [];
function traverse(value, paths) {
if (typeof value === "object" && value !== null && !Array.isArray(value)) for (const key in value) traverse(value[key], [...paths, key]);
else flatValueParis.push([paths, value]);
}
traverse(pnpmSettings, []);
workspace.queueChange(() => {
for (const [paths, value] of flatValueParis) workspace.setPath(paths, value);
});
let start = pnpmNode.range[0];
let end = pnpmNode.range[1];
const before = context.sourceCode.getTokenBefore(pnpmNode);
if (before) start = before.range[1];
const after = context.sourceCode.getTokenAfter(pnpmNode);
if (after?.type === "Punctuator" && after.value === ",") end = after.range[1];
return fixer.removeRange([start, end]);
} : void 0
});
return {};
}
});
//#endregion
//#region src/rules/json/json-valid-catalog.ts
const RULE_NAME$4 = "json-valid-catalog";
const DEFAULT_FIELDS = [
"dependencies",
"devDependencies",
"optionalDependencies",
"peerDependencies",
"resolutions",
"overrides",
"pnpm.overrides"
];
var json_valid_catalog_default = createEslintRule({
name: RULE_NAME$4,
meta: {
type: "layout",
docs: { description: "Enforce using valid catalog in `package.json`" },
fixable: "code",
schema: [{
type: "object",
properties: {
autoInsert: {
type: "boolean",
description: "Whether to auto insert to catalog if missing",
default: true
},
autoInsertDefaultSpecifier: {
type: "string",
description: "Default specifier to use when auto inserting to catalog",
default: "^0.0.0"
},
autofix: {
type: "boolean",
description: "Whether to autofix the linting error",
default: true
},
enforceNoConflict: {
type: "boolean",
description: "Whether to enforce no conflicts when adding packages to catalogs (will create version-specific catalogs)",
default: true
},
fields: {
type: "array",
description: "Fields to check for catalog",
default: DEFAULT_FIELDS
}
},
additionalProperties: false
}],
messages: { invalidCatalog: "Catalog \"{{specifier}}\" for package \"{{packageName}}\" is not defined in `pnpm-workspace.yaml`." }
},
defaultOptions: [{}],
create(context, [options = {}]) {
const { autoInsert = true, autofix = true, autoInsertDefaultSpecifier = "^0.0.0", fields = DEFAULT_FIELDS } = options || {};
for (const { packageName, specifier, property } of iterateDependencies(context, fields)) {
if (!specifier.startsWith("catalog:")) continue;
const workspace = getPnpmWorkspace(context);
if (!workspace) return {};
const currentCatalog = specifier.replace(/^catalog:/, "").trim() || "default";
const existingCatalogs = workspace.getPackageCatalogs(packageName);
if (!existingCatalogs.includes(currentCatalog)) context.report({
node: property.value,
messageId: "invalidCatalog",
data: {
specifier,
packageName
},
fix: !autofix || !autoInsert && !existingCatalogs.length ? void 0 : (fixer) => {
let catalog = existingCatalogs[0];
if (!catalog && autoInsert) {
catalog = currentCatalog;
workspace.queueChange(() => {
workspace.setPackage(catalog, packageName, autoInsertDefaultSpecifier);
}, "pre");
}
return fixer.replaceText(property.value, catalog === "default" ? JSON.stringify("catalog:") : JSON.stringify(`catalog:${catalog}`));
}
});
}
return {};
}
});
//#endregion
//#region src/rules/json/index.ts
const rules$2 = {
"json-enforce-catalog": json_enforce_catalog_default,
"json-valid-catalog": json_valid_catalog_default,
"json-prefer-workspace-settings": json_prefer_workspace_settings_default
};
//#endregion
//#region ../../node_modules/.pnpm/@antfu+utils@9.3.0/node_modules/@antfu/utils/dist/index.mjs
const toString = (v) => Object.prototype.toString.call(v);
function getTypeName(v) {
if (v === null) return "null";
const type = toString(v).slice(8, -1).toLowerCase();
return typeof v === "object" || typeof v === "function" ? type : typeof v;
}
function isDeepEqual(value1, value2) {
const type1 = getTypeName(value1);
if (type1 !== getTypeName(value2)) return false;
if (type1 === "array") {
if (value1.length !== value2.length) return false;
return value1.every((item, i) => {
return isDeepEqual(item, value2[i]);
});
}
if (type1 === "object") {
const keyArr = Object.keys(value1);
if (keyArr.length !== Object.keys(value2).length) return false;
return keyArr.every((key) => {
return isDeepEqual(value1[key], value2[key]);
});
}
return Object.is(value1, value2);
}
//#endregion
//#region src/rules/yaml/yaml-enforce-settings.ts
const RULE_NAME$3 = "yaml-enforce-settings";
var yaml_enforce_settings_default = createEslintRule({
name: RULE_NAME$3,
meta: {
type: "problem",
fixable: "code",
docs: { description: "Enforce settings in `pnpm-workspace.yaml`" },
schema: [{
type: "object",
additionalProperties: false,
properties: {
autofix: {
type: "boolean",
description: "Whether to autofix the linting error",
default: true
},
settings: {
description: "Exact settings to enforce, for both keys and values. Auto-fixable.",
type: "object"
},
requiredFields: {
description: "Required settings fields to enforce, regardless of their values. Not-autofixable.",
type: "array",
items: { type: "string" }
},
forbiddenFields: {
description: "Forbidden settings fields to enforce, regardless of their values. Not-autofixable.",
type: "array",
items: { type: "string" }
}
}
}],
messages: {
requiredFieldsMissing: "Required settings fields {{keys}} are missing in `pnpm-workspace.yaml`.",
settingMissing: "Setting \"{{key}}\" is missing in `pnpm-workspace.yaml`.",
settingMismatch: "Setting \"{{key}}\" has mismatch value. Expected: {{expected}}, Actual: {{actual}}.",
forbiddenFieldFound: "Forbidden setting field \"{{key}}\" is found in `pnpm-workspace.yaml`."
}
},
defaultOptions: [{
autofix: true,
settings: {},
requiredFields: [],
forbiddenFields: []
}],
create(context, [options = {}]) {
const { autofix = true, settings = {}, requiredFields = [], forbiddenFields = [] } = options || {};
if (Object.keys(settings).length === 0 && requiredFields.length === 0 && forbiddenFields.length === 0) throw new Error("Either `settings` or `requiredFields` or `forbiddenFields` must be provided, this rule is not functional currently.");
if (basename(context.filename) !== "pnpm-workspace.yaml") return {};
const workspace = getPnpmWorkspace(context);
if (!workspace || normalize(workspace.filepath) !== normalize(context.filename)) return {};
if (workspace.hasChanged() || workspace.hasQueue()) return {};
workspace.setContent(context.sourceCode.text);
const parsed = workspace.toJSON() || {};
const doc = workspace.getDocument();
const missingFields = [];
for (const key of requiredFields) if (!Object.hasOwn(parsed, key)) missingFields.push(key);
if (missingFields.length > 0) context.report({
loc: {
start: context.sourceCode.getLocFromIndex(0),
end: context.sourceCode.getLocFromIndex(0)
},
messageId: "requiredFieldsMissing",
data: { keys: missingFields.map((i) => JSON.stringify(i)).join(", ") }
});
for (const key of forbiddenFields) {
const node = doc.getIn([key], true);
if (!node) continue;
context.report({
loc: {
start: context.sourceCode.getLocFromIndex(node?.range?.[0] ?? 0),
end: context.sourceCode.getLocFromIndex(node?.range?.[1] ?? 0)
},
messageId: "forbiddenFieldFound",
data: { key }
});
}
for (const [key, value] of Object.entries(settings)) {
if (isDeepEqual(parsed[key], value)) continue;
const node = doc.getIn([key], true);
const actualValue = parsed[key];
const expectedStr = JSON.stringify(value);
const actualStr = actualValue !== void 0 ? JSON.stringify(actualValue) : "undefined";
if (!node) {
if (parsed[key] != null) throw new Error("Node should not be undefined");
context.report({
loc: {
start: context.sourceCode.getLocFromIndex(0),
end: context.sourceCode.getLocFromIndex(0)
},
messageId: "settingMismatch",
data: {
key,
expected: expectedStr,
actual: actualStr
},
fix: autofix ? (fixer) => {
const replacer = `\n${yaml.stringify({ [key]: value }, { collectionStyle: "block" })}\n`;
return fixer.insertTextBeforeRange([0, 0], replacer);
} : void 0
});
} else {
if (!node.range) throw new Error("Node range is not found");
const mapNode = doc.contents;
let pairItem;
if (mapNode?.items) {
for (const item of mapNode.items) if (item.key && typeof item.key === "object" && "value" in item.key && item.key.value === key) {
pairItem = item;
break;
}
}
let startIndex = node.range[0];
let endIndex = node.range[1];
if (pairItem?.key?.range && pairItem?.value?.range) {
startIndex = pairItem.key.range[0];
endIndex = pairItem.value.range[1];
}
context.report({
loc: {
start: context.sourceCode.getLocFromIndex(startIndex),
end: context.sourceCode.getLocFromIndex(endIndex)
},
messageId: "settingMismatch",
data: {
key,
expected: expectedStr,
actual: actualStr
},
fix: autofix ? (fixer) => {
const replacer = `\n${yaml.stringify({ [key]: value }, { collectionStyle: "block" })}\n`;
return fixer.replaceTextRange([startIndex, endIndex], replacer);
} : void 0
});
}
}
return {};
}
});
//#endregion
//#region src/rules/yaml/yaml-no-duplicate-catalog-item.ts
const RULE_NAME$2 = "yaml-no-duplicate-catalog-item";
var yaml_no_duplicate_catalog_item_default = createEslintRule({
name: RULE_NAME$2,
meta: {
type: "problem",
docs: { description: "Disallow duplicate catalog items in `pnpm-workspace.yaml`" },
fixable: "code",
schema: [{
type: "object",
properties: {
allow: {
type: "array",
items: { type: "string" }
},
checkDuplicates: {
type: "string",
enum: ["name-only", "exact-version"],
description: "Determines what constitutes a duplicate: \"name-only\" errors on any duplicate package name, \"exact-version\" only errors on identical version strings",
default: "name-only"
}
},
additionalProperties: false
}],
messages: { duplicateCatalogItem: "Catalog item \"{{name}}\" with version \"{{version}}\" is already defined in the \"{{existingCatalog}}\" catalog. You may want to remove one of them." }
},
defaultOptions: [{}],
create(context, [options = {}]) {
if (basename(context.filename) !== "pnpm-workspace.yaml") return {};
const workspace = getPnpmWorkspace(context);
if (!workspace || normalize(workspace.filepath) !== normalize(context.filename)) return {};
if (workspace.hasChanged() || workspace.hasQueue()) return {};
const { allow = [], checkDuplicates = "name-only" } = options;
workspace.setContent(context.sourceCode.text);
const json = workspace.toJSON() || {};
const exists = /* @__PURE__ */ new Map();
const catalogs = {
...json.catalogs,
default: json.catalog ?? json.catalogs?.default
};
const doc = workspace.getDocument();
for (const [catalog, object] of Object.entries(catalogs)) {
if (!object) continue;
for (const [key, version$1] of Object.entries(object)) {
if (allow.includes(key)) continue;
const existing = exists.get(key);
if (existing) {
if (checkDuplicates === "name-only" ? true : existing.version === version$1) {
const node = doc.getIn(catalog === "default" ? json.catalog ? ["catalog", key] : [
"catalogs",
catalog,
key
] : [
"catalogs",
catalog,
key
], true);
const start = context.sourceCode.getLocFromIndex(node.range[0]);
const end = context.sourceCode.getLocFromIndex(node.range[1]);
context.report({
loc: {
start,
end
},
messageId: "duplicateCatalogItem",
data: {
name: key,
version: String(version$1),
currentCatalog: catalog,
existingCatalog: existing.catalog
}
});
}
} else exists.set(key, {
catalog,
version: String(version$1)
});
}
}
return {};
}
});
//#endregion
//#region src/rules/yaml/yaml-no-unused-catalog-item.ts
const RULE_NAME$1 = "yaml-no-unused-catalog-item";
var yaml_no_unused_catalog_item_default = createEslintRule({
name: RULE_NAME$1,
meta: {
type: "problem",
docs: { description: "Disallow unused catalogs in `pnpm-workspace.yaml`" },
fixable: "code",
schema: [],
messages: { unusedCatalogItem: "Catalog item \"{{catalogItem}}\" is not used in any package.json." }
},
defaultOptions: [],
create(context) {
if (basename(context.filename) !== "pnpm-workspace.yaml") return {};
const workspace = getPnpmWorkspace(context);
if (!workspace || normalize(workspace.filepath) !== normalize(context.filename)) return {};
if (workspace.hasChanged() || workspace.hasQueue()) return {};
workspace.setContent(context.sourceCode.text);
const parsed = workspace.toJSON() || {};
const root = resolve(dirname(context.filename));
const entries = /* @__PURE__ */ new Map();
const doc = workspace.getDocument();
const catalogs = { default: doc.getIn(["catalog"]) };
for (const item of doc.getIn(["catalogs"])?.items || []) catalogs[String(item.key)] = item.value;
for (const [catalog, map] of Object.entries(catalogs)) {
if (!map) continue;
for (const item of map.items) entries.set(`${String(item.key)}:${catalog}`, item);
}
for (const [packageName, specifier] of Object.entries(parsed.overrides || {})) if (specifier.startsWith("catalog:")) {
const catalog = specifier.slice(8) || "default";
entries.delete(`${packageName}:${catalog}`);
}
if (entries.size === 0) return {};
const dirs = parsed.packages ? globSync(parsed.packages, {
cwd: root,
dot: false,
ignore: [
"**/node_modules/**",
"**/dist/**",
"**/build/**",
"**/dist/**",
"**/dist/**"
],
absolute: true,
expandDirectories: false,
onlyDirectories: true
}) : [];
dirs.push(root);
const packages = dirs.map((dir) => resolve(dir, "package.json")).filter((x) => existsSync(x)).sort();
const FIELDS = [
"dependencies",
"devDependencies",
"peerDependencies",
"optionalDependencies",
"overrides",
"resolutions",
"pnpm.overrides"
];
for (const path of packages) {
const pkg = JSON.parse(readFileSync(path, "utf-8"));
for (const field of FIELDS) {
const map = getObjectPath(pkg, field.split("."));
if (!map) continue;
for (const [name$1, value] of Object.entries(map)) {
if (!value.startsWith("catalog:")) continue;
const key = `${name$1}:${value.slice(8) || "default"}`;
entries.delete(key);
}
}
}
if (entries.size > 0) for (const [key, value] of Array.from(entries.entries()).sort((a, b) => a[0].localeCompare(b[0]))) {
const start = context.sourceCode.getLocFromIndex(value.key.range[0]);
const end = context.sourceCode.getLocFromIndex(value.value.range.at(-1));
context.report({
loc: {
start,
end
},
messageId: "unusedCatalogItem",
data: { catalogItem: key }
});
}
return {};
}
});
function getObjectPath(obj, path) {
let current = obj;
for (const key of path) {
current = current[key];
if (!current) return void 0;
}
return current;
}
//#endregion
//#region src/rules/yaml/yaml-valid-packages.ts
const RULE_NAME = "yaml-valid-packages";
function reportError(context, item, messageId, data) {
const start = context.sourceCode.getLocFromIndex(item.range[0]);
const end = context.sourceCode.getLocFromIndex(item.range[1]);
context.report({
loc: {
start,
end
},
messageId,
data
});
}
var yaml_valid_packages_default = createEslintRule({
name: RULE_NAME,
meta: {
type: "problem",
docs: { description: "Ensure all package patterns in `pnpm-workspace.yaml` match at least one directory" },
schema: [],
messages: {
noMatch: "Package pattern \"{{pattern}}\" does not match any directories with a package.json file.",
invalidType: "Package pattern must be a string, got {{type}}."
}
},
defaultOptions: [],
create(context) {
if (basename(context.filename) !== "pnpm-workspace.yaml") return {};
const workspace = getPnpmWorkspace(context);
if (!workspace || normalize(workspace.filepath) !== normalize(context.filename)) return {};
if (workspace.hasChanged() || workspace.hasQueue()) return {};
workspace.setContent(context.sourceCode.text);
const parsed = workspace.toJSON() || {};
if (!parsed.packages || !Array.isArray(parsed.packages)) return {};
const packagesNode = workspace.getDocument().getIn(["packages"]);
if (!packagesNode) return {};
const root = resolve(dirname(context.filename));
for (let i = 0; i < parsed.packages.length; i++) {
const item = packagesNode.items[i];
if (!item?.range) continue;
const pattern = parsed.packages[i];
if (typeof pattern !== "string") {
reportError(context, item, "invalidType", { type: typeof pattern });
continue;
}
if (globSync(join(pattern, "package.json"), {
cwd: root,
dot: false,
ignore: [
"**/node_modules/**",
"**/dist/**",
"**/build/**"
],
absolute: false,
expandDirectories: false,
onlyFiles: true
}).length === 0) reportError(context, item, "noMatch", { pattern });
}
return {};
}
});
//#endregion
//#region src/rules/yaml/index.ts
const rules$1 = {
"yaml-no-unused-catalog-item": yaml_no_unused_catalog_item_default,
"yaml-no-duplicate-catalog-item": yaml_no_duplicate_catalog_item_default,
"yaml-valid-packages": yaml_valid_packages_default,
"yaml-enforce-settings": yaml_enforce_settings_default
};
//#endregion
//#region src/rules/index.ts
const rules = {
...rules$2,
...rules$1
};
//#endregion
//#region src/index.ts
const plugin = {
meta: {
name,
version
},
rules
};
const configsJson = [{
name: "pnpm/package.json",
files: ["package.json", "**/package.json"],
languageOptions: { parser: jsoncParser },
plugins: { pnpm: plugin },
rules: {
"pnpm/json-enforce-catalog": "error",
"pnpm/json-valid-catalog": "error",
"pnpm/json-prefer-workspace-settings": "error"
}
}];
const configsYaml = [{
name: "pnpm/pnpm-workspace-yaml",
files: ["pnpm-workspace.yaml"],
languageOptions: { parser: yamlParser },
plugins: { pnpm: plugin },
rules: {
"pnpm/yaml-no-unused-catalog-item": "error",
"pnpm/yaml-no-duplicate-catalog-item": "error",
"pnpm/yaml-valid-packages": "error"
}
}];
const configs = {
recommended: [...configsJson],
json: configsJson,
yaml: configsYaml
};
plugin.configs = configs;
var src_default = plugin;
//#endregion
export { configs, src_default as default, plugin };
+62
View File
@@ -0,0 +1,62 @@
{
"name": "eslint-plugin-pnpm",
"type": "module",
"version": "1.6.0",
"description": "ESLint Plugin for pnpm",
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
"contributors": [
{
"name": "三咲智子 Kevin Deng",
"email": "sxzz@sxzz.moe"
}
],
"license": "MIT",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/antfu"
},
{
"type": "individual",
"url": "https://github.com/sponsors/sxzz"
}
],
"homepage": "https://github.com/antfu/pnpm-workspace-utils#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/antfu/pnpm-workspace-utils.git",
"directory": "packages/eslint-plugin-pnpm"
},
"bugs": "https://github.com/antfu/pnpm-workspace-utils/issues",
"keywords": [
"eslint-plugin",
"pnpm"
],
"sideEffects": false,
"exports": {
".": "./dist/index.mjs",
"./package.json": "./package.json"
},
"types": "./dist/index.d.mts",
"files": [
"dist"
],
"peerDependencies": {
"eslint": "^9.0.0 || ^10.0.0"
},
"dependencies": {
"empathic": "^2.0.0",
"jsonc-eslint-parser": "^3.1.0",
"pathe": "^2.0.3",
"tinyglobby": "^0.2.15",
"yaml": "^2.8.2",
"yaml-eslint-parser": "^2.0.0",
"pnpm-workspace-yaml": "1.6.0"
},
"devDependencies": {
"@typescript-eslint/parser": "^8.56.1"
},
"scripts": {
"start": "tsx src/index.ts"
}
}