@n8n/eslint-plugin-community-nodes 0.26.0 → 0.28.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.
Files changed (92) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/README.md +7 -1
  3. package/dist/plugin.d.ts +36 -0
  4. package/dist/plugin.d.ts.map +1 -1
  5. package/dist/plugin.js +12 -0
  6. package/dist/plugin.js.map +1 -1
  7. package/dist/rules/credential-password-field.d.ts.map +1 -1
  8. package/dist/rules/credential-password-field.js +3 -31
  9. package/dist/rules/credential-password-field.js.map +1 -1
  10. package/dist/rules/credential-unnecessary-password.d.ts +2 -0
  11. package/dist/rules/credential-unnecessary-password.d.ts.map +1 -0
  12. package/dist/rules/credential-unnecessary-password.js +54 -0
  13. package/dist/rules/credential-unnecessary-password.js.map +1 -0
  14. package/dist/rules/index.d.ts +7 -1
  15. package/dist/rules/index.d.ts.map +1 -1
  16. package/dist/rules/index.js +12 -0
  17. package/dist/rules/index.js.map +1 -1
  18. package/dist/rules/no-dead-files.d.ts +7 -0
  19. package/dist/rules/no-dead-files.d.ts.map +1 -0
  20. package/dist/rules/no-dead-files.js +57 -0
  21. package/dist/rules/no-dead-files.js.map +1 -0
  22. package/dist/rules/no-hardcoded-secrets.d.ts +2 -0
  23. package/dist/rules/no-hardcoded-secrets.d.ts.map +1 -0
  24. package/dist/rules/no-hardcoded-secrets.js +147 -0
  25. package/dist/rules/no-hardcoded-secrets.js.map +1 -0
  26. package/dist/rules/no-restricted-globals.d.ts +1 -1
  27. package/dist/rules/no-restricted-globals.d.ts.map +1 -1
  28. package/dist/rules/no-restricted-globals.js +18 -5
  29. package/dist/rules/no-restricted-globals.js.map +1 -1
  30. package/dist/rules/no-silent-error-swallowing.d.ts +2 -0
  31. package/dist/rules/no-silent-error-swallowing.d.ts.map +1 -0
  32. package/dist/rules/no-silent-error-swallowing.js +112 -0
  33. package/dist/rules/no-silent-error-swallowing.js.map +1 -0
  34. package/dist/rules/require-files-array.d.ts +2 -0
  35. package/dist/rules/require-files-array.d.ts.map +1 -0
  36. package/dist/rules/require-files-array.js +41 -0
  37. package/dist/rules/require-files-array.js.map +1 -0
  38. package/dist/rules/require-homepage.d.ts +2 -0
  39. package/dist/rules/require-homepage.d.ts.map +1 -0
  40. package/dist/rules/require-homepage.js +52 -0
  41. package/dist/rules/require-homepage.js.map +1 -0
  42. package/dist/rules/webhook-lifecycle-complete.d.ts.map +1 -1
  43. package/dist/rules/webhook-lifecycle-complete.js +2 -3
  44. package/dist/rules/webhook-lifecycle-complete.js.map +1 -1
  45. package/dist/typecheck.tsbuildinfo +1 -1
  46. package/dist/utils/constants.d.ts +5 -0
  47. package/dist/utils/constants.d.ts.map +1 -0
  48. package/dist/utils/constants.js +15 -0
  49. package/dist/utils/constants.js.map +1 -0
  50. package/dist/utils/credential-fields.d.ts +20 -0
  51. package/dist/utils/credential-fields.d.ts.map +1 -0
  52. package/dist/utils/credential-fields.js +45 -0
  53. package/dist/utils/credential-fields.js.map +1 -0
  54. package/dist/utils/file-utils.d.ts +6 -0
  55. package/dist/utils/file-utils.d.ts.map +1 -1
  56. package/dist/utils/file-utils.js +16 -0
  57. package/dist/utils/file-utils.js.map +1 -1
  58. package/dist/utils/index.d.ts +2 -0
  59. package/dist/utils/index.d.ts.map +1 -1
  60. package/dist/utils/index.js +2 -0
  61. package/dist/utils/index.js.map +1 -1
  62. package/docs/rules/credential-password-field.md +3 -1
  63. package/docs/rules/credential-unnecessary-password.md +46 -0
  64. package/docs/rules/no-dead-files.md +56 -0
  65. package/docs/rules/no-hardcoded-secrets.md +59 -0
  66. package/docs/rules/no-restricted-globals.md +14 -0
  67. package/docs/rules/no-silent-error-swallowing.md +72 -0
  68. package/docs/rules/require-files-array.md +43 -0
  69. package/docs/rules/require-homepage.md +49 -0
  70. package/package.json +2 -2
  71. package/src/plugin.ts +12 -0
  72. package/src/rules/credential-password-field.ts +4 -40
  73. package/src/rules/credential-unnecessary-password.test.ts +105 -0
  74. package/src/rules/credential-unnecessary-password.ts +72 -0
  75. package/src/rules/index.ts +12 -0
  76. package/src/rules/no-dead-files.test.ts +128 -0
  77. package/src/rules/no-dead-files.ts +71 -0
  78. package/src/rules/no-hardcoded-secrets.test.ts +124 -0
  79. package/src/rules/no-hardcoded-secrets.ts +161 -0
  80. package/src/rules/no-restricted-globals.test.ts +26 -2
  81. package/src/rules/no-restricted-globals.ts +22 -5
  82. package/src/rules/no-silent-error-swallowing.test.ts +245 -0
  83. package/src/rules/no-silent-error-swallowing.ts +127 -0
  84. package/src/rules/require-files-array.test.ts +57 -0
  85. package/src/rules/require-files-array.ts +50 -0
  86. package/src/rules/require-homepage.test.ts +80 -0
  87. package/src/rules/require-homepage.ts +59 -0
  88. package/src/rules/webhook-lifecycle-complete.ts +4 -5
  89. package/src/utils/constants.ts +21 -0
  90. package/src/utils/credential-fields.ts +57 -0
  91. package/src/utils/file-utils.ts +23 -0
  92. package/src/utils/index.ts +2 -0
