@n8n/eslint-plugin-community-nodes 0.19.0 → 0.21.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$colon$unchecked.log +4 -0
- package/README.md +9 -1
- package/dist/plugin.d.ts +48 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +16 -0
- package/dist/plugin.js.map +1 -1
- package/dist/rules/cred-filename-against-convention.d.ts +2 -0
- package/dist/rules/cred-filename-against-convention.d.ts.map +1 -0
- package/dist/rules/cred-filename-against-convention.js +42 -0
- package/dist/rules/cred-filename-against-convention.js.map +1 -0
- package/dist/rules/icon-prefer-themed-variants.d.ts +2 -0
- package/dist/rules/icon-prefer-themed-variants.d.ts.map +1 -0
- package/dist/rules/icon-prefer-themed-variants.js +53 -0
- package/dist/rules/icon-prefer-themed-variants.js.map +1 -0
- package/dist/rules/icon-validation.d.ts +1 -1
- package/dist/rules/icon-validation.d.ts.map +1 -1
- package/dist/rules/icon-validation.js +4 -25
- package/dist/rules/icon-validation.js.map +1 -1
- package/dist/rules/index.d.ts +9 -1
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +16 -0
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/no-dangerous-functions.d.ts +2 -0
- package/dist/rules/no-dangerous-functions.d.ts.map +1 -0
- package/dist/rules/no-dangerous-functions.js +121 -0
- package/dist/rules/no-dangerous-functions.js.map +1 -0
- package/dist/rules/no-emoji-in-options.d.ts +2 -0
- package/dist/rules/no-emoji-in-options.d.ts.map +1 -0
- package/dist/rules/no-emoji-in-options.js +86 -0
- package/dist/rules/no-emoji-in-options.js.map +1 -0
- package/dist/rules/node-filename-against-convention.d.ts +2 -0
- package/dist/rules/node-filename-against-convention.d.ts.map +1 -0
- package/dist/rules/node-filename-against-convention.js +61 -0
- package/dist/rules/node-filename-against-convention.js.map +1 -0
- package/dist/rules/node-registration-complete.d.ts +2 -0
- package/dist/rules/node-registration-complete.d.ts.map +1 -0
- package/dist/rules/node-registration-complete.js +60 -0
- package/dist/rules/node-registration-complete.js.map +1 -0
- package/dist/rules/require-version.d.ts +2 -0
- package/dist/rules/require-version.d.ts.map +1 -0
- package/dist/rules/require-version.js +50 -0
- package/dist/rules/require-version.js.map +1 -0
- package/dist/rules/valid-author.d.ts +2 -0
- package/dist/rules/valid-author.d.ts.map +1 -0
- package/dist/rules/valid-author.js +89 -0
- package/dist/rules/valid-author.js.map +1 -0
- package/dist/typecheck.tsbuildinfo +1 -0
- package/dist/utils/file-utils.d.ts +7 -2
- package/dist/utils/file-utils.d.ts.map +1 -1
- package/dist/utils/file-utils.js +47 -10
- package/dist/utils/file-utils.js.map +1 -1
- package/docs/rules/cred-filename-against-convention.md +42 -0
- package/docs/rules/icon-prefer-themed-variants.md +71 -0
- package/docs/rules/icon-validation.md +5 -3
- package/docs/rules/no-dangerous-functions.md +41 -0
- package/docs/rules/no-emoji-in-options.md +60 -0
- package/docs/rules/node-filename-against-convention.md +50 -0
- package/docs/rules/node-registration-complete.md +46 -0
- package/docs/rules/require-version.md +51 -0
- package/docs/rules/valid-author.md +60 -0
- package/package.json +5 -4
- package/src/plugin.ts +16 -0
- package/src/rules/cred-filename-against-convention.test.ts +72 -0
- package/src/rules/cred-filename-against-convention.ts +48 -0
- package/src/rules/icon-prefer-themed-variants.test.ts +128 -0
- package/src/rules/icon-prefer-themed-variants.ts +70 -0
- package/src/rules/icon-validation.test.ts +10 -0
- package/src/rules/icon-validation.ts +4 -28
- package/src/rules/index.ts +16 -0
- package/src/rules/no-dangerous-functions.test.ts +83 -0
- package/src/rules/no-dangerous-functions.ts +155 -0
- package/src/rules/no-emoji-in-options.test.ts +157 -0
- package/src/rules/no-emoji-in-options.ts +105 -0
- package/src/rules/node-filename-against-convention.test.ts +115 -0
- package/src/rules/node-filename-against-convention.ts +76 -0
- package/src/rules/node-registration-complete.test.ts +87 -0
- package/src/rules/node-registration-complete.ts +79 -0
- package/src/rules/require-version.test.ts +90 -0
- package/src/rules/require-version.ts +62 -0
- package/src/rules/valid-author.test.ts +108 -0
- package/src/rules/valid-author.ts +100 -0
- package/src/utils/file-utils.ts +58 -11
- package/.turbo/turbo-build.log +0 -4
- package/tsconfig.build.tsbuildinfo +0 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createRule,
|
|
7
|
+
findJsonProperty,
|
|
8
|
+
findNodeSourceFilesOnDisk,
|
|
9
|
+
readPackageJsonNodes,
|
|
10
|
+
} from '../utils/index.js';
|
|
11
|
+
|
|
12
|
+
export const NodeRegistrationCompleteRule = createRule({
|
|
13
|
+
name: 'node-registration-complete',
|
|
14
|
+
meta: {
|
|
15
|
+
type: 'problem',
|
|
16
|
+
docs: {
|
|
17
|
+
description:
|
|
18
|
+
'Ensure every `.node.ts` file in the `nodes/` directory is registered in the "n8n.nodes" array of package.json',
|
|
19
|
+
},
|
|
20
|
+
messages: {
|
|
21
|
+
nodeNotRegistered:
|
|
22
|
+
'The node file "{{ nodeFile }}" is not registered in the "n8n.nodes" array of package.json. Add it so n8n can discover the node.',
|
|
23
|
+
},
|
|
24
|
+
schema: [],
|
|
25
|
+
},
|
|
26
|
+
defaultOptions: [],
|
|
27
|
+
create(context) {
|
|
28
|
+
if (!context.filename.endsWith('package.json')) {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
ObjectExpression(node: TSESTree.ObjectExpression) {
|
|
34
|
+
// Only inspect the root object of the package.json file.
|
|
35
|
+
if (node.parent?.type !== AST_NODE_TYPES.ExpressionStatement) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const nodeFilesOnDisk = findNodeSourceFilesOnDisk(context.filename);
|
|
40
|
+
if (nodeFilesOnDisk.length === 0) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const registered = new Set(
|
|
45
|
+
readPackageJsonNodes(context.filename).map((filePath) => path.resolve(filePath)),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const packageDir = path.dirname(context.filename);
|
|
49
|
+
const reportTarget = resolveReportTarget(node);
|
|
50
|
+
|
|
51
|
+
for (const nodeFile of nodeFilesOnDisk) {
|
|
52
|
+
if (registered.has(path.resolve(nodeFile))) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
context.report({
|
|
57
|
+
node: reportTarget,
|
|
58
|
+
messageId: 'nodeNotRegistered',
|
|
59
|
+
data: { nodeFile: path.relative(packageDir, nodeFile) },
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Reports against the most specific available node: the `n8n.nodes` array, the
|
|
69
|
+
* `n8n` object, or the package.json root object as a fallback.
|
|
70
|
+
*/
|
|
71
|
+
function resolveReportTarget(root: TSESTree.ObjectExpression): TSESTree.Node {
|
|
72
|
+
const n8nProperty = findJsonProperty(root, 'n8n');
|
|
73
|
+
if (n8nProperty?.value.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
74
|
+
return n8nProperty ?? root;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const nodesProperty = findJsonProperty(n8nProperty.value, 'nodes');
|
|
78
|
+
return nodesProperty ?? n8nProperty;
|
|
79
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
|
|
3
|
+
import { RequireVersionRule } from './require-version.js';
|
|
4
|
+
|
|
5
|
+
const ruleTester = new RuleTester();
|
|
6
|
+
|
|
7
|
+
ruleTester.run('require-version', RequireVersionRule, {
|
|
8
|
+
valid: [
|
|
9
|
+
{
|
|
10
|
+
name: 'version is a valid semver string',
|
|
11
|
+
filename: 'package.json',
|
|
12
|
+
code: '{ "name": "n8n-nodes-example", "version": "1.0.0" }',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'version with pre-release and build metadata',
|
|
16
|
+
filename: 'package.json',
|
|
17
|
+
code: '{ "name": "n8n-nodes-example", "version": "1.2.3-beta.1+build.5" }',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'zero version is valid',
|
|
21
|
+
filename: 'package.json',
|
|
22
|
+
code: '{ "name": "n8n-nodes-example", "version": "0.1.0" }',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'non-package.json file is ignored',
|
|
26
|
+
filename: 'some-config.json',
|
|
27
|
+
code: '{ "name": "n8n-nodes-example" }',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'nested objects with missing version are not checked',
|
|
31
|
+
filename: 'package.json',
|
|
32
|
+
code: '{ "name": "n8n-nodes-example", "version": "1.0.0", "config": { "nested": "value" } }',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'objects inside arrays (e.g. contributors) are not flagged',
|
|
36
|
+
filename: 'package.json',
|
|
37
|
+
code: `{
|
|
38
|
+
"name": "n8n-nodes-example",
|
|
39
|
+
"version": "1.0.0",
|
|
40
|
+
"contributors": [
|
|
41
|
+
{ "name": "Alice", "email": "alice@example.com" }
|
|
42
|
+
]
|
|
43
|
+
}`,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
invalid: [
|
|
47
|
+
{
|
|
48
|
+
name: 'version field is missing entirely',
|
|
49
|
+
filename: 'package.json',
|
|
50
|
+
code: '{ "name": "n8n-nodes-example", "description": "Example" }',
|
|
51
|
+
errors: [{ messageId: 'missingVersion' }],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'empty package.json object',
|
|
55
|
+
filename: 'package.json',
|
|
56
|
+
code: '{}',
|
|
57
|
+
errors: [{ messageId: 'missingVersion' }],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'version is an empty string',
|
|
61
|
+
filename: 'package.json',
|
|
62
|
+
code: '{ "name": "n8n-nodes-example", "version": "" }',
|
|
63
|
+
errors: [{ messageId: 'invalidVersion' }],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'version is not a valid semver',
|
|
67
|
+
filename: 'package.json',
|
|
68
|
+
code: '{ "name": "n8n-nodes-example", "version": "1.0" }',
|
|
69
|
+
errors: [{ messageId: 'invalidVersion' }],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'version has a leading "v"',
|
|
73
|
+
filename: 'package.json',
|
|
74
|
+
code: '{ "name": "n8n-nodes-example", "version": "v1.0.0" }',
|
|
75
|
+
errors: [{ messageId: 'invalidVersion' }],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'version is a range, not an exact version',
|
|
79
|
+
filename: 'package.json',
|
|
80
|
+
code: '{ "name": "n8n-nodes-example", "version": "^1.0.0" }',
|
|
81
|
+
errors: [{ messageId: 'invalidVersion' }],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'version is a number, not a string',
|
|
85
|
+
filename: 'package.json',
|
|
86
|
+
code: '{ "name": "n8n-nodes-example", "version": 1 }',
|
|
87
|
+
errors: [{ messageId: 'invalidVersion' }],
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
3
|
+
|
|
4
|
+
import { createRule, findJsonProperty } from '../utils/index.js';
|
|
5
|
+
|
|
6
|
+
// Official SemVer 2.0.0 regex (https://semver.org/), anchored.
|
|
7
|
+
const SEMVER_REGEX =
|
|
8
|
+
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
|
|
9
|
+
|
|
10
|
+
export const RequireVersionRule = createRule({
|
|
11
|
+
name: 'require-version',
|
|
12
|
+
meta: {
|
|
13
|
+
type: 'problem',
|
|
14
|
+
docs: {
|
|
15
|
+
description: 'Require a valid "version" field in community node package.json',
|
|
16
|
+
},
|
|
17
|
+
messages: {
|
|
18
|
+
missingVersion:
|
|
19
|
+
'The package.json must have a "version" field. npm requires a valid semantic version (e.g. "1.0.0") to publish the package.',
|
|
20
|
+
invalidVersion:
|
|
21
|
+
'The "version" field must be a valid semantic version string (e.g. "1.0.0"), got {{ value }}.',
|
|
22
|
+
},
|
|
23
|
+
schema: [],
|
|
24
|
+
},
|
|
25
|
+
defaultOptions: [],
|
|
26
|
+
create(context) {
|
|
27
|
+
if (!context.filename.endsWith('package.json')) {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
ObjectExpression(node: TSESTree.ObjectExpression) {
|
|
33
|
+
if (node.parent?.type !== AST_NODE_TYPES.ExpressionStatement) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const versionProp = findJsonProperty(node, 'version');
|
|
38
|
+
|
|
39
|
+
if (!versionProp) {
|
|
40
|
+
context.report({
|
|
41
|
+
node,
|
|
42
|
+
messageId: 'missingVersion',
|
|
43
|
+
});
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const valueNode = versionProp.value;
|
|
48
|
+
const value = valueNode.type === AST_NODE_TYPES.Literal ? valueNode.value : null;
|
|
49
|
+
|
|
50
|
+
if (typeof value !== 'string' || !SEMVER_REGEX.test(value)) {
|
|
51
|
+
const rawValue =
|
|
52
|
+
valueNode.type === AST_NODE_TYPES.Literal ? String(valueNode.raw) : 'non-literal';
|
|
53
|
+
context.report({
|
|
54
|
+
node: versionProp,
|
|
55
|
+
messageId: 'invalidVersion',
|
|
56
|
+
data: { value: rawValue },
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
|
|
3
|
+
import { ValidAuthorRule } from './valid-author.js';
|
|
4
|
+
|
|
5
|
+
const ruleTester = new RuleTester();
|
|
6
|
+
|
|
7
|
+
ruleTester.run('valid-author', ValidAuthorRule, {
|
|
8
|
+
valid: [
|
|
9
|
+
{
|
|
10
|
+
name: 'author object with non-empty name and email',
|
|
11
|
+
filename: 'package.json',
|
|
12
|
+
code: '{ "name": "n8n-nodes-example", "author": { "name": "Jane Doe", "email": "jane@example.com" } }',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'author object with extra fields',
|
|
16
|
+
filename: 'package.json',
|
|
17
|
+
code: '{ "author": { "name": "Jane Doe", "email": "jane@example.com", "url": "https://example.com" } }',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'author shorthand string with name and email',
|
|
21
|
+
filename: 'package.json',
|
|
22
|
+
code: '{ "author": "Jane Doe <jane@example.com>" }',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'author shorthand string with name, email and url',
|
|
26
|
+
filename: 'package.json',
|
|
27
|
+
code: '{ "author": "Jane Doe <jane@example.com> (https://example.com)" }',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'non-package.json file is ignored',
|
|
31
|
+
filename: 'some-config.json',
|
|
32
|
+
code: '{ "name": "n8n-nodes-example" }',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'nested objects are not checked',
|
|
36
|
+
filename: 'package.json',
|
|
37
|
+
code: '{ "author": { "name": "Jane Doe", "email": "jane@example.com" }, "config": { "nested": "value" } }',
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
invalid: [
|
|
41
|
+
{
|
|
42
|
+
name: 'missing author field entirely',
|
|
43
|
+
filename: 'package.json',
|
|
44
|
+
code: '{ "name": "n8n-nodes-example", "version": "1.0.0" }',
|
|
45
|
+
errors: [{ messageId: 'authorMissing' }],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'empty package.json object',
|
|
49
|
+
filename: 'package.json',
|
|
50
|
+
code: '{}',
|
|
51
|
+
errors: [{ messageId: 'authorMissing' }],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'author object missing email',
|
|
55
|
+
filename: 'package.json',
|
|
56
|
+
code: '{ "author": { "name": "Jane Doe" } }',
|
|
57
|
+
errors: [{ messageId: 'authorEmailMissing' }],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'author object missing name',
|
|
61
|
+
filename: 'package.json',
|
|
62
|
+
code: '{ "author": { "email": "jane@example.com" } }',
|
|
63
|
+
errors: [{ messageId: 'authorNameMissing' }],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'author object with empty name and email strings',
|
|
67
|
+
filename: 'package.json',
|
|
68
|
+
code: '{ "author": { "name": "", "email": " " } }',
|
|
69
|
+
errors: [{ messageId: 'authorNameMissing' }, { messageId: 'authorEmailMissing' }],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'author object with non-string name',
|
|
73
|
+
filename: 'package.json',
|
|
74
|
+
code: '{ "author": { "name": 123, "email": "jane@example.com" } }',
|
|
75
|
+
errors: [{ messageId: 'authorNameMissing' }],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'author shorthand string with name only',
|
|
79
|
+
filename: 'package.json',
|
|
80
|
+
code: '{ "author": "Jane Doe" }',
|
|
81
|
+
errors: [{ messageId: 'authorEmailMissing' }],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'author shorthand string with email only',
|
|
85
|
+
filename: 'package.json',
|
|
86
|
+
code: '{ "author": "<jane@example.com>" }',
|
|
87
|
+
errors: [{ messageId: 'authorNameMissing' }],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'author shorthand string with empty email angle brackets',
|
|
91
|
+
filename: 'package.json',
|
|
92
|
+
code: '{ "author": "Jane Doe <>" }',
|
|
93
|
+
errors: [{ messageId: 'authorEmailMissing' }],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'empty author string',
|
|
97
|
+
filename: 'package.json',
|
|
98
|
+
code: '{ "author": "" }',
|
|
99
|
+
errors: [{ messageId: 'authorNameMissing' }, { messageId: 'authorEmailMissing' }],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'author as empty array',
|
|
103
|
+
filename: 'package.json',
|
|
104
|
+
code: '{ "author": [] }',
|
|
105
|
+
errors: [{ messageId: 'authorMissing' }],
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
3
|
+
|
|
4
|
+
import { createRule, findJsonProperty, getTopLevelObjectInJson } from '../utils/index.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parses an npm "person" shorthand string of the form
|
|
8
|
+
* `Name <email> (url)` into its name and email parts. Any part may be absent.
|
|
9
|
+
*/
|
|
10
|
+
function parsePersonString(value: string): { name: string; email: string } {
|
|
11
|
+
const emailMatch = /<([^>]*)>/.exec(value);
|
|
12
|
+
const email = (emailMatch?.[1] ?? '').trim();
|
|
13
|
+
|
|
14
|
+
const ltIndex = value.indexOf('<');
|
|
15
|
+
const parenIndex = value.indexOf('(');
|
|
16
|
+
let nameEnd = value.length;
|
|
17
|
+
if (ltIndex !== -1) nameEnd = Math.min(nameEnd, ltIndex);
|
|
18
|
+
if (parenIndex !== -1) nameEnd = Math.min(nameEnd, parenIndex);
|
|
19
|
+
const name = value.slice(0, nameEnd).trim();
|
|
20
|
+
|
|
21
|
+
return { name, email };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isNonEmptyStringLiteral(node: TSESTree.Property | null): boolean {
|
|
25
|
+
if (!node || node.value.type !== AST_NODE_TYPES.Literal) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const { value } = node.value;
|
|
29
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const ValidAuthorRule = createRule({
|
|
33
|
+
name: 'valid-author',
|
|
34
|
+
meta: {
|
|
35
|
+
type: 'problem',
|
|
36
|
+
docs: {
|
|
37
|
+
description: 'Require a non-empty author name and email in package.json',
|
|
38
|
+
},
|
|
39
|
+
messages: {
|
|
40
|
+
authorMissing: 'package.json must have an "author" field with a non-empty name and email.',
|
|
41
|
+
authorNameMissing: 'The "author" field must include a non-empty name.',
|
|
42
|
+
authorEmailMissing: 'The "author" field must include a non-empty email.',
|
|
43
|
+
},
|
|
44
|
+
schema: [],
|
|
45
|
+
},
|
|
46
|
+
defaultOptions: [],
|
|
47
|
+
create(context) {
|
|
48
|
+
if (!context.filename.endsWith('package.json')) {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
ObjectExpression(node: TSESTree.ObjectExpression) {
|
|
54
|
+
const root = getTopLevelObjectInJson(node);
|
|
55
|
+
if (!root) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const authorProp = findJsonProperty(root, 'author');
|
|
60
|
+
if (!authorProp) {
|
|
61
|
+
context.report({ node: root, messageId: 'authorMissing' });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const authorValue = authorProp.value;
|
|
66
|
+
|
|
67
|
+
// Shorthand string form: "Name <email> (url)"
|
|
68
|
+
if (authorValue.type === AST_NODE_TYPES.Literal) {
|
|
69
|
+
if (typeof authorValue.value !== 'string') {
|
|
70
|
+
context.report({ node: authorProp, messageId: 'authorMissing' });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const { name, email } = parsePersonString(authorValue.value);
|
|
75
|
+
if (name.length === 0) {
|
|
76
|
+
context.report({ node: authorProp, messageId: 'authorNameMissing' });
|
|
77
|
+
}
|
|
78
|
+
if (email.length === 0) {
|
|
79
|
+
context.report({ node: authorProp, messageId: 'authorEmailMissing' });
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Object form: { "name": "...", "email": "..." }
|
|
85
|
+
if (authorValue.type === AST_NODE_TYPES.ObjectExpression) {
|
|
86
|
+
if (!isNonEmptyStringLiteral(findJsonProperty(authorValue, 'name'))) {
|
|
87
|
+
context.report({ node: authorProp, messageId: 'authorNameMissing' });
|
|
88
|
+
}
|
|
89
|
+
if (!isNonEmptyStringLiteral(findJsonProperty(authorValue, 'email'))) {
|
|
90
|
+
context.report({ node: authorProp, messageId: 'authorEmailMissing' });
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Any other shape (e.g. array, null) cannot carry a name and email.
|
|
96
|
+
context.report({ node: authorProp, messageId: 'authorMissing' });
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
});
|
package/src/utils/file-utils.ts
CHANGED
|
@@ -158,20 +158,17 @@ export function validateIconPath(
|
|
|
158
158
|
): {
|
|
159
159
|
isValid: boolean;
|
|
160
160
|
isFile: boolean;
|
|
161
|
-
isSvg: boolean;
|
|
162
161
|
exists: boolean;
|
|
163
162
|
} {
|
|
164
163
|
const isFile = iconPath.startsWith('file:');
|
|
165
164
|
const relativePath = iconPath.replace(/^file:/, '');
|
|
166
|
-
const isSvg = relativePath.endsWith('.svg');
|
|
167
165
|
// Should not use safeJoinPath here because iconPath can be outside of the node class folder
|
|
168
166
|
const fullPath = path.join(baseDir, relativePath);
|
|
169
167
|
const exists = fileExistsWithCaseSync(fullPath);
|
|
170
168
|
|
|
171
169
|
return {
|
|
172
|
-
isValid: isFile &&
|
|
170
|
+
isValid: isFile && exists,
|
|
173
171
|
isFile,
|
|
174
|
-
isSvg,
|
|
175
172
|
exists,
|
|
176
173
|
};
|
|
177
174
|
}
|
|
@@ -182,6 +179,44 @@ export function readPackageJsonNodes(packageJsonPath: string): string[] {
|
|
|
182
179
|
return resolveN8nFilePaths(packageJsonPath, nodePaths);
|
|
183
180
|
}
|
|
184
181
|
|
|
182
|
+
function findFilesRecursively(dir: string, matches: (fileName: string) => boolean): string[] {
|
|
183
|
+
const results: string[] = [];
|
|
184
|
+
|
|
185
|
+
let entries;
|
|
186
|
+
try {
|
|
187
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
188
|
+
} catch {
|
|
189
|
+
return results;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
for (const entry of entries) {
|
|
193
|
+
const fullPath = path.join(dir, entry.name);
|
|
194
|
+
if (entry.isDirectory()) {
|
|
195
|
+
results.push(...findFilesRecursively(fullPath, matches));
|
|
196
|
+
} else if (entry.isFile() && matches(entry.name)) {
|
|
197
|
+
results.push(fullPath);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return results;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Finds all `*.node.ts` source files in the package's `nodes/` directory,
|
|
206
|
+
* returning their absolute paths. Returns an empty array if there is no
|
|
207
|
+
* `nodes/` directory.
|
|
208
|
+
*/
|
|
209
|
+
export function findNodeSourceFilesOnDisk(packageJsonPath: string): string[] {
|
|
210
|
+
const packageDir = dirname(packageJsonPath);
|
|
211
|
+
const nodesDir = safeJoinPath(packageDir, 'nodes');
|
|
212
|
+
|
|
213
|
+
if (!existsSync(nodesDir)) {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return findFilesRecursively(nodesDir, (fileName) => fileName.endsWith('.node.ts'));
|
|
218
|
+
}
|
|
219
|
+
|
|
185
220
|
export function areAllCredentialUsagesTestedByNodes(
|
|
186
221
|
credentialName: string,
|
|
187
222
|
packageDir: string,
|
|
@@ -268,7 +303,9 @@ function fileExistsWithCaseSync(filePath: string): boolean {
|
|
|
268
303
|
}
|
|
269
304
|
}
|
|
270
305
|
|
|
271
|
-
|
|
306
|
+
const ICON_EXTENSIONS = ['.svg', '.png'];
|
|
307
|
+
|
|
308
|
+
export function findSimilarIconFiles(targetPath: string, baseDir: string): string[] {
|
|
272
309
|
try {
|
|
273
310
|
const targetFileName = path.basename(targetPath, path.extname(targetPath));
|
|
274
311
|
const targetDir = path.dirname(targetPath);
|
|
@@ -279,15 +316,25 @@ export function findSimilarSvgFiles(targetPath: string, baseDir: string): string
|
|
|
279
316
|
return [];
|
|
280
317
|
}
|
|
281
318
|
|
|
282
|
-
const files = readdirSync(searchDir)
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
319
|
+
const files = readdirSync(searchDir).filter((file) =>
|
|
320
|
+
ICON_EXTENSIONS.includes(path.extname(file).toLowerCase()),
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
// Map icon base names to their actual filenames so suggestions keep their extension.
|
|
324
|
+
const baseNameToFiles = new Map<string, string[]>();
|
|
325
|
+
for (const file of files) {
|
|
326
|
+
const baseName = path.basename(file, path.extname(file));
|
|
327
|
+
const existing = baseNameToFiles.get(baseName) ?? [];
|
|
328
|
+
existing.push(file);
|
|
329
|
+
baseNameToFiles.set(baseName, existing);
|
|
330
|
+
}
|
|
286
331
|
|
|
287
|
-
const candidateNames = new Set(
|
|
332
|
+
const candidateNames = new Set(baseNameToFiles.keys());
|
|
288
333
|
const similarNames = findSimilarStrings(targetFileName, candidateNames);
|
|
289
334
|
|
|
290
|
-
return similarNames.
|
|
335
|
+
return similarNames.flatMap((name) =>
|
|
336
|
+
(baseNameToFiles.get(name) ?? []).map((file) => path.join(targetDir, file)),
|
|
337
|
+
);
|
|
291
338
|
} catch {
|
|
292
339
|
return [];
|
|
293
340
|
}
|
package/.turbo/turbo-build.log
DELETED