@n8n/eslint-plugin-community-nodes 0.10.0 → 0.12.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 (50) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/README.md +22 -17
  3. package/dist/plugin.d.ts +30 -0
  4. package/dist/plugin.d.ts.map +1 -1
  5. package/dist/plugin.js +10 -0
  6. package/dist/plugin.js.map +1 -1
  7. package/dist/rules/index.d.ts +5 -0
  8. package/dist/rules/index.d.ts.map +1 -1
  9. package/dist/rules/index.js +10 -0
  10. package/dist/rules/index.js.map +1 -1
  11. package/dist/rules/missing-paired-item.d.ts +16 -0
  12. package/dist/rules/missing-paired-item.d.ts.map +1 -0
  13. package/dist/rules/missing-paired-item.js +121 -0
  14. package/dist/rules/missing-paired-item.js.map +1 -0
  15. package/dist/rules/no-forbidden-lifecycle-scripts.d.ts +2 -0
  16. package/dist/rules/no-forbidden-lifecycle-scripts.d.ts.map +1 -0
  17. package/dist/rules/no-forbidden-lifecycle-scripts.js +59 -0
  18. package/dist/rules/no-forbidden-lifecycle-scripts.js.map +1 -0
  19. package/dist/rules/node-connection-type-literal.d.ts +2 -0
  20. package/dist/rules/node-connection-type-literal.d.ts.map +1 -0
  21. package/dist/rules/node-connection-type-literal.js +77 -0
  22. package/dist/rules/node-connection-type-literal.js.map +1 -0
  23. package/dist/rules/options-sorted-alphabetically.d.ts +2 -0
  24. package/dist/rules/options-sorted-alphabetically.d.ts.map +1 -0
  25. package/dist/rules/options-sorted-alphabetically.js +95 -0
  26. package/dist/rules/options-sorted-alphabetically.js.map +1 -0
  27. package/dist/rules/require-continue-on-fail.d.ts +2 -0
  28. package/dist/rules/require-continue-on-fail.d.ts.map +1 -0
  29. package/dist/rules/require-continue-on-fail.js +76 -0
  30. package/dist/rules/require-continue-on-fail.js.map +1 -0
  31. package/docs/rules/missing-paired-item.md +70 -0
  32. package/docs/rules/no-forbidden-lifecycle-scripts.md +46 -0
  33. package/docs/rules/node-connection-type-literal.md +46 -0
  34. package/docs/rules/options-sorted-alphabetically.md +63 -0
  35. package/docs/rules/require-continue-on-fail.md +56 -0
  36. package/package.json +9 -6
  37. package/src/plugin.ts +10 -0
  38. package/src/rules/index.ts +10 -0
  39. package/src/rules/missing-paired-item.test.ts +229 -0
  40. package/src/rules/missing-paired-item.ts +149 -0
  41. package/src/rules/no-forbidden-lifecycle-scripts.test.ts +103 -0
  42. package/src/rules/no-forbidden-lifecycle-scripts.ts +69 -0
  43. package/src/rules/node-connection-type-literal.test.ts +128 -0
  44. package/src/rules/node-connection-type-literal.ts +98 -0
  45. package/src/rules/options-sorted-alphabetically.test.ts +468 -0
  46. package/src/rules/options-sorted-alphabetically.ts +129 -0
  47. package/src/rules/require-continue-on-fail.test.ts +129 -0
  48. package/src/rules/require-continue-on-fail.ts +88 -0
  49. package/tsconfig.build.tsbuildinfo +1 -0
  50. package/tsconfig.json +1 -1
@@ -6,14 +6,19 @@ import { CredentialDocumentationUrlRule } from './credential-documentation-url.j
6
6
  import { CredentialPasswordFieldRule } from './credential-password-field.js';
7
7
  import { CredentialTestRequiredRule } from './credential-test-required.js';
8
8
  import { IconValidationRule } from './icon-validation.js';
9
+ import { MissingPairedItemRule } from './missing-paired-item.js';
9
10
  import { NoCredentialReuseRule } from './no-credential-reuse.js';