package/src/plugin.ts CHANGED
@@ -24,6 +24,7 @@ const configs = {
24
24
  '@n8n/community-nodes/no-restricted-globals': 'error',
25
25
  '@n8n/community-nodes/no-restricted-imports': 'error',
26
26
  '@n8n/community-nodes/credential-password-field': 'error',
27
+ '@n8n/community-nodes/credential-unnecessary-password': 'warn',
27
28
  '@n8n/community-nodes/n8n-object-validation': 'error',
28
29
  '@n8n/community-nodes/no-deprecated-workflow-functions': 'error',
29
30
  '@n8n/community-nodes/no-emoji-in-options': 'error',
@@ -32,10 +33,13 @@ const configs = {
32
33
  '@n8n/community-nodes/credential-test-required': 'error',
33
34
  '@n8n/community-nodes/no-credential-reuse': 'error',
34
35
  '@n8n/community-nodes/no-dangerous-functions': 'error',
36
+ '@n8n/community-nodes/no-dead-files': 'warn',
35
37
  '@n8n/community-nodes/no-forbidden-lifecycle-scripts': 'error',
38
+ '@n8n/community-nodes/no-hardcoded-secrets': 'error',
36
39
  '@n8n/community-nodes/no-http-request-with-manual-auth': 'error',
37
40
  '@n8n/community-nodes/no-overrides-field': 'error',
38
41
  '@n8n/community-nodes/no-runtime-dependencies': 'error',
42
+ '@n8n/community-nodes/no-silent-error-swallowing': 'error',
39
43
  '@n8n/community-nodes/no-template-placeholders': 'error',
40
44
  '@n8n/community-nodes/icon-validation': 'error',
41
45
  '@n8n/community-nodes/icon-prefer-themed-variants': 'warn',
@@ -48,6 +52,8 @@ const configs = {
48
52
  '@n8n/community-nodes/missing-paired-item': 'error',
49
53
  '@n8n/community-nodes/no-builder-hint-leakage': 'error',
50
54
  '@n8n/community-nodes/node-registration-complete': 'warn',
55
+ '@n8n/community-nodes/require-files-array': 'warn',
56
+ '@n8n/community-nodes/require-homepage': 'warn',
51
57
  '@n8n/community-nodes/require-node-api-error': 'error',
52
58
  '@n8n/community-nodes/require-node-description-fields': 'error',
53
59
  '@n8n/community-nodes/require-version': 'error',
@@ -66,6 +72,7 @@ const configs = {
66
72
  rules: {
67
73
  '@n8n/community-nodes/ai-node-package-json': 'error',
68
74
  '@n8n/community-nodes/credential-password-field': 'error',
75
+ '@n8n/community-nodes/credential-unnecessary-password': 'warn',
69
76
  '@n8n/community-nodes/n8n-object-validation': 'error',
70
77
  '@n8n/community-nodes/no-deprecated-workflow-functions': 'error',
71
78
  '@n8n/community-nodes/no-emoji-in-options': 'error',
@@ -74,10 +81,13 @@ const configs = {
74
81
  '@n8n/community-nodes/credential-test-required': 'error',
75
82
  '@n8n/community-nodes/no-credential-reuse': 'error',
76
83
  '@n8n/community-nodes/no-dangerous-functions': 'error',
84
+ '@n8n/community-nodes/no-dead-files': 'warn',
77
85
  '@n8n/community-nodes/no-forbidden-lifecycle-scripts': 'error',
86
+ '@n8n/community-nodes/no-hardcoded-secrets': 'error',
78
87
  '@n8n/community-nodes/no-http-request-with-manual-auth': 'error',
79
88
  '@n8n/community-nodes/no-overrides-field': 'error',
80
89
  '@n8n/community-nodes/no-runtime-dependencies': 'error',
90
+ '@n8n/community-nodes/no-silent-error-swallowing': 'error',
81
91
  '@n8n/community-nodes/no-template-placeholders': 'error',
82
92
  '@n8n/community-nodes/icon-validation': 'error',
83
93
  '@n8n/community-nodes/icon-prefer-themed-variants': 'warn',
@@ -90,6 +100,8 @@ const configs = {
90
100
  '@n8n/community-nodes/missing-paired-item': 'error',
91
101
  '@n8n/community-nodes/no-builder-hint-leakage': 'error',
92
102
  '@n8n/community-nodes/node-registration-complete': 'warn',
103
+ '@n8n/community-nodes/require-files-array': 'warn',
104
+ '@n8n/community-nodes/require-homepage': 'warn',
93
105
  '@n8n/community-nodes/require-node-api-error': 'error',
94
106
  '@n8n/community-nodes/require-node-description-fields': 'error',
95
107
  '@n8n/community-nodes/require-version': 'error',
@@ -6,47 +6,11 @@ import {
6
6
  findClassProperty,
7
7
  findObjectProperty,
8
8
  getStringLiteralValue,
9
- getBooleanLiteralValue,
9
+ isSensitiveName,
10
+ hasPasswordTypeOption,
10
11
  createRule,
11
12
  } from '../utils/index.js';
12
13
 
13
- const SENSITIVE_PATTERNS = [
14
- 'password',
15
- 'secret',
16
- 'token',
17
- 'cert',
18
- 'passphrase',
19
- 'apikey',
20
- 'secretkey',
21
- 'privatekey',
22
- 'authkey',
23
- ];
24
-
25
- const NON_SENSITIVE_PATTERNS = ['url', 'pub', 'id'];
26
-
27
- function isSensitiveFieldName(name: string): boolean {
28
- const lowerName = name.toLowerCase();
29
-
30
- if (NON_SENSITIVE_PATTERNS.some((pattern) => lowerName.includes(pattern))) {
31
- return false;
32
- }
33
-
34
- return SENSITIVE_PATTERNS.some((pattern) => lowerName.includes(pattern));
35
- }
36
-
37
- function hasPasswordTypeOption(element: TSESTree.ObjectExpression): boolean {
38
- const typeOptionsProperty = findObjectProperty(element, 'typeOptions');
39
-
40
- if (typeOptionsProperty?.value.type !== TSESTree.AST_NODE_TYPES.ObjectExpression) {
41
- return false;
42
- }
43
-
44
- const passwordProperty = findObjectProperty(typeOptionsProperty.value, 'password');
45
- const passwordValue = passwordProperty ? getBooleanLiteralValue(passwordProperty.value) : null;
46
-
47
- return passwordValue === true;
48
- }
49
-
50
14
  function createPasswordFix(
51
15
  element: TSESTree.ObjectExpression,
52
16
  typeOptionsProperty: TSESTree.Property | null,
@@ -87,7 +51,7 @@ export const CredentialPasswordFieldRule = createRule({
87
51
  meta: {
88
52
  type: 'problem',
89
53
  docs: {
90
- description: 'Ensure credential fields with sensitive names have typeOptions.password = true',
54
+ description: 'Ensure fields with sensitive names have typeOptions.password = true',
91
55
  },
92
56
  messages: {
93
57
  missingPasswordOption:
@@ -120,7 +84,7 @@ export const CredentialPasswordFieldRule = createRule({
120
84
  const nameProperty = findObjectProperty(element, 'name');
121
85
  const fieldName = nameProperty ? getStringLiteralValue(nameProperty.value) : null;
122
86
 
123
- if (!fieldName || !isSensitiveFieldName(fieldName)) {
87
+ if (!fieldName || !isSensitiveName(fieldName)) {
124
88
  continue;
125
89
  }
126
90
 
@@ -0,0 +1,105 @@
1
+ import { RuleTester } from '@typescript-eslint/rule-tester';
2
+
3
+ import { CredentialUnnecessaryPasswordRule } from './credential-unnecessary-password.js';
4
+
5
+ const ruleTester = new RuleTester();
6
+
7
+ function createCredentialCode(properties: string[]): string {
8
+ return `
9
+ import type { ICredentialType, INodeProperties } from 'n8n-workflow';
10
+
11
+ export class TestCredential implements ICredentialType {
12
+ name = 'testApi';
13
+ displayName = 'Test API';
14
+
15
+ properties: INodeProperties[] = [
16
+ ${properties.map((prop) => `\t\t${prop}`).join(',\n')},
17
+ ];
18
+ }`;
19
+ }
20
+
21
+ function createProperty(
22
+ displayName: string,
23
+ name: string,
24
+ options: { password?: boolean } = {},
25
+ ): string {
26
+ const typeOptionsStr =
27
+ options.password !== undefined ? `\n\t\t\ttypeOptions: { password: ${options.password} },` : '';
28
+
29
+ return `{
30
+ displayName: '${displayName}',
31
+ name: '${name}',
32
+ type: 'string',
33
+ default: '',${typeOptionsStr}
34
+ }`;
35
+ }
36
+
37
+ function createRegularClass(): string {
38
+ return `
39
+ export class RegularClass {
40
+ properties = [
41
+ {
42
+ name: 'region',
43
+ type: 'string',
44
+ typeOptions: { password: true },
45
+ },
46
+ ];
47
+ }`;
48
+ }
49
+
50
+ ruleTester.run('credential-unnecessary-password', CredentialUnnecessaryPasswordRule, {
51
+ valid: [
52
+ {
53
+ name: 'non-sensitive field without password option',
54
+ code: createCredentialCode([createProperty('Region', 'region')]),
55
+ },
56
+ {
57
+ name: 'non-sensitive field with password: false',
58
+ code: createCredentialCode([createProperty('Base URL', 'baseUrl', { password: false })]),
59
+ },
60
+ {
61
+ name: 'sensitive field correctly masked',
62
+ code: createCredentialCode([createProperty('API Key', 'apiKey', { password: true })]),
63
+ },
64
+ {
65
+ name: 'masked field with a sensitive marker is left alone even when its name contains a non-sensitive substring',
66
+ code: createCredentialCode([
67
+ // 'androidToken' contains the non-sensitive substring 'id' but is a real secret
68
+ createProperty('Android Token', 'androidToken', { password: true }),
69
+ createProperty('Token URL', 'tokenUrl', { password: true }),
70
+ ]),
71
+ },
72
+ {
73
+ name: 'class does not implement ICredentialType',
74
+ code: createRegularClass(),
75
+ },
76
+ ],
77
+ invalid: [
78
+ {
79
+ name: 'region field should not use password option',
80
+ code: createCredentialCode([createProperty('Region', 'region', { password: true })]),
81
+ errors: [{ messageId: 'unnecessaryPasswordOption', data: { fieldName: 'region' } }],
82
+ },
83
+ {
84
+ name: 'URL field should not use password option',
85
+ code: createCredentialCode([createProperty('Base URL', 'baseUrl', { password: true })]),
86
+ errors: [{ messageId: 'unnecessaryPasswordOption', data: { fieldName: 'baseUrl' } }],
87
+ },
88
+ {
89
+ name: 'ID field should not use password option',
90
+ code: createCredentialCode([createProperty('Client ID', 'clientId', { password: true })]),
91
+ errors: [{ messageId: 'unnecessaryPasswordOption', data: { fieldName: 'clientId' } }],
92
+ },
93
+ {
94
+ name: 'multiple non-sensitive masked fields are each flagged',
95
+ code: createCredentialCode([
96
+ createProperty('Region', 'region', { password: true }),
97
+ createProperty('Host', 'host', { password: true }),
98
+ ]),
99
+ errors: [
100
+ { messageId: 'unnecessaryPasswordOption', data: { fieldName: 'region' } },
101
+ { messageId: 'unnecessaryPasswordOption', data: { fieldName: 'host' } },
102
+ ],
103
+ },
104
+ ],
105
+ });
@@ -0,0 +1,72 @@
1
+ import { TSESTree } from '@typescript-eslint/utils';
2
+
3
+ import {
4
+ isCredentialTypeClass,
5
+ findClassProperty,
6
+ findObjectProperty,
7
+ getStringLiteralValue,
8
+ isSensitiveName,
9
+ hasPasswordTypeOption,
10
+ createRule,
11
+ } from '../utils/index.js';
12
+
13
+ export const CredentialUnnecessaryPasswordRule = createRule({
14
+ name: 'credential-unnecessary-password',
15
+ meta: {
16
+ type: 'suggestion',
17
+ docs: {
18
+ description:
19
+ 'Warn when a credential field with no sensitive name uses typeOptions.password = true',
20
+ },
21
+ messages: {
22
+ unnecessaryPasswordOption:
23
+ "Field '{{ fieldName }}' does not appear to be sensitive but uses 'typeOptions: { password: true }'. Remove it unless the field holds a secret",
24
+ },
25
+ schema: [],
26
+ },
27
+ defaultOptions: [],
28
+ create(context) {
29
+ return {
30
+ ClassDeclaration(node) {
31
+ if (!isCredentialTypeClass(node)) {
32
+ return;
33
+ }
34
+
35
+ const propertiesProperty = findClassProperty(node, 'properties');
36
+ if (
37
+ !propertiesProperty?.value ||
38
+ propertiesProperty.value.type !== TSESTree.AST_NODE_TYPES.ArrayExpression
39
+ ) {
40
+ return;
41
+ }
42
+
43
+ for (const element of propertiesProperty.value.elements) {
44
+ if (element?.type !== TSESTree.AST_NODE_TYPES.ObjectExpression) {
45
+ continue;
46
+ }
47
+
48
+ const nameProperty = findObjectProperty(element, 'name');
49
+ const fieldName = nameProperty ? getStringLiteralValue(nameProperty.value) : null;
50
+
51
+ if (!fieldName || !hasPasswordTypeOption(element)) {
52
+ continue;
53
+ }
54
+
55
+ // Only flag a masked field when its name carries no sensitive marker at all.
56
+ // We check with `exclusions: []` (raw patterns, no URL/ID/pub exclusions) so a
57
+ // real secret like `androidToken` — non-sensitive under the default exclusions
58
+ // only because it contains the substring `id` — is not wrongly flagged.
59
+ // Reported without a fix: removing masking could still be wrong for an
60
+ // unlisted secret name, so the author confirms.
61
+ if (!isSensitiveName(fieldName, { exclusions: [] })) {
62
+ context.report({
63
+ node: element,
64
+ messageId: 'unnecessaryPasswordOption',
65
+ data: { fieldName },
66
+ });
67
+ }
68
+ }
69
+ },
70
+ };
71
+ },
72
+ });
@@ -6,6 +6,7 @@ import { CredClassOAuth2NamingRule } from './cred-class-oauth2-naming.js';
6
6
  import { CredentialDocumentationUrlRule } from './credential-documentation-url.js';
7
7
  import { CredentialPasswordFieldRule } from './credential-password-field.js';
8
8
  import { CredentialTestRequiredRule } from './credential-test-required.js';
9
+ import { CredentialUnnecessaryPasswordRule } from './credential-unnecessary-password.js';
9
10
  import { IconPreferThemedVariantsRule } from './icon-prefer-themed-variants.js';
10
11
  import { IconValidationRule } from './icon-validation.js';
11
12
  import { MissingPairedItemRule } from './missing-paired-item.js';
@@ -13,20 +14,25 @@ import { N8nObjectValidationRule } from './n8n-object-validation.js';
13
14
  import { NoBuilderHintLeakageRule } from './no-builder-hint-leakage.js';
14
15
  import { NoCredentialReuseRule } from './no-credential-reuse.js';
15
16
  import { NoDangerousFunctionsRule } from './no-dangerous-functions.js';
17
+ import { NoDeadFilesRule } from './no-dead-files.js';
16
18
  import { NoDeprecatedWorkflowFunctionsRule } from './no-deprecated-workflow-functions.js';
17
19
  import { NoEmojiInOptionsRule } from './no-emoji-in-options.js';
18
20
  import { NoForbiddenLifecycleScriptsRule } from './no-forbidden-lifecycle-scripts.js';
21
+ import { NoHardcodedSecretsRule } from './no-hardcoded-secrets.js';
19
22
  import { NoHttpRequestWithManualAuthRule } from './no-http-request-with-manual-auth.js';
20
23
  import { NoOverridesFieldRule } from './no-overrides-field.js';
21
24
  import { NoRestrictedGlobalsRule } from './no-restricted-globals.js';
22
25
  import { NoRestrictedImportsRule } from './no-restricted-imports.js';
23
26
  import { NoRuntimeDependenciesRule } from './no-runtime-dependencies.js';
27
+ import { NoSilentErrorSwallowingRule } from './no-silent-error-swallowing.js';
24
28
  import { NoTemplatePlaceholdersRule } from './no-template-placeholders.js';
25
29
  import { NodeClassDescriptionIconMissingRule } from './node-class-description-icon-missing.js';
26
30
  import { NodeConnectionTypeLiteralRule } from './node-connection-type-literal.js';
27
31
  import { NodeRegistrationCompleteRule } from './node-registration-complete.js';
28
32
  import { NodeUsableAsToolRule } from './node-usable-as-tool.js';
29
33
  import { PackageNameConventionRule } from './package-name-convention.js';
34
+ import { RequireFilesArrayRule } from './require-files-array.js';
35
+ import { RequireHomepageRule } from './require-homepage.js';
30
36
  import { RequireNodeApiErrorRule } from './require-node-api-error.js';
31
37
  import { RequireNodeDescriptionFieldsRule } from './require-node-description-fields.js';
32
38
  import { RequireVersionRule } from './require-version.js';
@@ -48,12 +54,16 @@ export const rules = {
48
54
  'node-usable-as-tool': NodeUsableAsToolRule,
49
55
  'package-name-convention': PackageNameConventionRule,
50
56
  'credential-test-required': CredentialTestRequiredRule,
57
+ 'credential-unnecessary-password': CredentialUnnecessaryPasswordRule,
51
58
  'no-credential-reuse': NoCredentialReuseRule,
52
59
  'no-dangerous-functions': NoDangerousFunctionsRule,
60
+ 'no-dead-files': NoDeadFilesRule,
53
61
  'no-forbidden-lifecycle-scripts': NoForbiddenLifecycleScriptsRule,
62
+ 'no-hardcoded-secrets': NoHardcodedSecretsRule,
54
63
  'no-http-request-with-manual-auth': NoHttpRequestWithManualAuthRule,
55
64
  'no-overrides-field': NoOverridesFieldRule,
56
65
  'no-runtime-dependencies': NoRuntimeDependenciesRule,
66
+ 'no-silent-error-swallowing': NoSilentErrorSwallowingRule,
57
67
  'no-template-placeholders': NoTemplatePlaceholdersRule,
58
68
  'icon-validation': IconValidationRule,
59
69
  'icon-prefer-themed-variants': IconPreferThemedVariantsRule,
@@ -68,6 +78,8 @@ export const rules = {
68
78
  'missing-paired-item': MissingPairedItemRule,
69
79
  'no-builder-hint-leakage': NoBuilderHintLeakageRule,
70
80
  'n8n-object-validation': N8nObjectValidationRule,
81
+ 'require-files-array': RequireFilesArrayRule,
82
+ 'require-homepage': RequireHomepageRule,
71
83
  'require-node-api-error': RequireNodeApiErrorRule,
72
84
  'require-node-description-fields': RequireNodeDescriptionFieldsRule,
73
85
  'require-version': RequireVersionRule,
@@ -0,0 +1,128 @@
1
+ import { RuleTester } from '@typescript-eslint/rule-tester';
2
+ import { afterEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import { isDeadFileName, NoDeadFilesRule } from './no-dead-files.js';
5
+ import * as fileUtils from '../utils/file-utils.js';
6
+
7
+ vi.mock('../utils/file-utils.js', async () => {
8
+ const actual = await vi.importActual('../utils/file-utils.js');
9
+ return {
10
+ ...actual,
11
+ findFilesInPackageDirs: vi.fn(),
12
+ };
13
+ });
14
+
15
+ const mockFindFilesInPackageDirs = vi.mocked(fileUtils.findFilesInPackageDirs);
16
+
17
+ const packageJsonPath = '/tmp/package.json';
18
+
19
+ const ruleTester = new RuleTester();
20
+
21
+ function setup(deadFiles: string[]): void {
22
+ mockFindFilesInPackageDirs.mockReturnValue(deadFiles);
23
+ }
24
+
25
+ afterEach(() => {
26
+ vi.clearAllMocks();
27
+ });
28
+
29
+ // Default: no dead files on disk.
30
+ setup([]);
31
+
32
+ ruleTester.run('no-dead-files', NoDeadFilesRule, {
33
+ valid: [
34
+ {
35
+ name: 'package with no dead files',
36
+ filename: packageJsonPath,
37
+ code: '{ "name": "n8n-nodes-example", "n8n": { "nodes": ["dist/nodes/Foo/Foo.node.js"] } }',
38
+ },
39
+ {
40
+ name: 'non-package.json file is ignored',
41
+ filename: 'some-config.json',
42
+ code: '{ "name": "n8n-nodes-example" }',
43
+ before() {
44
+ setup(['/tmp/nodes/Foo/Foo.node.ts.bak']);
45
+ },
46
+ },
47
+ ],
48
+ invalid: [
49
+ {
50
+ name: 'single backup file is reported',
51
+ filename: packageJsonPath,
52
+ code: '{ "name": "n8n-nodes-example" }',
53
+ before() {
54
+ setup(['/tmp/nodes/Foo/Foo.node.ts.bak']);
55
+ },
56
+ errors: [
57
+ {
58
+ messageId: 'deadFileFound',
59
+ data: { file: 'nodes/Foo/Foo.node.ts.bak' },
60
+ },
61
+ ],
62
+ },
63
+ {
64
+ name: 'multiple dead files are each reported',
65
+ filename: packageJsonPath,
66
+ code: '{ "name": "n8n-nodes-example" }',
67
+ before() {
68
+ setup([
69
+ '/tmp/nodes/Foo/Foo.node.ts.backup',
70
+ '/tmp/nodes/Foo/test.js',
71
+ '/tmp/nodes/Bar/Bar.node.ts:Zone.Identifier',
72
+ ]);
73
+ },
74
+ errors: [
75
+ { messageId: 'deadFileFound', data: { file: 'nodes/Foo/Foo.node.ts.backup' } },
76
+ { messageId: 'deadFileFound', data: { file: 'nodes/Foo/test.js' } },
77
+ { messageId: 'deadFileFound', data: { file: 'nodes/Bar/Bar.node.ts:Zone.Identifier' } },
78
+ ],
79
+ },
80
+ {
81
+ name: 'stray test.ts file is reported',
82
+ filename: packageJsonPath,
83
+ code: '{ "name": "n8n-nodes-example" }',
84
+ before() {
85
+ setup(['/tmp/nodes/test.ts']);
86
+ },
87
+ errors: [{ messageId: 'deadFileFound', data: { file: 'nodes/test.ts' } }],
88
+ },
89
+ {
90
+ name: 'dead file in credentials directory is reported',
91
+ filename: packageJsonPath,
92
+ code: '{ "name": "n8n-nodes-example" }',
93
+ before() {
94
+ setup(['/tmp/credentials/MyApi.credentials.ts.bak']);
95
+ },
96
+ errors: [
97
+ { messageId: 'deadFileFound', data: { file: 'credentials/MyApi.credentials.ts.bak' } },
98
+ ],
99
+ },
100
+ ],
101
+ });
102
+
103
+ describe('isDeadFileName', () => {
104
+ it.each([
105
+ 'Foo.node.ts.backup',
106
+ 'notes.backup',
107
+ 'Foo.node.ts.bak',
108
+ 'archive.bak',
109
+ 'Zone.Identifier',
110
+ 'Foo.node.ts:Zone.Identifier',
111
+ 'test.js',
112
+ 'test.ts',
113
+ ])('flags %s as a dead file', (fileName) => {
114
+ expect(isDeadFileName(fileName)).toBe(true);
115
+ });
116
+
117
+ it.each([
118
+ 'Foo.node.ts',
119
+ 'Foo.node.js',
120
+ 'index.ts',
121
+ 'test.json',
122
+ 'mytest.js',
123
+ 'backup.ts',
124
+ 'GenericFunctions.ts',
125
+ ])('does not flag %s', (fileName) => {
126
+ expect(isDeadFileName(fileName)).toBe(false);
127
+ });
128
+ });
@@ -0,0 +1,71 @@
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 { createRule, findFilesInPackageDirs } from '../utils/index.js';
6
+
7
+ // Package subdirectories that ship in the published artifact and should stay clean.
8
+ const SCAN_DIRS = ['nodes', 'credentials'];
9
+
10
+ const DEAD_FILE_MATCHERS: Array<(fileName: string) => boolean> = [
11
+ (fileName) => fileName.endsWith('.backup'),
12
+ (fileName) => fileName.endsWith('.bak'),
13
+ // Windows "mark of the web" ADS files, extracted as literal filenames on
14
+ // filesystems without alternate data streams (e.g. `Foo.node.ts:Zone.Identifier`).
15
+ (fileName) => fileName.endsWith('Zone.Identifier'),
16
+ (fileName) => fileName === 'test.js',
17
+ (fileName) => fileName === 'test.ts',
18
+ ];
19
+
20
+ /**
21
+ * Returns true if the given filename looks like a leftover backup, editor, or
22
+ * test-artifact file that should not ship with a community node.
23
+ */
24
+ export function isDeadFileName(fileName: string): boolean {
25
+ return DEAD_FILE_MATCHERS.some((matches) => matches(fileName));
26
+ }
27
+
28
+ export const NoDeadFilesRule = createRule({
29
+ name: 'no-dead-files',
30
+ meta: {
31
+ type: 'problem',
32
+ docs: {
33
+ description:
34
+ 'Disallow leftover backup, editor, and test-artifact files in the `nodes/` and `credentials/` directories of a community node package',
35
+ },
36
+ messages: {
37
+ deadFileFound:
38
+ 'Remove the leftover file "{{ file }}". Backup, editor, and test-artifact files should not be published with a community node.',
39
+ },
40
+ schema: [],
41
+ },
42
+ defaultOptions: [],
43
+ create(context) {
44
+ if (!context.filename.endsWith('package.json')) {
45
+ return {};
46
+ }
47
+
48
+ return {
49
+ ObjectExpression(node: TSESTree.ObjectExpression) {
50
+ // Only inspect the root object of the package.json file, so the scan runs once.
51
+ if (node.parent?.type !== AST_NODE_TYPES.ExpressionStatement) {
52
+ return;
53
+ }
54
+
55
+ const packageDir = path.dirname(context.filename);
56
+
57
+ for (const deadFile of findFilesInPackageDirs(
58
+ context.filename,
59
+ SCAN_DIRS,
60
+ isDeadFileName,
61
+ )) {
62
+ context.report({
63
+ node,
64
+ messageId: 'deadFileFound',
65
+ data: { file: path.relative(packageDir, deadFile) },
66
+ });
67
+ }
68
+ },
69
+ };
70
+ },
71
+ });