@blumintinc/eslint-plugin-blumint 1.0.4 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/lib/index.js +44 -2
- package/lib/rules/class-methods-read-top-to-bottom.js +1 -0
- package/lib/rules/consistent-callback-naming.d.ts +2 -0
- package/lib/rules/consistent-callback-naming.js +84 -0
- package/lib/rules/dynamic-https-errors.js +0 -1
- package/lib/rules/enforce-callable-types.d.ts +3 -0
- package/lib/rules/enforce-callable-types.js +97 -0
- package/lib/rules/enforce-callback-memo.d.ts +3 -0
- package/lib/rules/enforce-callback-memo.js +79 -0
- package/lib/rules/enforce-dynamic-firebase-imports.d.ts +4 -0
- package/lib/rules/enforce-dynamic-firebase-imports.js +49 -0
- package/lib/rules/enforce-identifiable-firestore-type.d.ts +3 -0
- package/lib/rules/enforce-identifiable-firestore-type.js +89 -0
- package/lib/rules/enforce-serializable-params.d.ts +9 -0
- package/lib/rules/enforce-serializable-params.js +127 -0
- package/lib/rules/export-if-in-doubt.js +0 -1
- package/lib/rules/extract-global-constants.js +2 -0
- package/lib/rules/global-const-style.d.ts +5 -0
- package/lib/rules/global-const-style.js +77 -0
- package/lib/rules/no-conditional-literals-in-jsx.js +0 -1
- package/lib/rules/no-jsx-whitespace-literal.d.ts +1 -0
- package/lib/rules/no-jsx-whitespace-literal.js +34 -0
- package/lib/rules/no-unpinned-dependencies.js +1 -0
- package/lib/rules/no-unused-props.d.ts +6 -0
- package/lib/rules/no-unused-props.js +91 -0
- package/lib/rules/no-useless-fragment.js +2 -0
- package/lib/rules/prefer-fragment-shorthand.js +1 -0
- package/lib/rules/prefer-type-over-interface.js +3 -1
- package/lib/rules/require-dynamic-firebase-imports.d.ts +6 -0
- package/lib/rules/require-dynamic-firebase-imports.js +84 -0
- package/lib/rules/require-https-error.d.ts +6 -0
- package/lib/rules/require-https-error.js +74 -0
- package/lib/rules/require-image-overlayed.d.ts +7 -0
- package/lib/rules/require-image-overlayed.js +80 -0
- package/lib/rules/require-memo.js +5 -3
- package/lib/rules/require-usememo-object-literals.d.ts +4 -0
- package/lib/rules/require-usememo-object-literals.js +53 -0
- package/lib/rules/use-custom-link.d.ts +1 -0
- package/lib/rules/use-custom-link.js +49 -0
- package/lib/rules/use-custom-router.d.ts +1 -0
- package/lib/rules/use-custom-router.js +65 -0
- package/lib/utils/ASTHelpers.js +2 -0
- package/lib/utils/graph/ClassGraphBuilder.js +4 -1
- package/lib/utils/graph/ClassGraphSorterReadability.js +8 -3
- package/package.json +3 -4
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const createRule_1 = require("../utils/createRule");
|
|
3
|
+
module.exports = (0, createRule_1.createRule)({
|
|
4
|
+
name: 'require-image-overlayed',
|
|
5
|
+
meta: {
|
|
6
|
+
type: 'problem',
|
|
7
|
+
docs: {
|
|
8
|
+
description: 'Enforce using ImageOverlayed component instead of next/image or img tags',
|
|
9
|
+
recommended: 'error',
|
|
10
|
+
},
|
|
11
|
+
fixable: 'code',
|
|
12
|
+
schema: [
|
|
13
|
+
{
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {
|
|
16
|
+
componentPath: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
default: 'src/components/ImageOverlayed',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
additionalProperties: false,
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
messages: {
|
|
25
|
+
useImageOverlayed: 'Use ImageOverlayed component from {{ componentPath }} instead of {{ component }}',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
defaultOptions: [{ componentPath: 'src/components/ImageOverlayed' }],
|
|
29
|
+
create(context) {
|
|
30
|
+
const options = context.options[0] || {
|
|
31
|
+
componentPath: 'src/components/ImageOverlayed',
|
|
32
|
+
};
|
|
33
|
+
const sourceCode = context.getSourceCode();
|
|
34
|
+
return {
|
|
35
|
+
// Handle JSX img elements
|
|
36
|
+
JSXElement(node) {
|
|
37
|
+
if (node.openingElement.name.name === 'img') {
|
|
38
|
+
context.report({
|
|
39
|
+
node,
|
|
40
|
+
messageId: 'useImageOverlayed',
|
|
41
|
+
data: {
|
|
42
|
+
componentPath: options.componentPath,
|
|
43
|
+
component: 'img tag',
|
|
44
|
+
},
|
|
45
|
+
fix(fixer) {
|
|
46
|
+
const attributes = node.openingElement.attributes
|
|
47
|
+
.map((attr) => sourceCode.getText(attr))
|
|
48
|
+
.join(' ');
|
|
49
|
+
return fixer.replaceText(node, `<ImageOverlayed ${attributes} />`);
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
// Handle next/image imports and usage
|
|
55
|
+
ImportDeclaration(node) {
|
|
56
|
+
if (node.source.value === 'next/image' && node.specifiers.length > 0) {
|
|
57
|
+
const imageSpecifier = node.specifiers.find((spec) => (spec.type === 'ImportDefaultSpecifier' ||
|
|
58
|
+
spec.type === 'ImportSpecifier') &&
|
|
59
|
+
(spec.local.name === 'Image' || spec.imported?.name === 'Image'));
|
|
60
|
+
if (imageSpecifier) {
|
|
61
|
+
const localName = imageSpecifier.local.name;
|
|
62
|
+
// Report the import
|
|
63
|
+
context.report({
|
|
64
|
+
node,
|
|
65
|
+
messageId: 'useImageOverlayed',
|
|
66
|
+
data: {
|
|
67
|
+
componentPath: options.componentPath,
|
|
68
|
+
component: 'next/image',
|
|
69
|
+
},
|
|
70
|
+
fix(fixer) {
|
|
71
|
+
return fixer.replaceText(node, `import ${localName} from '${options.componentPath}';`);
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
//# sourceMappingURL=require-image-overlayed.js.map
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.requireMemo = void 0;
|
|
4
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
5
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
4
6
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
7
|
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
6
8
|
const isComponentExplicitlyUnmemoized = (componentName) => componentName.toLowerCase().includes('unmemoized');
|
|
@@ -43,7 +45,7 @@ const isUnmemoizedExportedFunctionComponent = (parentNode, node) => {
|
|
|
43
45
|
};
|
|
44
46
|
function isMemoImport(importPath) {
|
|
45
47
|
// Match both absolute and relative paths ending with util/memo
|
|
46
|
-
return /(
|
|
48
|
+
return /(?:^|\/|\\)util\/memo$/.test(importPath);
|
|
47
49
|
}
|
|
48
50
|
function checkFunction(context, node) {
|
|
49
51
|
const fileName = context.getFilename();
|
|
@@ -135,7 +137,7 @@ function calculateImportPath(currentFilePath) {
|
|
|
135
137
|
// Calculate relative path based on current file depth from src
|
|
136
138
|
// Subtract 1 from depth to exclude the filename itself
|
|
137
139
|
const depth = parts.length - (srcIndex + 1) - 1;
|
|
138
|
-
return '../'.repeat(depth) + 'util/memo';
|
|
140
|
+
return depth > 0 ? '../'.repeat(depth) + 'util/memo' : './util/memo';
|
|
139
141
|
}
|
|
140
142
|
exports.requireMemo = {
|
|
141
143
|
create: (context) => ({
|
|
@@ -156,7 +158,7 @@ exports.requireMemo = {
|
|
|
156
158
|
recommended: 'error',
|
|
157
159
|
},
|
|
158
160
|
messages: {
|
|
159
|
-
requireMemo: 'Component definition not wrapped in
|
|
161
|
+
requireMemo: 'Component definition not wrapped in memo()',
|
|
160
162
|
},
|
|
161
163
|
schema: [],
|
|
162
164
|
fixable: 'code',
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.requireUseMemoObjectLiterals = void 0;
|
|
4
|
+
const createRule_1 = require("../utils/createRule");
|
|
5
|
+
exports.requireUseMemoObjectLiterals = (0, createRule_1.createRule)({
|
|
6
|
+
name: 'require-usememo-object-literals',
|
|
7
|
+
meta: {
|
|
8
|
+
type: 'suggestion',
|
|
9
|
+
docs: {
|
|
10
|
+
description: 'Enforce using useMemo for inline object literals passed as props to JSX components',
|
|
11
|
+
recommended: 'error',
|
|
12
|
+
},
|
|
13
|
+
messages: {
|
|
14
|
+
requireUseMemo: 'Inline object/array literals in JSX props should be wrapped in useMemo to prevent unnecessary re-renders',
|
|
15
|
+
},
|
|
16
|
+
schema: [],
|
|
17
|
+
},
|
|
18
|
+
defaultOptions: [],
|
|
19
|
+
create(context) {
|
|
20
|
+
return {
|
|
21
|
+
JSXAttribute(node) {
|
|
22
|
+
// Skip if the value is not an expression
|
|
23
|
+
if (!node.value || node.value.type !== 'JSXExpressionContainer') {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const { expression } = node.value;
|
|
27
|
+
// Check if the expression is an object or array literal
|
|
28
|
+
if ((expression.type === 'ObjectExpression' ||
|
|
29
|
+
expression.type === 'ArrayExpression') &&
|
|
30
|
+
// Ensure we're in a function component context
|
|
31
|
+
context
|
|
32
|
+
.getAncestors()
|
|
33
|
+
.some((ancestor) => ancestor.type === 'FunctionDeclaration' ||
|
|
34
|
+
ancestor.type === 'ArrowFunctionExpression' ||
|
|
35
|
+
ancestor.type === 'FunctionExpression')) {
|
|
36
|
+
// Check if the parent component name starts with an uppercase letter
|
|
37
|
+
// to ensure it's a React component
|
|
38
|
+
const jsxElement = node.parent;
|
|
39
|
+
const elementName = jsxElement.name.type === 'JSXIdentifier'
|
|
40
|
+
? jsxElement.name.name
|
|
41
|
+
: '';
|
|
42
|
+
if (elementName && /^[A-Z]/.test(elementName)) {
|
|
43
|
+
context.report({
|
|
44
|
+
node: expression,
|
|
45
|
+
messageId: 'requireUseMemo',
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
//# sourceMappingURL=require-usememo-object-literals.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const useCustomLink: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"useCustomLink", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useCustomLink = void 0;
|
|
4
|
+
const createRule_1 = require("../utils/createRule");
|
|
5
|
+
exports.useCustomLink = (0, createRule_1.createRule)({
|
|
6
|
+
name: 'use-custom-link',
|
|
7
|
+
meta: {
|
|
8
|
+
type: 'problem',
|
|
9
|
+
docs: {
|
|
10
|
+
description: 'Enforce using src/components/Link instead of next/link',
|
|
11
|
+
recommended: 'error',
|
|
12
|
+
},
|
|
13
|
+
fixable: 'code',
|
|
14
|
+
schema: [],
|
|
15
|
+
messages: {
|
|
16
|
+
useCustomLink: 'Import Link from src/components/Link instead of next/link',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
defaultOptions: [],
|
|
20
|
+
create(context) {
|
|
21
|
+
return {
|
|
22
|
+
ImportDeclaration(node) {
|
|
23
|
+
if (node.source.value === 'next/link') {
|
|
24
|
+
const importSpecifiers = node.specifiers;
|
|
25
|
+
// Handle different import types (default, named, namespace)
|
|
26
|
+
const defaultSpecifier = importSpecifiers.find((specifier) => specifier.type === 'ImportDefaultSpecifier');
|
|
27
|
+
const defaultAsSpecifier = importSpecifiers.find((specifier) => specifier.type === 'ImportSpecifier' &&
|
|
28
|
+
specifier.imported.name === 'default');
|
|
29
|
+
if (defaultSpecifier || defaultAsSpecifier) {
|
|
30
|
+
context.report({
|
|
31
|
+
node,
|
|
32
|
+
messageId: 'useCustomLink',
|
|
33
|
+
fix(fixer) {
|
|
34
|
+
// Get the local name of the imported Link component
|
|
35
|
+
const localName = defaultSpecifier?.local?.name ||
|
|
36
|
+
defaultAsSpecifier?.local?.name ||
|
|
37
|
+
'Link';
|
|
38
|
+
// Create the new import statement
|
|
39
|
+
const newImport = `import ${localName} from 'src/components/Link';`;
|
|
40
|
+
return fixer.replaceText(node, newImport);
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
//# sourceMappingURL=use-custom-link.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const useCustomRouter: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"useCustomRouter", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useCustomRouter = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
exports.useCustomRouter = (0, createRule_1.createRule)({
|
|
7
|
+
name: 'use-custom-router',
|
|
8
|
+
meta: {
|
|
9
|
+
type: 'suggestion',
|
|
10
|
+
docs: {
|
|
11
|
+
description: 'Enforce using src/hooks/routing/useRouter instead of next/router',
|
|
12
|
+
recommended: 'error',
|
|
13
|
+
},
|
|
14
|
+
fixable: 'code',
|
|
15
|
+
schema: [],
|
|
16
|
+
messages: {
|
|
17
|
+
useCustomRouter: 'Import useRouter from src/hooks/routing/useRouter instead of next/router',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
defaultOptions: [],
|
|
21
|
+
create(context) {
|
|
22
|
+
return {
|
|
23
|
+
ImportDeclaration(node) {
|
|
24
|
+
if (node.source.value === 'next/router') {
|
|
25
|
+
const specifiers = node.specifiers.filter((specifier) => specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
26
|
+
specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
27
|
+
specifier.imported.name === 'useRouter');
|
|
28
|
+
if (specifiers.length > 0) {
|
|
29
|
+
context.report({
|
|
30
|
+
node,
|
|
31
|
+
messageId: 'useCustomRouter',
|
|
32
|
+
fix(fixer) {
|
|
33
|
+
// If there are other imports from next/router, keep them
|
|
34
|
+
const otherSpecifiers = node.specifiers.filter((specifier) => specifier.type !== utils_1.AST_NODE_TYPES.ImportSpecifier ||
|
|
35
|
+
(specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
36
|
+
specifier.imported.name !== 'useRouter'));
|
|
37
|
+
if (otherSpecifiers.length === 0) {
|
|
38
|
+
// If useRouter is the only import, replace the entire import
|
|
39
|
+
return fixer.replaceText(node, `import { ${specifiers
|
|
40
|
+
.map((s) => s.local.name !== s.imported.name
|
|
41
|
+
? `useRouter as ${s.local.name}`
|
|
42
|
+
: 'useRouter')
|
|
43
|
+
.join(', ')} } from 'src/hooks/routing/useRouter';`);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
// Create a new import for useRouter and keep other imports
|
|
47
|
+
const useRouterImport = `import { ${specifiers
|
|
48
|
+
.map((s) => s.local.name !== s.imported.name
|
|
49
|
+
? `useRouter as ${s.local.name}`
|
|
50
|
+
: 'useRouter')
|
|
51
|
+
.join(', ')} } from 'src/hooks/routing/useRouter';\n`;
|
|
52
|
+
const otherImports = `import { ${otherSpecifiers
|
|
53
|
+
.map((s) => s.local.name)
|
|
54
|
+
.join(', ')} } from 'next/router';`;
|
|
55
|
+
return fixer.replaceText(node, useRouterImport + otherImports);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
//# sourceMappingURL=use-custom-router.js.map
|
package/lib/utils/ASTHelpers.js
CHANGED
|
@@ -84,6 +84,8 @@ class ASTHelpers {
|
|
|
84
84
|
return (this.declarationIncludesIdentifier(node.test) ||
|
|
85
85
|
this.declarationIncludesIdentifier(node.consequent) ||
|
|
86
86
|
this.declarationIncludesIdentifier(node.alternate));
|
|
87
|
+
case 'TemplateLiteral':
|
|
88
|
+
return node.expressions.some((expr) => this.declarationIncludesIdentifier(expr));
|
|
87
89
|
case 'TSAsExpression':
|
|
88
90
|
return this.declarationIncludesIdentifier(node.expression);
|
|
89
91
|
default:
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ClassGraphBuilder = void 0;
|
|
4
|
-
/* eslint-disable security/detect-object-injection */
|
|
5
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
6
5
|
const ClassGraphSorterReadability_1 = require("./ClassGraphSorterReadability");
|
|
7
6
|
const ASTHelpers_1 = require("../ASTHelpers");
|
|
@@ -66,6 +65,10 @@ class ClassGraphBuilder {
|
|
|
66
65
|
addDependencies(node, methodName) {
|
|
67
66
|
const newDependencies = ASTHelpers_1.ASTHelpers.classMethodDependenciesOf(node, this.graph, this.className).filter((name) => !!this.graph[name] && name !== methodName);
|
|
68
67
|
if (this.graph[methodName]) {
|
|
68
|
+
// Ensure dependencies is initialized as an array
|
|
69
|
+
if (!Array.isArray(this.graph[methodName].dependencies)) {
|
|
70
|
+
this.graph[methodName].dependencies = [];
|
|
71
|
+
}
|
|
69
72
|
this.graph[methodName].dependencies.push(...newDependencies);
|
|
70
73
|
}
|
|
71
74
|
}
|
|
@@ -66,13 +66,18 @@ class ClassGraphSorterReadability extends ClassGraphSorter_1.ClassGraphSorter {
|
|
|
66
66
|
const visited = new Set();
|
|
67
67
|
const dfsSortedNodes = [];
|
|
68
68
|
const dfs = (node) => {
|
|
69
|
-
if (visited.has(node.name)
|
|
69
|
+
if (!node || visited.has(node.name)) {
|
|
70
70
|
return;
|
|
71
71
|
}
|
|
72
72
|
visited.add(node.name);
|
|
73
73
|
dfsSortedNodes.push(node);
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
// Ensure node.dependencies exists and is an array before iterating
|
|
75
|
+
const dependencies = Array.isArray(node.dependencies) ? node.dependencies : [];
|
|
76
|
+
for (const dep of dependencies) {
|
|
77
|
+
const depNode = this.graph[String(dep)];
|
|
78
|
+
if (depNode) {
|
|
79
|
+
dfs(depNode);
|
|
80
|
+
}
|
|
76
81
|
}
|
|
77
82
|
};
|
|
78
83
|
searchNodes.forEach((node) => dfs(node));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blumintinc/eslint-plugin-blumint",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Custom eslint rules for use within BluMint",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Brodie McGuire",
|
|
@@ -29,7 +29,6 @@
|
|
|
29
29
|
"lint": "npm-run-all \"lint:*\"",
|
|
30
30
|
"lint:eslint-docs": "npm-run-all \"update:eslint-docs -- --check\"",
|
|
31
31
|
"lint:js": "eslint ./src",
|
|
32
|
-
"lint:md": "remark .",
|
|
33
32
|
"lint:shell": "shellcheck .devcontainer/git-flow-completion.bash",
|
|
34
33
|
"lint:fix": "tsc && eslint ./**/* --quiet --fix",
|
|
35
34
|
"test": "jest --passWithNoTests --reporters=default --reporters=jest-junit",
|
|
@@ -64,7 +63,7 @@
|
|
|
64
63
|
"cz-conventional-changelog": "3.3.0",
|
|
65
64
|
"del-cli": "4.0.1",
|
|
66
65
|
"dotenv-cli": "5.0.0",
|
|
67
|
-
"eslint": "8.
|
|
66
|
+
"eslint": "8.57.0",
|
|
68
67
|
"eslint-config-prettier": "8.5.0",
|
|
69
68
|
"eslint-doc-generator": "1.0.0",
|
|
70
69
|
"eslint-import-resolver-typescript": "3.5.5",
|
|
@@ -94,7 +93,7 @@
|
|
|
94
93
|
"eslint": ">=7"
|
|
95
94
|
},
|
|
96
95
|
"engines": {
|
|
97
|
-
"node": "^
|
|
96
|
+
"node": "^22.0.0"
|
|
98
97
|
},
|
|
99
98
|
"config": {
|
|
100
99
|
"commitizen": {
|