@atom8n/eslint-plugin-community-nodes 0.7.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/.turbo/turbo-build.log +4 -0
- package/README.md +60 -0
- package/dist/plugin.d.ts +177 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +54 -0
- package/dist/plugin.js.map +1 -0
- package/dist/rules/credential-documentation-url.d.ts +7 -0
- package/dist/rules/credential-documentation-url.d.ts.map +1 -0
- package/dist/rules/credential-documentation-url.js +100 -0
- package/dist/rules/credential-documentation-url.js.map +1 -0
- package/dist/rules/credential-password-field.d.ts +2 -0
- package/dist/rules/credential-password-field.d.ts.map +1 -0
- package/dist/rules/credential-password-field.js +108 -0
- package/dist/rules/credential-password-field.js.map +1 -0
- package/dist/rules/credential-test-required.d.ts +2 -0
- package/dist/rules/credential-test-required.d.ts.map +1 -0
- package/dist/rules/credential-test-required.js +117 -0
- package/dist/rules/credential-test-required.js.map +1 -0
- package/dist/rules/icon-validation.d.ts +2 -0
- package/dist/rules/icon-validation.d.ts.map +1 -0
- package/dist/rules/icon-validation.js +197 -0
- package/dist/rules/icon-validation.js.map +1 -0
- package/dist/rules/index.d.ts +17 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +25 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/no-credential-reuse.d.ts +2 -0
- package/dist/rules/no-credential-reuse.d.ts.map +1 -0
- package/dist/rules/no-credential-reuse.js +91 -0
- package/dist/rules/no-credential-reuse.js.map +1 -0
- package/dist/rules/no-deprecated-workflow-functions.d.ts +2 -0
- package/dist/rules/no-deprecated-workflow-functions.d.ts.map +1 -0
- package/dist/rules/no-deprecated-workflow-functions.js +172 -0
- package/dist/rules/no-deprecated-workflow-functions.js.map +1 -0
- package/dist/rules/no-restricted-globals.d.ts +3 -0
- package/dist/rules/no-restricted-globals.d.ts.map +1 -0
- package/dist/rules/no-restricted-globals.js +60 -0
- package/dist/rules/no-restricted-globals.js.map +1 -0
- package/dist/rules/no-restricted-imports.d.ts +2 -0
- package/dist/rules/no-restricted-imports.d.ts.map +1 -0
- package/dist/rules/no-restricted-imports.js +80 -0
- package/dist/rules/no-restricted-imports.js.map +1 -0
- package/dist/rules/node-usable-as-tool.d.ts +2 -0
- package/dist/rules/node-usable-as-tool.d.ts.map +1 -0
- package/dist/rules/node-usable-as-tool.js +58 -0
- package/dist/rules/node-usable-as-tool.js.map +1 -0
- package/dist/rules/package-name-convention.d.ts +2 -0
- package/dist/rules/package-name-convention.d.ts.map +1 -0
- package/dist/rules/package-name-convention.js +88 -0
- package/dist/rules/package-name-convention.js.map +1 -0
- package/dist/rules/resource-operation-pattern.d.ts +2 -0
- package/dist/rules/resource-operation-pattern.d.ts.map +1 -0
- package/dist/rules/resource-operation-pattern.js +79 -0
- package/dist/rules/resource-operation-pattern.js.map +1 -0
- package/dist/utils/ast-utils.d.ts +26 -0
- package/dist/utils/ast-utils.d.ts.map +1 -0
- package/dist/utils/ast-utils.js +135 -0
- package/dist/utils/ast-utils.js.map +1 -0
- package/dist/utils/file-utils.d.ts +26 -0
- package/dist/utils/file-utils.d.ts.map +1 -0
- package/dist/utils/file-utils.js +221 -0
- package/dist/utils/file-utils.js.map +1 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/rule-creator.d.ts +3 -0
- package/dist/utils/rule-creator.d.ts.map +1 -0
- package/dist/utils/rule-creator.js +5 -0
- package/dist/utils/rule-creator.js.map +1 -0
- package/docs/rules/credential-documentation-url.md +94 -0
- package/docs/rules/credential-password-field.md +45 -0
- package/docs/rules/credential-test-required.md +58 -0
- package/docs/rules/icon-validation.md +67 -0
- package/docs/rules/no-credential-reuse.md +82 -0
- package/docs/rules/no-deprecated-workflow-functions.md +61 -0
- package/docs/rules/no-restricted-globals.md +44 -0
- package/docs/rules/no-restricted-imports.md +47 -0
- package/docs/rules/node-usable-as-tool.md +43 -0
- package/docs/rules/package-name-convention.md +52 -0
- package/docs/rules/resource-operation-pattern.md +84 -0
- package/eslint.config.mjs +27 -0
- package/package.json +58 -0
- package/src/plugin.ts +59 -0
- package/src/rules/credential-documentation-url.test.ts +306 -0
- package/src/rules/credential-documentation-url.ts +129 -0
- package/src/rules/credential-password-field.test.ts +232 -0
- package/src/rules/credential-password-field.ts +141 -0
- package/src/rules/credential-test-required.test.ts +174 -0
- package/src/rules/credential-test-required.ts +145 -0
- package/src/rules/icon-validation.test.ts +279 -0
- package/src/rules/icon-validation.ts +239 -0
- package/src/rules/index.ts +27 -0
- package/src/rules/no-credential-reuse.test.ts +474 -0
- package/src/rules/no-credential-reuse.ts +121 -0
- package/src/rules/no-deprecated-workflow-functions.test.ts +187 -0
- package/src/rules/no-deprecated-workflow-functions.ts +200 -0
- package/src/rules/no-restricted-globals.test.ts +136 -0
- package/src/rules/no-restricted-globals.ts +74 -0
- package/src/rules/no-restricted-imports.test.ts +182 -0
- package/src/rules/no-restricted-imports.ts +91 -0
- package/src/rules/node-usable-as-tool.test.ts +81 -0
- package/src/rules/node-usable-as-tool.ts +72 -0
- package/src/rules/package-name-convention.test.ts +189 -0
- package/src/rules/package-name-convention.ts +107 -0
- package/src/rules/resource-operation-pattern.test.ts +217 -0
- package/src/rules/resource-operation-pattern.ts +104 -0
- package/src/utils/ast-utils.ts +207 -0
- package/src/utils/file-utils.ts +294 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/rule-creator.ts +6 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.eslint.json +5 -0
- package/tsconfig.json +10 -0
- package/vite.config.ts +4 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
|
|
3
|
+
import { NoRestrictedImportsRule } from './no-restricted-imports.js';
|
|
4
|
+
|
|
5
|
+
const ruleTester = new RuleTester();
|
|
6
|
+
|
|
7
|
+
ruleTester.run('no-restricted-imports', NoRestrictedImportsRule, {
|
|
8
|
+
valid: [
|
|
9
|
+
{
|
|
10
|
+
code: 'import { WorkflowExecuteMode } from "n8n-workflow";',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
code: 'import _ from "lodash";',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
code: 'import moment from "moment";',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
code: 'import pLimit from "p-limit";',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
code: 'import { DateTime } from "luxon";',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
code: 'import { z } from "zod";',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
code: 'import crypto from "crypto";',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
code: 'import crypto from "node:crypto";',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
code: 'import { helper } from "./helper";',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
code: 'import { utils } from "../utils";',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
code: 'const helper = require("./helper");',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
code: 'const utils = require("../utils");',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
code: 'const _ = require("lodash");',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
code: 'require.resolve("lodash");',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
code: 'require.resolve("./helper");',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
code: 'require.resolve("../utils");',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
code: 'const workflow = await import("n8n-workflow");',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
code: 'import("lodash").then((_) => {});',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
code: 'const helper = await import("./helper");',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
code: 'import("../utils").then((utils) => {});',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
code: 'import(`lodash`).then((_) => {});',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
code: 'require(`./helper`);',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
code: 'require.resolve(`n8n-workflow`);',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
code: 'const workflow = await import(`n8n-workflow`);',
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
invalid: [
|
|
83
|
+
{
|
|
84
|
+
code: 'import fs from "fs";',
|
|
85
|
+
errors: [{ messageId: 'restrictedImport', data: { modulePath: 'fs' } }],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
code: 'import path from "path";',
|
|
89
|
+
errors: [{ messageId: 'restrictedImport', data: { modulePath: 'path' } }],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
code: 'import express from "express";',
|
|
93
|
+
errors: [{ messageId: 'restrictedImport', data: { modulePath: 'express' } }],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
code: 'import axios from "axios";',
|
|
97
|
+
errors: [{ messageId: 'restrictedImport', data: { modulePath: 'axios' } }],
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
code: 'import { Client } from "@elastic/elasticsearch";',
|
|
101
|
+
errors: [{ messageId: 'restrictedImport', data: { modulePath: '@elastic/elasticsearch' } }],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
code: 'const fs = require("fs");',
|
|
105
|
+
errors: [{ messageId: 'restrictedRequire', data: { modulePath: 'fs' } }],
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
code: 'const path = require("path");',
|
|
109
|
+
errors: [{ messageId: 'restrictedRequire', data: { modulePath: 'path' } }],
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
code: 'const express = require("express");',
|
|
113
|
+
errors: [{ messageId: 'restrictedRequire', data: { modulePath: 'express' } }],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
code: 'require.resolve("fs");',
|
|
117
|
+
errors: [{ messageId: 'restrictedRequire', data: { modulePath: 'fs' } }],
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
code: 'require.resolve("express");',
|
|
121
|
+
errors: [{ messageId: 'restrictedRequire', data: { modulePath: 'express' } }],
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
code: 'const resolved = require.resolve("axios");',
|
|
125
|
+
errors: [{ messageId: 'restrictedRequire', data: { modulePath: 'axios' } }],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
code: `
|
|
129
|
+
import fs from "fs";
|
|
130
|
+
import path from "path";
|
|
131
|
+
import { WorkflowExecuteMode } from "n8n-workflow";`,
|
|
132
|
+
errors: [
|
|
133
|
+
{ messageId: 'restrictedImport', data: { modulePath: 'fs' } },
|
|
134
|
+
{ messageId: 'restrictedImport', data: { modulePath: 'path' } },
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
code: `
|
|
139
|
+
const fs = require("fs");
|
|
140
|
+
const express = require("express");
|
|
141
|
+
const lodash = require("lodash");`,
|
|
142
|
+
errors: [
|
|
143
|
+
{ messageId: 'restrictedRequire', data: { modulePath: 'fs' } },
|
|
144
|
+
{ messageId: 'restrictedRequire', data: { modulePath: 'express' } },
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
code: 'const fs = await import("fs");',
|
|
149
|
+
errors: [{ messageId: 'restrictedDynamicImport', data: { modulePath: 'fs' } }],
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
code: 'import("path").then((path) => {});',
|
|
153
|
+
errors: [{ messageId: 'restrictedDynamicImport', data: { modulePath: 'path' } }],
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
code: 'const express = await import("express");',
|
|
157
|
+
errors: [{ messageId: 'restrictedDynamicImport', data: { modulePath: 'express' } }],
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
code: 'const path = require(`path`);',
|
|
161
|
+
errors: [{ messageId: 'restrictedRequire', data: { modulePath: 'path' } }],
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
code: 'require.resolve(`express`);',
|
|
165
|
+
errors: [{ messageId: 'restrictedRequire', data: { modulePath: 'express' } }],
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
code: 'const axios = await import(`axios`);',
|
|
169
|
+
errors: [{ messageId: 'restrictedDynamicImport', data: { modulePath: 'axios' } }],
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
code: `
|
|
173
|
+
const fs = await import("fs");
|
|
174
|
+
import("axios").then((axios) => {});
|
|
175
|
+
const workflow = await import("n8n-workflow");`,
|
|
176
|
+
errors: [
|
|
177
|
+
{ messageId: 'restrictedDynamicImport', data: { modulePath: 'fs' } },
|
|
178
|
+
{ messageId: 'restrictedDynamicImport', data: { modulePath: 'axios' } },
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getModulePath,
|
|
3
|
+
isDirectRequireCall,
|
|
4
|
+
isRequireMemberCall,
|
|
5
|
+
createRule,
|
|
6
|
+
} from '../utils/index.js';
|
|
7
|
+
|
|
8
|
+
const allowedModules = [
|
|
9
|
+
'n8n-workflow',
|
|
10
|
+
'lodash',
|
|
11
|
+
'moment',
|
|
12
|
+
'p-limit',
|
|
13
|
+
'luxon',
|
|
14
|
+
'zod',
|
|
15
|
+
'crypto',
|
|
16
|
+
'node:crypto',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const isModuleAllowed = (modulePath: string): boolean => {
|
|
20
|
+
if (modulePath.startsWith('./') || modulePath.startsWith('../')) return true;
|
|
21
|
+
|
|
22
|
+
const moduleName = modulePath.startsWith('@')
|
|
23
|
+
? modulePath.split('/').slice(0, 2).join('/')
|
|
24
|
+
: modulePath.split('/')[0];
|
|
25
|
+
if (!moduleName) return true;
|
|
26
|
+
return allowedModules.includes(moduleName);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const NoRestrictedImportsRule = createRule({
|
|
30
|
+
name: 'no-restricted-imports',
|
|
31
|
+
meta: {
|
|
32
|
+
type: 'problem',
|
|
33
|
+
docs: {
|
|
34
|
+
description: 'Disallow usage of restricted imports in community nodes.',
|
|
35
|
+
},
|
|
36
|
+
messages: {
|
|
37
|
+
restrictedImport:
|
|
38
|
+
"Import of '{{ modulePath }}' is not allowed. n8n Cloud does not allow community nodes with dependencies.",
|
|
39
|
+
restrictedRequire:
|
|
40
|
+
"Require of '{{ modulePath }}' is not allowed. n8n Cloud does not allow community nodes with dependencies.",
|
|
41
|
+
restrictedDynamicImport:
|
|
42
|
+
"Dynamic import of '{{ modulePath }}' is not allowed. n8n Cloud does not allow community nodes with dependencies.",
|
|
43
|
+
},
|
|
44
|
+
schema: [],
|
|
45
|
+
},
|
|
46
|
+
defaultOptions: [],
|
|
47
|
+
create(context) {
|
|
48
|
+
return {
|
|
49
|
+
ImportDeclaration(node) {
|
|
50
|
+
const modulePath = getModulePath(node.source);
|
|
51
|
+
if (modulePath && !isModuleAllowed(modulePath)) {
|
|
52
|
+
context.report({
|
|
53
|
+
node,
|
|
54
|
+
messageId: 'restrictedImport',
|
|
55
|
+
data: {
|
|
56
|
+
modulePath,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
ImportExpression(node) {
|
|
63
|
+
const modulePath = getModulePath(node.source);
|
|
64
|
+
if (modulePath && !isModuleAllowed(modulePath)) {
|
|
65
|
+
context.report({
|
|
66
|
+
node,
|
|
67
|
+
messageId: 'restrictedDynamicImport',
|
|
68
|
+
data: {
|
|
69
|
+
modulePath,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
CallExpression(node) {
|
|
76
|
+
if (isDirectRequireCall(node) || isRequireMemberCall(node)) {
|
|
77
|
+
const modulePath = getModulePath(node.arguments[0] ?? null);
|
|
78
|
+
if (modulePath && !isModuleAllowed(modulePath)) {
|
|
79
|
+
context.report({
|
|
80
|
+
node,
|
|
81
|
+
messageId: 'restrictedRequire',
|
|
82
|
+
data: {
|
|
83
|
+
modulePath,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
|
|
3
|
+
import { NodeUsableAsToolRule } from './node-usable-as-tool.js';
|
|
4
|
+
|
|
5
|
+
const ruleTester = new RuleTester();
|
|
6
|
+
|
|
7
|
+
function createNodeCode(
|
|
8
|
+
usableAsTool?: boolean | 'missing',
|
|
9
|
+
hasDescription: boolean = true,
|
|
10
|
+
): string {
|
|
11
|
+
let usableAsToolProperty = '';
|
|
12
|
+
if (usableAsTool === true) {
|
|
13
|
+
usableAsToolProperty = ',\n\t\tusableAsTool: true';
|
|
14
|
+
} else if (usableAsTool === false) {
|
|
15
|
+
usableAsToolProperty = ',\n\t\tusableAsTool: false';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!hasDescription) {
|
|
19
|
+
return `
|
|
20
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
21
|
+
|
|
22
|
+
export class TestNode implements INodeType {
|
|
23
|
+
displayName = 'Test Node';
|
|
24
|
+
}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return `
|
|
28
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
29
|
+
|
|
30
|
+
export class TestNode implements INodeType {
|
|
31
|
+
description: INodeTypeDescription = {
|
|
32
|
+
displayName: 'Test Node',
|
|
33
|
+
name: 'testNode',
|
|
34
|
+
group: ['input'],
|
|
35
|
+
version: 1,
|
|
36
|
+
description: 'A test node',
|
|
37
|
+
defaults: {
|
|
38
|
+
name: 'Test Node',
|
|
39
|
+
},
|
|
40
|
+
inputs: ['main'],
|
|
41
|
+
outputs: ['main'],
|
|
42
|
+
properties: []${usableAsToolProperty},
|
|
43
|
+
};
|
|
44
|
+
}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function createNonNodeClass(): string {
|
|
48
|
+
return `
|
|
49
|
+
export class RegularClass {
|
|
50
|
+
someProperty = 'value';
|
|
51
|
+
}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
ruleTester.run('node-usable-as-tool', NodeUsableAsToolRule, {
|
|
55
|
+
valid: [
|
|
56
|
+
{
|
|
57
|
+
name: 'node with usableAsTool set to true',
|
|
58
|
+
code: createNodeCode(true),
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'class that does not implement INodeType',
|
|
62
|
+
code: createNonNodeClass(),
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'node with usableAsTool set to false',
|
|
66
|
+
code: createNodeCode(false),
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'node without description property',
|
|
70
|
+
code: createNodeCode(undefined, false),
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
invalid: [
|
|
74
|
+
{
|
|
75
|
+
name: 'node missing usableAsTool property',
|
|
76
|
+
code: createNodeCode('missing'),
|
|
77
|
+
errors: [{ messageId: 'missingUsableAsTool' }],
|
|
78
|
+
output: createNodeCode(true),
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { TSESTree } from '@typescript-eslint/types';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
isNodeTypeClass,
|
|
5
|
+
findClassProperty,
|
|
6
|
+
findObjectProperty,
|
|
7
|
+
createRule,
|
|
8
|
+
} from '../utils/index.js';
|
|
9
|
+
|
|
10
|
+
export const NodeUsableAsToolRule = createRule({
|
|
11
|
+
name: 'node-usable-as-tool',
|
|
12
|
+
meta: {
|
|
13
|
+
type: 'problem',
|
|
14
|
+
docs: {
|
|
15
|
+
description: 'Ensure node classes have usableAsTool property',
|
|
16
|
+
},
|
|
17
|
+
messages: {
|
|
18
|
+
missingUsableAsTool:
|
|
19
|
+
'Node class should have usableAsTool property. When in doubt, set it to true.',
|
|
20
|
+
},
|
|
21
|
+
fixable: 'code',
|
|
22
|
+
schema: [],
|
|
23
|
+
},
|
|
24
|
+
defaultOptions: [],
|
|
25
|
+
create(context) {
|
|
26
|
+
return {
|
|
27
|
+
ClassDeclaration(node) {
|
|
28
|
+
if (!isNodeTypeClass(node)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const descriptionProperty = findClassProperty(node, 'description');
|
|
33
|
+
if (!descriptionProperty) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const descriptionValue = descriptionProperty.value;
|
|
38
|
+
if (descriptionValue?.type !== TSESTree.AST_NODE_TYPES.ObjectExpression) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const usableAsToolProperty = findObjectProperty(descriptionValue, 'usableAsTool');
|
|
43
|
+
|
|
44
|
+
if (!usableAsToolProperty) {
|
|
45
|
+
context.report({
|
|
46
|
+
node,
|
|
47
|
+
messageId: 'missingUsableAsTool',
|
|
48
|
+
fix(fixer) {
|
|
49
|
+
if (descriptionValue?.type === TSESTree.AST_NODE_TYPES.ObjectExpression) {
|
|
50
|
+
const properties = descriptionValue.properties;
|
|
51
|
+
if (properties.length === 0) {
|
|
52
|
+
const openBrace = descriptionValue.range[0] + 1;
|
|
53
|
+
return fixer.insertTextAfterRange(
|
|
54
|
+
[openBrace, openBrace],
|
|
55
|
+
'\n\t\tusableAsTool: true,',
|
|
56
|
+
);
|
|
57
|
+
} else {
|
|
58
|
+
const lastProperty = properties.at(-1);
|
|
59
|
+
if (lastProperty) {
|
|
60
|
+
return fixer.insertTextAfter(lastProperty, ',\n\t\tusableAsTool: true');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return null;
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
});
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
|
|
3
|
+
import { PackageNameConventionRule } from './package-name-convention.js';
|
|
4
|
+
|
|
5
|
+
const ruleTester = new RuleTester();
|
|
6
|
+
|
|
7
|
+
ruleTester.run('package-name-convention', PackageNameConventionRule, {
|
|
8
|
+
valid: [
|
|
9
|
+
{
|
|
10
|
+
name: 'valid unscoped package name',
|
|
11
|
+
filename: 'package.json',
|
|
12
|
+
code: '{ "name": "n8n-nodes-example", "version": "1.0.0" }',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'valid unscoped package name with dashes',
|
|
16
|
+
filename: 'package.json',
|
|
17
|
+
code: '{ "name": "n8n-nodes-my-service", "version": "1.0.0" }',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'valid scoped package name',
|
|
21
|
+
filename: 'package.json',
|
|
22
|
+
code: '{ "name": "@mycompany/n8n-nodes-example", "version": "1.0.0" }',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'valid scoped package name with dashes',
|
|
26
|
+
filename: 'package.json',
|
|
27
|
+
code: '{ "name": "@author/n8n-nodes-service", "version": "1.0.0" }',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'object without name property',
|
|
31
|
+
filename: 'package.json',
|
|
32
|
+
code: '{ "version": "1.0.0", "description": "test" }',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'non-package.json file ignored',
|
|
36
|
+
filename: 'some-config.json',
|
|
37
|
+
code: '{ "name": "my-config", "type": "config" }',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'nested name fields should be ignored - only top-level name matters',
|
|
41
|
+
filename: 'package.json',
|
|
42
|
+
code: `{
|
|
43
|
+
"name": "n8n-nodes-example",
|
|
44
|
+
"version": "1.0.0",
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"name": "invalid-nested-name"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"name": "another-invalid-name"
|
|
50
|
+
},
|
|
51
|
+
"author": {
|
|
52
|
+
"name": "John Doe"
|
|
53
|
+
}
|
|
54
|
+
}`,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'deeply nested name fields should be ignored',
|
|
58
|
+
filename: 'package.json',
|
|
59
|
+
code: `{
|
|
60
|
+
"name": "@author/n8n-nodes-service",
|
|
61
|
+
"version": "1.0.0",
|
|
62
|
+
"config": {
|
|
63
|
+
"nested": {
|
|
64
|
+
"deeply": {
|
|
65
|
+
"name": "very-invalid-name"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"repository": {
|
|
70
|
+
"type": "git",
|
|
71
|
+
"url": "https://github.com/user/repo",
|
|
72
|
+
"directory": {
|
|
73
|
+
"name": "bad-name"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}`,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
invalid: [
|
|
80
|
+
{
|
|
81
|
+
name: 'invalid package name - generic',
|
|
82
|
+
filename: 'package.json',
|
|
83
|
+
code: '{ "name": "my-package", "version": "1.0.0" }',
|
|
84
|
+
errors: [
|
|
85
|
+
{
|
|
86
|
+
messageId: 'invalidPackageName',
|
|
87
|
+
data: { packageName: 'my-package' },
|
|
88
|
+
suggestions: [
|
|
89
|
+
{
|
|
90
|
+
messageId: 'renameTo',
|
|
91
|
+
data: { suggestedName: 'n8n-nodes-my-package' },
|
|
92
|
+
output: '{ "name": "n8n-nodes-my-package", "version": "1.0.0" }',
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'invalid package name - missing nodes',
|
|
100
|
+
filename: 'package.json',
|
|
101
|
+
code: '{ "name": "n8n-example", "version": "1.0.0" }',
|
|
102
|
+
errors: [
|
|
103
|
+
{
|
|
104
|
+
messageId: 'invalidPackageName',
|
|
105
|
+
data: { packageName: 'n8n-example' },
|
|
106
|
+
suggestions: [
|
|
107
|
+
{
|
|
108
|
+
messageId: 'renameTo',
|
|
109
|
+
data: { suggestedName: 'n8n-nodes-example' },
|
|
110
|
+
output: '{ "name": "n8n-nodes-example", "version": "1.0.0" }',
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'invalid scoped package name',
|
|
118
|
+
filename: 'package.json',
|
|
119
|
+
code: '{ "name": "@company/example-nodes", "version": "1.0.0" }',
|
|
120
|
+
errors: [
|
|
121
|
+
{
|
|
122
|
+
messageId: 'invalidPackageName',
|
|
123
|
+
data: { packageName: '@company/example-nodes' },
|
|
124
|
+
suggestions: [
|
|
125
|
+
{
|
|
126
|
+
messageId: 'renameTo',
|
|
127
|
+
data: { suggestedName: '@company/n8n-nodes-example' },
|
|
128
|
+
output: '{ "name": "@company/n8n-nodes-example", "version": "1.0.0" }',
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: 'invalid package name - wrong order',
|
|
136
|
+
filename: 'package.json',
|
|
137
|
+
code: '{ "name": "nodes-n8n-example", "version": "1.0.0" }',
|
|
138
|
+
errors: [
|
|
139
|
+
{
|
|
140
|
+
messageId: 'invalidPackageName',
|
|
141
|
+
data: { packageName: 'nodes-n8n-example' },
|
|
142
|
+
suggestions: [
|
|
143
|
+
{
|
|
144
|
+
messageId: 'renameTo',
|
|
145
|
+
data: { suggestedName: 'n8n-nodes-example' },
|
|
146
|
+
output: '{ "name": "n8n-nodes-example", "version": "1.0.0" }',
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'empty package name',
|
|
154
|
+
filename: 'package.json',
|
|
155
|
+
code: '{ "name": "", "version": "1.0.0" }',
|
|
156
|
+
errors: [
|
|
157
|
+
{
|
|
158
|
+
messageId: 'invalidPackageName',
|
|
159
|
+
data: { packageName: '' },
|
|
160
|
+
suggestions: [],
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: 'incomplete package name with missing suffix',
|
|
166
|
+
filename: 'package.json',
|
|
167
|
+
code: '{ "name": "n8n-nodes-", "version": "1.0.0" }',
|
|
168
|
+
errors: [
|
|
169
|
+
{
|
|
170
|
+
messageId: 'invalidPackageName',
|
|
171
|
+
data: { packageName: 'n8n-nodes-' },
|
|
172
|
+
suggestions: [],
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: 'incomplete scoped package name with missing suffix',
|
|
178
|
+
filename: 'package.json',
|
|
179
|
+
code: '{ "name": "@company/n8n-nodes-", "version": "1.0.0" }',
|
|
180
|
+
errors: [
|
|
181
|
+
{
|
|
182
|
+
messageId: 'invalidPackageName',
|
|
183
|
+
data: { packageName: '@company/n8n-nodes-' },
|
|
184
|
+
suggestions: [],
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
});
|