@n8n/eslint-plugin-community-nodes 0.22.0 → 0.24.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 +5 -0
- package/dist/plugin.d.ts +30 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +10 -0
- package/dist/plugin.js.map +1 -1
- package/dist/rules/index.d.ts +5 -0
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +10 -0
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/no-asterisk-in-option-names.d.ts +2 -0
- package/dist/rules/no-asterisk-in-option-names.d.ts.map +1 -0
- package/dist/rules/no-asterisk-in-option-names.js +90 -0
- package/dist/rules/no-asterisk-in-option-names.js.map +1 -0
- package/dist/rules/no-duplicate-param-options.d.ts +2 -0
- package/dist/rules/no-duplicate-param-options.d.ts.map +1 -0
- package/dist/rules/no-duplicate-param-options.js +97 -0
- package/dist/rules/no-duplicate-param-options.js.map +1 -0
- package/dist/rules/require-mit-license.d.ts +2 -0
- package/dist/rules/require-mit-license.d.ts.map +1 -0
- package/dist/rules/require-mit-license.js +57 -0
- package/dist/rules/require-mit-license.js.map +1 -0
- package/dist/rules/require-param-default.d.ts +15 -0
- package/dist/rules/require-param-default.d.ts.map +1 -0
- package/dist/rules/require-param-default.js +75 -0
- package/dist/rules/require-param-default.js.map +1 -0
- package/dist/rules/trigger-node-conventions.d.ts +2 -0
- package/dist/rules/trigger-node-conventions.d.ts.map +1 -0
- package/dist/rules/trigger-node-conventions.js +70 -0
- package/dist/rules/trigger-node-conventions.js.map +1 -0
- package/dist/typecheck.tsbuildinfo +1 -1
- package/docs/rules/no-asterisk-in-option-names.md +61 -0
- package/docs/rules/no-duplicate-param-options.md +46 -0
- package/docs/rules/require-mit-license.md +38 -0
- package/docs/rules/require-param-default.md +60 -0
- package/docs/rules/trigger-node-conventions.md +49 -0
- package/package.json +6 -5
- package/src/plugin.ts +10 -0
- package/src/rules/index.ts +10 -0
- package/src/rules/no-asterisk-in-option-names.test.ts +185 -0
- package/src/rules/no-asterisk-in-option-names.ts +111 -0
- package/src/rules/no-duplicate-param-options.test.ts +125 -0
- package/src/rules/no-duplicate-param-options.ts +125 -0
- package/src/rules/no-forbidden-lifecycle-scripts.test.ts +2 -2
- package/src/rules/require-mit-license.test.ts +67 -0
- package/src/rules/require-mit-license.ts +65 -0
- package/src/rules/require-param-default.test.ts +227 -0
- package/src/rules/require-param-default.ts +89 -0
- package/src/rules/trigger-node-conventions.test.ts +110 -0
- package/src/rules/trigger-node-conventions.ts +92 -0
- package/.turbo/turbo-build$colon$unchecked.log +0 -4
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
|
|
3
|
+
import { NoDuplicateParamOptionsRule } from './no-duplicate-param-options.js';
|
|
4
|
+
|
|
5
|
+
const ruleTester = new RuleTester();
|
|
6
|
+
|
|
7
|
+
function nodeCode(options: string, type = 'options'): string {
|
|
8
|
+
return `
|
|
9
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
10
|
+
|
|
11
|
+
export class TestNode implements INodeType {
|
|
12
|
+
description: INodeTypeDescription = {
|
|
13
|
+
displayName: 'Test Node',
|
|
14
|
+
name: 'testNode',
|
|
15
|
+
properties: [
|
|
16
|
+
{
|
|
17
|
+
displayName: 'Resource',
|
|
18
|
+
name: 'resource',
|
|
19
|
+
type: '${type}',
|
|
20
|
+
options: [${options}],
|
|
21
|
+
default: '',
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
ruleTester.run('no-duplicate-param-options', NoDuplicateParamOptionsRule, {
|
|
30
|
+
valid: [
|
|
31
|
+
{
|
|
32
|
+
name: 'non-INodeType class is skipped',
|
|
33
|
+
filename: '/tmp/TestNode.node.ts',
|
|
34
|
+
code: 'export class Foo {}',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'unique names and values',
|
|
38
|
+
filename: '/tmp/TestNode.node.ts',
|
|
39
|
+
code: nodeCode(`
|
|
40
|
+
{ name: 'Create', value: 'create' },
|
|
41
|
+
{ name: 'Delete', value: 'delete' },
|
|
42
|
+
`),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'unique numeric values',
|
|
46
|
+
filename: '/tmp/TestNode.node.ts',
|
|
47
|
+
code: nodeCode(`
|
|
48
|
+
{ name: 'One', value: 1 },
|
|
49
|
+
{ name: 'Two', value: 2 },
|
|
50
|
+
`),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'multiOptions with unique options',
|
|
54
|
+
filename: '/tmp/TestNode.node.ts',
|
|
55
|
+
code: nodeCode(
|
|
56
|
+
`
|
|
57
|
+
{ name: 'A', value: 'a' },
|
|
58
|
+
{ name: 'B', value: 'b' },
|
|
59
|
+
`,
|
|
60
|
+
'multiOptions',
|
|
61
|
+
),
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'non-node.ts file is skipped',
|
|
65
|
+
filename: '/tmp/TestNode.ts',
|
|
66
|
+
code: nodeCode(`
|
|
67
|
+
{ name: 'Dup', value: 'a' },
|
|
68
|
+
{ name: 'Dup', value: 'b' },
|
|
69
|
+
`),
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
invalid: [
|
|
73
|
+
{
|
|
74
|
+
name: 'duplicate name',
|
|
75
|
+
filename: '/tmp/TestNode.node.ts',
|
|
76
|
+
code: nodeCode(`
|
|
77
|
+
{ name: 'Create', value: 'create' },
|
|
78
|
+
{ name: 'Create', value: 'create2' },
|
|
79
|
+
`),
|
|
80
|
+
errors: [{ messageId: 'duplicateName', data: { value: 'Create', displayName: 'Resource' } }],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'duplicate value',
|
|
84
|
+
filename: '/tmp/TestNode.node.ts',
|
|
85
|
+
code: nodeCode(`
|
|
86
|
+
{ name: 'Create', value: 'create' },
|
|
87
|
+
{ name: 'Make', value: 'create' },
|
|
88
|
+
`),
|
|
89
|
+
errors: [{ messageId: 'duplicateValue', data: { value: 'create', displayName: 'Resource' } }],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'duplicate numeric value',
|
|
93
|
+
filename: '/tmp/TestNode.node.ts',
|
|
94
|
+
code: nodeCode(`
|
|
95
|
+
{ name: 'One', value: 1 },
|
|
96
|
+
{ name: 'Uno', value: 1 },
|
|
97
|
+
`),
|
|
98
|
+
errors: [{ messageId: 'duplicateValue', data: { value: '1', displayName: 'Resource' } }],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'duplicate name and value reported separately',
|
|
102
|
+
filename: '/tmp/TestNode.node.ts',
|
|
103
|
+
code: nodeCode(`
|
|
104
|
+
{ name: 'Create', value: 'create' },
|
|
105
|
+
{ name: 'Create', value: 'create' },
|
|
106
|
+
`),
|
|
107
|
+
errors: [
|
|
108
|
+
{ messageId: 'duplicateName', data: { value: 'Create', displayName: 'Resource' } },
|
|
109
|
+
{ messageId: 'duplicateValue', data: { value: 'create', displayName: 'Resource' } },
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: 'duplicate in multiOptions',
|
|
114
|
+
filename: '/tmp/TestNode.node.ts',
|
|
115
|
+
code: nodeCode(
|
|
116
|
+
`
|
|
117
|
+
{ name: 'A', value: 'a' },
|
|
118
|
+
{ name: 'A', value: 'b' },
|
|
119
|
+
`,
|
|
120
|
+
'multiOptions',
|
|
121
|
+
),
|
|
122
|
+
errors: [{ messageId: 'duplicateName', data: { value: 'A', displayName: 'Resource' } }],
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
isNodeTypeClass,
|
|
6
|
+
findClassProperty,
|
|
7
|
+
findObjectProperty,
|
|
8
|
+
getStringLiteralValue,
|
|
9
|
+
getLiteralValue,
|
|
10
|
+
isFileType,
|
|
11
|
+
createRule,
|
|
12
|
+
} from '../utils/index.js';
|
|
13
|
+
|
|
14
|
+
export const NoDuplicateParamOptionsRule = createRule({
|
|
15
|
+
name: 'no-duplicate-param-options',
|
|
16
|
+
meta: {
|
|
17
|
+
type: 'problem',
|
|
18
|
+
docs: {
|
|
19
|
+
description: 'Disallow duplicate option names or values within a single node parameter',
|
|
20
|
+
},
|
|
21
|
+
messages: {
|
|
22
|
+
duplicateName: 'Duplicate option name "{{ value }}" in parameter "{{ displayName }}".',
|
|
23
|
+
duplicateValue: 'Duplicate option value "{{ value }}" in parameter "{{ displayName }}".',
|
|
24
|
+
},
|
|
25
|
+
schema: [],
|
|
26
|
+
},
|
|
27
|
+
defaultOptions: [],
|
|
28
|
+
create(context) {
|
|
29
|
+
if (!isFileType(context.filename, '.node.ts')) {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const checkProperty = (property: TSESTree.ObjectExpression): void => {
|
|
34
|
+
const typeProperty = findObjectProperty(property, 'type');
|
|
35
|
+
const type = typeProperty ? getStringLiteralValue(typeProperty.value) : null;
|
|
36
|
+
if (type !== 'options' && type !== 'multiOptions') {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const optionsProperty = findObjectProperty(property, 'options');
|
|
41
|
+
if (
|
|
42
|
+
!optionsProperty?.value ||
|
|
43
|
+
optionsProperty.value.type !== AST_NODE_TYPES.ArrayExpression
|
|
44
|
+
) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const displayNameProperty = findObjectProperty(property, 'displayName');
|
|
49
|
+
const displayName =
|
|
50
|
+
(displayNameProperty ? getStringLiteralValue(displayNameProperty.value) : null) ??
|
|
51
|
+
'unknown';
|
|
52
|
+
|
|
53
|
+
const seenNames = new Set<string>();
|
|
54
|
+
const seenValues = new Set<string | number | boolean>();
|
|
55
|
+
|
|
56
|
+
for (const option of optionsProperty.value.elements) {
|
|
57
|
+
if (!option || option.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const nameProperty = findObjectProperty(option, 'name');
|
|
62
|
+
const name = nameProperty ? getStringLiteralValue(nameProperty.value) : null;
|
|
63
|
+
if (name !== null) {
|
|
64
|
+
if (seenNames.has(name)) {
|
|
65
|
+
context.report({
|
|
66
|
+
node: nameProperty!.value,
|
|
67
|
+
messageId: 'duplicateName',
|
|
68
|
+
data: { value: name, displayName },
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
seenNames.add(name);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const valueProperty = findObjectProperty(option, 'value');
|
|
75
|
+
const value = valueProperty ? getLiteralValue(valueProperty.value) : null;
|
|
76
|
+
if (value !== null) {
|
|
77
|
+
if (seenValues.has(value)) {
|
|
78
|
+
context.report({
|
|
79
|
+
node: valueProperty!.value,
|
|
80
|
+
messageId: 'duplicateValue',
|
|
81
|
+
data: { value: String(value), displayName },
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
seenValues.add(value);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const analyzeNodeDescription = (descriptionValue: TSESTree.Expression | null): void => {
|
|
90
|
+
if (!descriptionValue || descriptionValue.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const propertiesProperty = findObjectProperty(descriptionValue, 'properties');
|
|
95
|
+
if (
|
|
96
|
+
!propertiesProperty?.value ||
|
|
97
|
+
propertiesProperty.value.type !== AST_NODE_TYPES.ArrayExpression
|
|
98
|
+
) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const property of propertiesProperty.value.elements) {
|
|
103
|
+
if (!property || property.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
checkProperty(property);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
ClassDeclaration(node) {
|
|
112
|
+
if (!isNodeTypeClass(node)) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const descriptionProperty = findClassProperty(node, 'description');
|
|
117
|
+
if (!descriptionProperty) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
analyzeNodeDescription(descriptionProperty.value);
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
});
|
|
@@ -14,7 +14,7 @@ ruleTester.run('no-forbidden-lifecycle-scripts', NoForbiddenLifecycleScriptsRule
|
|
|
14
14
|
{
|
|
15
15
|
name: 'only safe scripts',
|
|
16
16
|
filename: 'package.json',
|
|
17
|
-
code: '{ "name": "n8n-nodes-example", "scripts": { "build": "tsc", "test": "
|
|
17
|
+
code: '{ "name": "n8n-nodes-example", "scripts": { "build": "tsc", "test": "vitest", "dev": "nodemon" } }',
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
20
|
name: 'non-package.json file is ignored',
|
|
@@ -82,7 +82,7 @@ ruleTester.run('no-forbidden-lifecycle-scripts', NoForbiddenLifecycleScriptsRule
|
|
|
82
82
|
{
|
|
83
83
|
name: 'mix of allowed and forbidden scripts — only forbidden reported',
|
|
84
84
|
filename: 'package.json',
|
|
85
|
-
code: '{ "name": "n8n-nodes-example", "scripts": { "build": "tsc", "prepare": "npm run build", "test": "
|
|
85
|
+
code: '{ "name": "n8n-nodes-example", "scripts": { "build": "tsc", "prepare": "npm run build", "test": "vitest" } }',
|
|
86
86
|
errors: [{ messageId: 'forbiddenScript', data: { scriptName: 'prepare' } }],
|
|
87
87
|
},
|
|
88
88
|
{
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
|
|
3
|
+
import { RequireMitLicenseRule } from './require-mit-license.js';
|
|
4
|
+
|
|
5
|
+
const ruleTester = new RuleTester();
|
|
6
|
+
|
|
7
|
+
ruleTester.run('require-mit-license', RequireMitLicenseRule, {
|
|
8
|
+
valid: [
|
|
9
|
+
{
|
|
10
|
+
name: 'license is MIT',
|
|
11
|
+
filename: 'package.json',
|
|
12
|
+
code: '{ "name": "n8n-nodes-example", "license": "MIT" }',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'non-package.json file is ignored',
|
|
16
|
+
filename: 'some-config.json',
|
|
17
|
+
code: '{ "name": "n8n-nodes-example", "license": "Apache-2.0" }',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'nested objects are not checked',
|
|
21
|
+
filename: 'package.json',
|
|
22
|
+
code: '{ "name": "n8n-nodes-example", "license": "MIT", "config": { "license": "Apache-2.0" } }',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'objects inside arrays are not flagged',
|
|
26
|
+
filename: 'package.json',
|
|
27
|
+
code: `{
|
|
28
|
+
"name": "n8n-nodes-example",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"contributors": [
|
|
31
|
+
{ "name": "Alice" },
|
|
32
|
+
{ "name": "Bob" }
|
|
33
|
+
]
|
|
34
|
+
}`,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
invalid: [
|
|
38
|
+
{
|
|
39
|
+
name: 'missing license field',
|
|
40
|
+
filename: 'package.json',
|
|
41
|
+
code: '{ "name": "n8n-nodes-example", "version": "1.0.0" }',
|
|
42
|
+
output: '{ "name": "n8n-nodes-example", "version": "1.0.0", "license": "MIT" }',
|
|
43
|
+
errors: [{ messageId: 'missingLicense' }],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'empty package.json object',
|
|
47
|
+
filename: 'package.json',
|
|
48
|
+
code: '{}',
|
|
49
|
+
output: '{ "license": "MIT" }',
|
|
50
|
+
errors: [{ messageId: 'missingLicense' }],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'wrong license value',
|
|
54
|
+
filename: 'package.json',
|
|
55
|
+
code: '{ "name": "n8n-nodes-example", "license": "Apache-2.0" }',
|
|
56
|
+
output: '{ "name": "n8n-nodes-example", "license": "MIT" }',
|
|
57
|
+
errors: [{ messageId: 'wrongLicense' }],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'case-mismatched license value',
|
|
61
|
+
filename: 'package.json',
|
|
62
|
+
code: '{ "name": "n8n-nodes-example", "license": "mit" }',
|
|
63
|
+
output: '{ "name": "n8n-nodes-example", "license": "MIT" }',
|
|
64
|
+
errors: [{ messageId: 'wrongLicense' }],
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
3
|
+
|
|
4
|
+
import { createRule, findJsonProperty, getStringLiteralValue } from '../utils/index.js';
|
|
5
|
+
|
|
6
|
+
const REQUIRED_LICENSE = 'MIT';
|
|
7
|
+
|
|
8
|
+
export const RequireMitLicenseRule = createRule({
|
|
9
|
+
name: 'require-mit-license',
|
|
10
|
+
meta: {
|
|
11
|
+
type: 'problem',
|
|
12
|
+
docs: {
|
|
13
|
+
description: 'Require the "license" field in community node package.json to be "MIT"',
|
|
14
|
+
},
|
|
15
|
+
fixable: 'code',
|
|
16
|
+
messages: {
|
|
17
|
+
missingLicense: `The package.json must have a "license" field set to "${REQUIRED_LICENSE}".`,
|
|
18
|
+
wrongLicense: `The "license" field must be "${REQUIRED_LICENSE}".`,
|
|
19
|
+
},
|
|
20
|
+
schema: [],
|
|
21
|
+
},
|
|
22
|
+
defaultOptions: [],
|
|
23
|
+
create(context) {
|
|
24
|
+
if (!context.filename.endsWith('package.json')) {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
ObjectExpression(node: TSESTree.ObjectExpression) {
|
|
30
|
+
if (node.parent?.type !== AST_NODE_TYPES.ExpressionStatement) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const licenseProp = findJsonProperty(node, 'license');
|
|
35
|
+
|
|
36
|
+
if (!licenseProp) {
|
|
37
|
+
context.report({
|
|
38
|
+
node,
|
|
39
|
+
messageId: 'missingLicense',
|
|
40
|
+
fix(fixer) {
|
|
41
|
+
const lastProp = node.properties[node.properties.length - 1];
|
|
42
|
+
if (!lastProp) {
|
|
43
|
+
return fixer.replaceText(node, `{ "license": "${REQUIRED_LICENSE}" }`);
|
|
44
|
+
}
|
|
45
|
+
return fixer.insertTextAfter(lastProp, `, "license": "${REQUIRED_LICENSE}"`);
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (getStringLiteralValue(licenseProp.value) === REQUIRED_LICENSE) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
context.report({
|
|
56
|
+
node: licenseProp.value,
|
|
57
|
+
messageId: 'wrongLicense',
|
|
58
|
+
fix(fixer) {
|
|
59
|
+
return fixer.replaceText(licenseProp.value, `"${REQUIRED_LICENSE}"`);
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
});
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
|
|
3
|
+
import { RequireParamDefaultRule } from './require-param-default.js';
|
|
4
|
+
|
|
5
|
+
const ruleTester = new RuleTester();
|
|
6
|
+
|
|
7
|
+
function createNodeCode(properties: string): string {
|
|
8
|
+
return `
|
|
9
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
10
|
+
|
|
11
|
+
export class TestNode implements INodeType {
|
|
12
|
+
description: INodeTypeDescription = {
|
|
13
|
+
displayName: 'Test Node',
|
|
14
|
+
name: 'testNode',
|
|
15
|
+
group: ['input'],
|
|
16
|
+
version: 1,
|
|
17
|
+
description: 'A test node',
|
|
18
|
+
defaults: { name: 'Test Node' },
|
|
19
|
+
inputs: [],
|
|
20
|
+
outputs: [],
|
|
21
|
+
properties: [
|
|
22
|
+
${properties}
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
ruleTester.run('require-param-default', RequireParamDefaultRule, {
|
|
30
|
+
valid: [
|
|
31
|
+
{
|
|
32
|
+
name: 'class that does not implement INodeType',
|
|
33
|
+
filename: '/tmp/TestNode.node.ts',
|
|
34
|
+
code: `
|
|
35
|
+
export class NotANode {
|
|
36
|
+
description = {
|
|
37
|
+
properties: [{ displayName: 'Field', name: 'field', type: 'string' }],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
`,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'param-shaped object outside description (e.g. in a method) is ignored',
|
|
44
|
+
filename: '/tmp/TestNode.node.ts',
|
|
45
|
+
code: `
|
|
46
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
47
|
+
|
|
48
|
+
export class TestNode implements INodeType {
|
|
49
|
+
description: INodeTypeDescription = {
|
|
50
|
+
displayName: 'Test Node',
|
|
51
|
+
name: 'testNode',
|
|
52
|
+
properties: [],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
methods = {
|
|
56
|
+
loadOptions: {
|
|
57
|
+
async getThings() {
|
|
58
|
+
const config = { displayName: 'X', name: 'x', type: 'string' };
|
|
59
|
+
return [config];
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
`,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'non .node.ts file is ignored',
|
|
68
|
+
filename: '/tmp/helper.ts',
|
|
69
|
+
code: `
|
|
70
|
+
export class TestNode {
|
|
71
|
+
description = {
|
|
72
|
+
properties: [{ displayName: 'Field', name: 'field', type: 'string' }],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
`,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'string parameter with default',
|
|
79
|
+
filename: '/tmp/TestNode.node.ts',
|
|
80
|
+
code: createNodeCode(`
|
|
81
|
+
{
|
|
82
|
+
displayName: 'Field',
|
|
83
|
+
name: 'field',
|
|
84
|
+
type: 'string',
|
|
85
|
+
default: '',
|
|
86
|
+
},
|
|
87
|
+
`),
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'options parameter with default',
|
|
91
|
+
filename: '/tmp/TestNode.node.ts',
|
|
92
|
+
code: createNodeCode(`
|
|
93
|
+
{
|
|
94
|
+
displayName: 'Operation',
|
|
95
|
+
name: 'operation',
|
|
96
|
+
type: 'options',
|
|
97
|
+
options: [{ name: 'Create', value: 'create' }],
|
|
98
|
+
default: 'create',
|
|
99
|
+
},
|
|
100
|
+
`),
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'notice parameter with empty default',
|
|
104
|
+
filename: '/tmp/TestNode.node.ts',
|
|
105
|
+
code: createNodeCode(`
|
|
106
|
+
{
|
|
107
|
+
displayName: 'Some notice',
|
|
108
|
+
name: 'notice',
|
|
109
|
+
type: 'notice',
|
|
110
|
+
default: '',
|
|
111
|
+
},
|
|
112
|
+
`),
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: 'options-array entries (name/value, no displayName) are not parameters',
|
|
116
|
+
filename: '/tmp/TestNode.node.ts',
|
|
117
|
+
code: createNodeCode(`
|
|
118
|
+
{
|
|
119
|
+
displayName: 'Operation',
|
|
120
|
+
name: 'operation',
|
|
121
|
+
type: 'options',
|
|
122
|
+
options: [{ name: 'Create', value: 'create', type: 'whatever' }],
|
|
123
|
+
default: 'create',
|
|
124
|
+
},
|
|
125
|
+
`),
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'object with name and type but no displayName is not a parameter',
|
|
129
|
+
filename: '/tmp/TestNode.node.ts',
|
|
130
|
+
code: createNodeCode(`
|
|
131
|
+
{
|
|
132
|
+
displayName: 'Routing',
|
|
133
|
+
name: 'routing',
|
|
134
|
+
type: 'string',
|
|
135
|
+
default: '',
|
|
136
|
+
routing: { request: { method: 'GET', name: 'x', type: 'y' } },
|
|
137
|
+
},
|
|
138
|
+
`),
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: 'nested collection options all have defaults',
|
|
142
|
+
filename: '/tmp/TestNode.node.ts',
|
|
143
|
+
code: createNodeCode(`
|
|
144
|
+
{
|
|
145
|
+
displayName: 'Additional Fields',
|
|
146
|
+
name: 'additionalFields',
|
|
147
|
+
type: 'collection',
|
|
148
|
+
default: {},
|
|
149
|
+
options: [
|
|
150
|
+
{
|
|
151
|
+
displayName: 'Limit',
|
|
152
|
+
name: 'limit',
|
|
153
|
+
type: 'number',
|
|
154
|
+
default: 50,
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
},
|
|
158
|
+
`),
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
invalid: [
|
|
162
|
+
{
|
|
163
|
+
name: 'string parameter missing default',
|
|
164
|
+
filename: '/tmp/TestNode.node.ts',
|
|
165
|
+
code: createNodeCode(`
|
|
166
|
+
{
|
|
167
|
+
displayName: 'Field',
|
|
168
|
+
name: 'field',
|
|
169
|
+
type: 'string',
|
|
170
|
+
},
|
|
171
|
+
`),
|
|
172
|
+
errors: [{ messageId: 'missingDefault', data: { name: 'field' } }],
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: 'notice parameter missing default',
|
|
176
|
+
filename: '/tmp/TestNode.node.ts',
|
|
177
|
+
code: createNodeCode(`
|
|
178
|
+
{
|
|
179
|
+
displayName: 'Some notice',
|
|
180
|
+
name: 'notice',
|
|
181
|
+
type: 'notice',
|
|
182
|
+
},
|
|
183
|
+
`),
|
|
184
|
+
errors: [{ messageId: 'missingDefault', data: { name: 'notice' } }],
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: 'nested collection option missing default',
|
|
188
|
+
filename: '/tmp/TestNode.node.ts',
|
|
189
|
+
code: createNodeCode(`
|
|
190
|
+
{
|
|
191
|
+
displayName: 'Additional Fields',
|
|
192
|
+
name: 'additionalFields',
|
|
193
|
+
type: 'collection',
|
|
194
|
+
default: {},
|
|
195
|
+
options: [
|
|
196
|
+
{
|
|
197
|
+
displayName: 'Limit',
|
|
198
|
+
name: 'limit',
|
|
199
|
+
type: 'number',
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
`),
|
|
204
|
+
errors: [{ messageId: 'missingDefault', data: { name: 'limit' } }],
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: 'multiple parameters missing defaults',
|
|
208
|
+
filename: '/tmp/TestNode.node.ts',
|
|
209
|
+
code: createNodeCode(`
|
|
210
|
+
{
|
|
211
|
+
displayName: 'First',
|
|
212
|
+
name: 'first',
|
|
213
|
+
type: 'string',
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
displayName: 'Second',
|
|
217
|
+
name: 'second',
|
|
218
|
+
type: 'boolean',
|
|
219
|
+
},
|
|
220
|
+
`),
|
|
221
|
+
errors: [
|
|
222
|
+
{ messageId: 'missingDefault', data: { name: 'first' } },
|
|
223
|
+
{ messageId: 'missingDefault', data: { name: 'second' } },
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
});
|