10
11
  import { NoDeprecatedWorkflowFunctionsRule } from './no-deprecated-workflow-functions.js';
12
+ import { NoForbiddenLifecycleScriptsRule } from './no-forbidden-lifecycle-scripts.js';
11
13
  import { NoHttpRequestWithManualAuthRule } from './no-http-request-with-manual-auth.js';
12
14
  import { NoRestrictedGlobalsRule } from './no-restricted-globals.js';
13
15
  import { NoRestrictedImportsRule } from './no-restricted-imports.js';
14
16
  import { NodeClassDescriptionIconMissingRule } from './node-class-description-icon-missing.js';
17
+ import { NodeConnectionTypeLiteralRule } from './node-connection-type-literal.js';
15
18
  import { NodeUsableAsToolRule } from './node-usable-as-tool.js';
19
+ import { OptionsSortedAlphabeticallyRule } from './options-sorted-alphabetically.js';
16
20
  import { PackageNameConventionRule } from './package-name-convention.js';
21
+ import { RequireContinueOnFailRule } from './require-continue-on-fail.js';
17
22
  import { ResourceOperationPatternRule } from './resource-operation-pattern.js';
18
23
 
19
24
  export const rules = {
@@ -23,13 +28,18 @@ export const rules = {
23
28
  'credential-password-field': CredentialPasswordFieldRule,
24
29
  'no-deprecated-workflow-functions': NoDeprecatedWorkflowFunctionsRule,
25
30
  'node-usable-as-tool': NodeUsableAsToolRule,
31
+ 'options-sorted-alphabetically': OptionsSortedAlphabeticallyRule,
26
32
  'package-name-convention': PackageNameConventionRule,
27
33
  'credential-test-required': CredentialTestRequiredRule,
28
34
  'no-credential-reuse': NoCredentialReuseRule,
35
+ 'no-forbidden-lifecycle-scripts': NoForbiddenLifecycleScriptsRule,
29
36
  'no-http-request-with-manual-auth': NoHttpRequestWithManualAuthRule,
30
37
  'icon-validation': IconValidationRule,
31
38
  'resource-operation-pattern': ResourceOperationPatternRule,
32
39
  'credential-documentation-url': CredentialDocumentationUrlRule,
33
40
  'node-class-description-icon-missing': NodeClassDescriptionIconMissingRule,
34
41
  'cred-class-field-icon-missing': CredClassFieldIconMissingRule,
42
+ 'node-connection-type-literal': NodeConnectionTypeLiteralRule,
43
+ 'missing-paired-item': MissingPairedItemRule,
44
+ 'require-continue-on-fail': RequireContinueOnFailRule,
35
45
  } satisfies Record<string, AnyRuleModule>;
@@ -0,0 +1,229 @@
1
+ import { RuleTester } from '@typescript-eslint/rule-tester';
2
+
3
+ import { MissingPairedItemRule } from './missing-paired-item.js';
4
+
5
+ const ruleTester = new RuleTester();
6
+
7
+ ruleTester.run('missing-paired-item', MissingPairedItemRule, {
8
+ valid: [
9
+ {
10
+ name: 'object with json and pairedItem in execute()',
11
+ filename: 'MyNode.node.ts',
12
+ code: `
13
+ class MyNode implements INodeType {
14
+ description = {};
15
+ async execute() {
16
+ return [[{ json: { id: 1 }, pairedItem: { item: 0 } }]];
17
+ }
18
+ }`,
19
+ },
20
+ {
21
+ name: 'object with json and pairedItem as number shorthand',
22
+ filename: 'MyNode.node.ts',
23
+ code: `
24
+ class MyNode implements INodeType {
25
+ description = {};
26
+ async execute() {
27
+ return [[{ json: { id: 1 }, pairedItem: 0 }]];
28
+ }
29
+ }`,
30
+ },
31
+ {
32
+ name: 'non-node-type class is ignored',
33
+ filename: 'MyNode.node.ts',
34
+ code: `
35
+ class Helper {
36
+ async execute() {
37
+ return [[{ json: { id: 1 } }]];
38
+ }
39
+ }`,
40
+ },
41
+ {
42
+ name: 'non-.node.ts file is ignored',
43
+ filename: 'utils.ts',
44
+ code: `
45
+ class MyNode implements INodeType {
46
+ description = {};
47
+ async execute() {
48
+ return [[{ json: { id: 1 } }]];
49
+ }
50
+ }`,
51
+ },
52
+ {
53
+ name: 'object outside execute() is ignored',
54
+ filename: 'MyNode.node.ts',
55
+ code: `
56
+ class MyNode implements INodeType {
57
+ description = {};
58
+ async someHelper() {
59
+ return { json: { id: 1 } };
60
+ }
61
+ }`,
62
+ },
63
+ {
64
+ name: 'object without json property is ignored',
65
+ filename: 'MyNode.node.ts',
66
+ code: `
67
+ class MyNode implements INodeType {
68
+ description = {};
69
+ async execute() {
70
+ const options = { url: 'https://api.example.com', method: 'GET' };
71
+ return [[{ json: options, pairedItem: { item: 0 } }]];
72
+ }
73
+ }`,
74
+ },
75
+ {
76
+ name: 'object with unrecognized keys is not INodeExecutionData',
77
+ filename: 'MyNode.node.ts',
78
+ code: `
79
+ class MyNode implements INodeType {
80
+ description = {};
81
+ async execute() {
82
+ const config = { json: true, indent: 2, spaces: 4 };
83
+ return this.process(config);
84
+ }
85
+ }`,
86
+ },
87
+ {
88
+ name: 'class extending Node base class with pairedItem',
89
+ filename: 'MyNode.node.ts',
90
+ code: `
91
+ class MyNode extends Node {
92
+ description = {};
93
+ async execute() {
94
+ return [[{ json: { id: 1 }, pairedItem: { item: 0 } }]];
95
+ }
96
+ }`,
97
+ },
98
+ {
99
+ name: 'spread element in object is allowed',
100
+ filename: 'MyNode.node.ts',
101
+ code: `
102
+ class MyNode implements INodeType {
103
+ description = {};
104
+ async execute() {
105
+ return [[{ ...baseItem, json: { id: 1 } }]];
106
+ }
107
+ }`,
108
+ },
109
+ {
110
+ name: 'object inside constructExecutionMetaData is allowed',
111
+ filename: 'MyNode.node.ts',
112
+ code: `
113
+ class MyNode implements INodeType {
114
+ description = {};
115
+ async execute() {
116
+ const executionData = this.helpers.constructExecutionMetaData(
117
+ [{ json: { success: true } }],
118
+ { itemData: { item: i } },
119
+ );
120
+ return [executionData];
121
+ }
122
+ }`,
123
+ },
124
+ {
125
+ name: 'object with computed property key is not INodeExecutionData',
126
+ filename: 'MyNode.node.ts',
127
+ code: `
128
+ class MyNode implements INodeType {
129
+ description = {};
130
+ async execute() {
131
+ const obj = { json: data, [dynamicKey]: value };
132
+ return [[obj]];
133
+ }
134
+ }`,
135
+ },
136
+ ],
137
+ invalid: [
138
+ {
139
+ name: 'missing pairedItem in return statement',
140
+ filename: 'MyNode.node.ts',
141
+ code: `
142
+ class MyNode implements INodeType {
143
+ description = {};
144
+ async execute() {
145
+ return [[{ json: { id: 1 } }]];
146
+ }
147
+ }`,
148
+ errors: [{ messageId: 'missingPairedItem' }],
149
+ },
150
+ {
151
+ name: 'missing pairedItem in .map() callback',
152
+ filename: 'MyNode.node.ts',
153
+ code: `
154
+ class MyNode implements INodeType {
155
+ description = {};
156
+ async execute() {
157
+ const items = this.getInputData();
158
+ return [items.map((item, index) => ({ json: item.json }))];
159
+ }
160
+ }`,
161
+ errors: [{ messageId: 'missingPairedItem' }],
162
+ },
163
+ {
164
+ name: 'missing pairedItem in .push() call',
165
+ filename: 'MyNode.node.ts',
166
+ code: `
167
+ class MyNode implements INodeType {
168
+ description = {};
169
+ async execute() {
170
+ const returnData = [];
171
+ returnData.push({ json: { result: true } });
172
+ return [returnData];
173
+ }
174
+ }`,
175
+ errors: [{ messageId: 'missingPairedItem' }],
176
+ },
177
+ {
178
+ name: 'multiple objects missing pairedItem',
179
+ filename: 'MyNode.node.ts',
180
+ code: `
181
+ class MyNode implements INodeType {
182
+ description = {};
183
+ async execute() {
184
+ const returnData = [];
185
+ returnData.push({ json: { a: 1 } });
186
+ returnData.push({ json: { b: 2 } });
187
+ return [returnData];
188
+ }
189
+ }`,
190
+ errors: [{ messageId: 'missingPairedItem' }, { messageId: 'missingPairedItem' }],
191
+ },
192
+ {
193
+ name: 'object with json and binary but no pairedItem',
194
+ filename: 'MyNode.node.ts',
195
+ code: `
196
+ class MyNode implements INodeType {
197
+ description = {};
198
+ async execute() {
199
+ return [[{ json: { id: 1 }, binary: { data: binaryData } }]];
200
+ }
201
+ }`,
202
+ errors: [{ messageId: 'missingPairedItem' }],
203
+ },
204
+ {
205
+ name: 'object with json and executionStatus but no pairedItem',
206
+ filename: 'MyNode.node.ts',
207
+ code: `
208
+ class MyNode implements INodeType {
209
+ description = {};
210
+ async execute() {
211
+ return [[{ json: { id: 1 }, executionStatus: 'success' }]];
212
+ }
213
+ }`,
214
+ errors: [{ messageId: 'missingPairedItem' }],
215
+ },
216
+ {
217
+ name: 'class extending Node base class missing pairedItem',
218
+ filename: 'MyNode.node.ts',
219
+ code: `
220
+ class MyNode extends Node {
221
+ description = {};
222
+ async execute() {
223
+ return [[{ json: { id: 1 } }]];
224
+ }
225
+ }`,
226
+ errors: [{ messageId: 'missingPairedItem' }],
227
+ },
228
+ ],
229
+ });
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Flags object literals with a `json` property but no `pairedItem` property
3
+ * inside `execute()` methods of `INodeType` classes.
4
+ *
5
+ * Missing `pairedItem` breaks downstream item-referencing expressions like
6
+ * `$('NodeName').item`. This rule catches the three most common patterns:
7
+ *
8
+ * - Object literals in `.map()` callbacks
9
+ * - Object literals passed to `.push()` calls
10
+ * - Object literals in return statements (typically `return [[{ json }]]`)
11
+ *
12
+ * Only flags object literals directly — variable references are skipped since
13
+ * their shape cannot be determined without type resolution.
14
+ */
15
+
16
+ import type { TSESTree } from '@typescript-eslint/utils';
17
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
18
+
19
+ import {
20
+ createRule,
21
+ findObjectProperty,
22
+ isFileType,
23
+ isNodeTypeClass,
24
+ isThisHelpersMethodCall,
25
+ } from '../utils/index.js';
26
+
27
+ /**
28
+ * Checks whether the object is inside an array argument to
29
+ * `this.helpers.constructExecutionMetaData()`, which adds pairedItem
30
+ * via the second argument's `itemData` property.
31
+ */
32
+ function isInsideConstructExecutionMetaData(node: TSESTree.ObjectExpression): boolean {
33
+ // Pattern: constructExecutionMetaData([{ json: ... }], { itemData: ... })
34
+ // Walk up: ObjectExpression -> ArrayExpression -> CallExpression
35
+ const parent = node.parent;
36
+ if (parent?.type !== AST_NODE_TYPES.ArrayExpression) return false;
37
+
38
+ const grandparent = parent.parent;
39
+ if (grandparent?.type !== AST_NODE_TYPES.CallExpression) return false;
40
+
41
+ // Check it's the first argument
42
+ if (grandparent.arguments[0] !== parent) return false;
43
+
44
+ return isThisHelpersMethodCall(grandparent, 'constructExecutionMetaData');
45
+ }
46
+
47
+ export const MissingPairedItemRule = createRule({
48
+ name: 'missing-paired-item',
49
+ meta: {
50
+ type: 'problem',
51
+ docs: {
52
+ description:
53
+ 'Require pairedItem on INodeExecutionData objects in execute() methods to preserve item linking.',
54
+ },
55
+ messages: {
56
+ missingPairedItem:
57
+ 'Missing pairedItem on INodeExecutionData object. Add `pairedItem: { item: index }` to preserve item linking. See https://docs.n8n.io/integrations/creating-nodes/build/reference/paired-items/',
58
+ },
59
+ schema: [],
60
+ hasSuggestions: false,
61
+ },
62
+ defaultOptions: [],
63
+ create(context) {
64
+ if (!isFileType(context.filename, '.node.ts')) {
65
+ return {};
66
+ }
67
+
68
+ let inNodeTypeClass = false;
69
+ let inExecuteMethod = false;
70
+
71
+ return {
72
+ ClassDeclaration(node) {
73
+ if (isNodeTypeClass(node)) {
74
+ inNodeTypeClass = true;
75
+ }
76
+ },
77
+
78
+ 'ClassDeclaration:exit'() {
79
+ inNodeTypeClass = false;
80
+ inExecuteMethod = false;
81
+ },
82
+
83
+ MethodDefinition(node: TSESTree.MethodDefinition) {
84
+ if (
85
+ inNodeTypeClass &&
86
+ node.key.type === AST_NODE_TYPES.Identifier &&
87
+ node.key.name === 'execute'
88
+ ) {
89
+ inExecuteMethod = true;
90
+ }
91
+ },
92
+
93
+ 'MethodDefinition:exit'(node: TSESTree.MethodDefinition) {
94
+ if (
95
+ inExecuteMethod &&
96
+ node.key.type === AST_NODE_TYPES.Identifier &&
97
+ node.key.name === 'execute'
98
+ ) {
99
+ inExecuteMethod = false;
100
+ }
101
+ },
102
+
103
+ ObjectExpression(node: TSESTree.ObjectExpression) {
104
+ if (!inExecuteMethod) return;
105
+
106
+ const hasJson = findObjectProperty(node, 'json') !== null;
107
+ if (!hasJson) return;
108
+
109
+ const hasPairedItem = findObjectProperty(node, 'pairedItem') !== null;
110
+ if (hasPairedItem) return;
111
+
112
+ // Skip if inside constructExecutionMetaData() — it adds pairedItem via itemData
113
+ if (isInsideConstructExecutionMetaData(node)) return;
114
+
115
+ // Skip if the object contains spread elements — they may already provide pairedItem
116
+ const hasSpread = node.properties.some(
117
+ (prop) => prop.type === AST_NODE_TYPES.SpreadElement,
118
+ );
119
+ if (hasSpread) return;
120
+
121
+ // Only flag if this looks like an INodeExecutionData object literal —
122
+ // must have `json` and optionally `binary`/`error`, nothing unexpected.
123
+ // Objects with many unrelated keys are likely not INodeExecutionData.
124
+ const knownKeys = new Set([
125
+ 'json',
126
+ 'binary',
127
+ 'error',
128
+ 'pairedItem',
129
+ 'executionStatus',
130
+ 'metadata',
131
+ 'evaluationData',
132
+ 'redaction',
133
+ 'sendMessage',
134
+ 'index',
135
+ ]);
136
+ const allPropertiesKnown = node.properties.every(
137
+ (prop) =>
138
+ prop.type === AST_NODE_TYPES.Property &&
139
+ prop.key.type === AST_NODE_TYPES.Identifier &&
140
+ knownKeys.has(prop.key.name),
141
+ );
142
+
143
+ if (!allPropertiesKnown) return;
144
+
145
+ context.report({ node, messageId: 'missingPairedItem' });
146
+ },
147
+ };
148
+ },
149
+ });
@@ -0,0 +1,103 @@
1
+ import { RuleTester } from '@typescript-eslint/rule-tester';
2
+
3
+ import { NoForbiddenLifecycleScriptsRule } from './no-forbidden-lifecycle-scripts.js';
4
+
5
+ const ruleTester = new RuleTester();
6
+
7
+ ruleTester.run('no-forbidden-lifecycle-scripts', NoForbiddenLifecycleScriptsRule, {
8
+ valid: [
9
+ {
10
+ name: 'no scripts field',
11
+ filename: 'package.json',
12
+ code: '{ "name": "n8n-nodes-example", "version": "1.0.0" }',
13
+ },
14
+ {
15
+ name: 'only safe scripts',
16
+ filename: 'package.json',
17
+ code: '{ "name": "n8n-nodes-example", "scripts": { "build": "tsc", "test": "jest", "dev": "nodemon" } }',
18
+ },
19
+ {
20
+ name: 'non-package.json file is ignored',
21
+ filename: 'some-config.json',
22
+ code: '{ "scripts": { "prepare": "npm run build" } }',
23
+ },
24
+ {
25
+ name: 'empty scripts object',
26
+ filename: 'package.json',
27
+ code: '{ "name": "n8n-nodes-example", "scripts": {} }',
28
+ },
29
+ ],
30
+ invalid: [
31
+ {
32
+ name: 'prepare script is forbidden',
33
+ filename: 'package.json',
34
+ code: '{ "name": "n8n-nodes-example", "scripts": { "prepare": "npm run build" } }',
35
+ errors: [{ messageId: 'forbiddenScript', data: { scriptName: 'prepare' } }],
36
+ },
37
+ {
38
+ name: 'preinstall script is forbidden',
39
+ filename: 'package.json',
40
+ code: '{ "name": "n8n-nodes-example", "scripts": { "preinstall": "node setup.js" } }',
41
+ errors: [{ messageId: 'forbiddenScript', data: { scriptName: 'preinstall' } }],
42
+ },
43
+ {
44
+ name: 'install script is forbidden',
45
+ filename: 'package.json',
46
+ code: '{ "name": "n8n-nodes-example", "scripts": { "install": "node install.js" } }',
47
+ errors: [{ messageId: 'forbiddenScript', data: { scriptName: 'install' } }],
48
+ },
49
+ {
50
+ name: 'postinstall script is forbidden',
51
+ filename: 'package.json',
52
+ code: '{ "name": "n8n-nodes-example", "scripts": { "postinstall": "node setup.js" } }',
53
+ errors: [{ messageId: 'forbiddenScript', data: { scriptName: 'postinstall' } }],
54
+ },
55
+ {
56
+ name: 'prepublish script is forbidden',
57
+ filename: 'package.json',
58
+ code: '{ "name": "n8n-nodes-example", "scripts": { "prepublish": "npm run build" } }',
59
+ errors: [{ messageId: 'forbiddenScript', data: { scriptName: 'prepublish' } }],
60
+ },
61
+ {
62
+ name: 'preprepare script is forbidden',
63
+ filename: 'package.json',
64
+ code: '{ "name": "n8n-nodes-example", "scripts": { "preprepare": "echo prep" } }',
65
+ errors: [{ messageId: 'forbiddenScript', data: { scriptName: 'preprepare' } }],
66
+ },
67
+ {
68
+ name: 'postprepare script is forbidden',
69
+ filename: 'package.json',
70
+ code: '{ "name": "n8n-nodes-example", "scripts": { "postprepare": "echo done" } }',
71
+ errors: [{ messageId: 'forbiddenScript', data: { scriptName: 'postprepare' } }],
72
+ },
73
+ {
74
+ name: 'multiple forbidden scripts report separate errors',
75
+ filename: 'package.json',
76
+ code: '{ "name": "n8n-nodes-example", "scripts": { "prepare": "npm run build", "postinstall": "node setup.js" } }',
77
+ errors: [
78
+ { messageId: 'forbiddenScript', data: { scriptName: 'prepare' } },
79
+ { messageId: 'forbiddenScript', data: { scriptName: 'postinstall' } },
80
+ ],
81
+ },
82
+ {
83
+ name: 'mix of allowed and forbidden scripts — only forbidden reported',
84
+ filename: 'package.json',
85
+ code: '{ "name": "n8n-nodes-example", "scripts": { "build": "tsc", "prepare": "npm run build", "test": "jest" } }',
86
+ errors: [{ messageId: 'forbiddenScript', data: { scriptName: 'prepare' } }],
87
+ },
88
+ {
89
+ name: 'all seven forbidden scripts present',
90
+ filename: 'package.json',
91
+ code: '{ "name": "n8n-nodes-example", "scripts": { "prepare": "a", "preinstall": "b", "install": "c", "postinstall": "d", "prepublish": "e", "preprepare": "f", "postprepare": "g" } }',
92
+ errors: [
93
+ { messageId: 'forbiddenScript', data: { scriptName: 'prepare' } },
94
+ { messageId: 'forbiddenScript', data: { scriptName: 'preinstall' } },
95
+ { messageId: 'forbiddenScript', data: { scriptName: 'install' } },
96
+ { messageId: 'forbiddenScript', data: { scriptName: 'postinstall' } },
97
+ { messageId: 'forbiddenScript', data: { scriptName: 'prepublish' } },
98
+ { messageId: 'forbiddenScript', data: { scriptName: 'preprepare' } },
99
+ { messageId: 'forbiddenScript', data: { scriptName: 'postprepare' } },
100
+ ],
101
+ },
102
+ ],
103
+ });
@@ -0,0 +1,69 @@
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
+ const FORBIDDEN_SCRIPTS = [
7
+ 'prepare',
8
+ 'preinstall',
9
+ 'install',
10
+ 'postinstall',
11
+ 'prepublish',
12
+ 'preprepare',
13
+ 'postprepare',
14
+ ];
15
+
16
+ export const NoForbiddenLifecycleScriptsRule = createRule({
17
+ name: 'no-forbidden-lifecycle-scripts',
18
+ meta: {
19
+ type: 'problem',
20
+ docs: {
21
+ description:
22
+ 'Ban lifecycle scripts (prepare, preinstall, postinstall, etc.) in community node packages',
23
+ },
24
+ messages: {
25
+ forbiddenScript:
26
+ 'Lifecycle script "{{ scriptName }}" is not allowed in community node packages. These scripts execute arbitrary code during installation.',
27
+ },
28
+ schema: [],
29
+ },
30
+ defaultOptions: [],
31
+ create(context) {
32
+ if (!context.filename.endsWith('package.json')) {
33
+ return {};
34
+ }
35
+
36
+ return {
37
+ ObjectExpression(node: TSESTree.ObjectExpression) {
38
+ // Only process the root object, not nested ones
39
+ if (node.parent?.type === AST_NODE_TYPES.Property) {
40
+ return;
41
+ }
42
+
43
+ const scriptsProp = findJsonProperty(node, 'scripts');
44
+ if (!scriptsProp || scriptsProp.value.type !== AST_NODE_TYPES.ObjectExpression) {
45
+ return;
46
+ }
47
+
48
+ for (const property of scriptsProp.value.properties) {
49
+ if (property.type !== AST_NODE_TYPES.Property) continue;
50
+
51
+ const key =
52
+ property.key.type === AST_NODE_TYPES.Identifier
53
+ ? property.key.name
54
+ : property.key.type === AST_NODE_TYPES.Literal
55
+ ? String(property.key.value)
56
+ : null;
57
+
58
+ if (key !== null && FORBIDDEN_SCRIPTS.includes(key)) {
59
+ context.report({
60
+ node: property,
61
+ messageId: 'forbiddenScript',
62
+ data: { scriptName: key },
63
+ });
64
+ }
65
+ }
66
+ },
67
+ };
68
+ },
69
+ });