routie dev init since i didn't adhere to any proper guidance up until now
This commit is contained in:
+22
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user