@n8n/eslint-plugin-community-nodes 0.14.0 → 0.16.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 +1 -1
- package/README.md +7 -0
- 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-class-name-suffix.d.ts +2 -0
- package/dist/rules/cred-class-name-suffix.d.ts.map +1 -0
- package/dist/rules/cred-class-name-suffix.js +53 -0
- package/dist/rules/cred-class-name-suffix.js.map +1 -0
- package/dist/rules/cred-class-oauth2-naming.d.ts +2 -0
- package/dist/rules/cred-class-oauth2-naming.d.ts.map +1 -0
- package/dist/rules/cred-class-oauth2-naming.js +96 -0
- package/dist/rules/cred-class-oauth2-naming.js.map +1 -0
- package/dist/rules/index.d.ts +11 -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/n8n-object-validation.d.ts +5 -0
- package/dist/rules/n8n-object-validation.d.ts.map +1 -0
- package/dist/rules/n8n-object-validation.js +148 -0
- package/dist/rules/n8n-object-validation.js.map +1 -0
- package/dist/rules/no-builder-hint-leakage.d.ts +7 -0
- package/dist/rules/no-builder-hint-leakage.d.ts.map +1 -0
- package/dist/rules/no-builder-hint-leakage.js +99 -0
- package/dist/rules/no-builder-hint-leakage.js.map +1 -0
- package/dist/rules/no-overrides-field.js +1 -1
- package/dist/rules/no-overrides-field.js.map +1 -1
- package/dist/rules/no-runtime-dependencies.d.ts +2 -0
- package/dist/rules/no-runtime-dependencies.d.ts.map +1 -0
- package/dist/rules/no-runtime-dependencies.js +41 -0
- package/dist/rules/no-runtime-dependencies.js.map +1 -0
- package/dist/rules/no-template-placeholders.d.ts +2 -0
- package/dist/rules/no-template-placeholders.d.ts.map +1 -0
- package/dist/rules/no-template-placeholders.js +57 -0
- package/dist/rules/no-template-placeholders.js.map +1 -0
- package/dist/rules/node-operation-error-itemindex.d.ts +12 -0
- package/dist/rules/node-operation-error-itemindex.d.ts.map +1 -0
- package/dist/rules/node-operation-error-itemindex.js +184 -0
- package/dist/rules/node-operation-error-itemindex.js.map +1 -0
- package/dist/rules/valid-credential-references.d.ts +2 -0
- package/dist/rules/valid-credential-references.d.ts.map +1 -0
- package/dist/rules/valid-credential-references.js +77 -0
- package/dist/rules/valid-credential-references.js.map +1 -0
- package/dist/rules/webhook-lifecycle-complete.d.ts +1 -1
- package/dist/rules/webhook-lifecycle-complete.d.ts.map +1 -1
- package/dist/rules/webhook-lifecycle-complete.js +8 -0
- package/dist/rules/webhook-lifecycle-complete.js.map +1 -1
- package/dist/utils/ast-utils.d.ts.map +1 -1
- package/dist/utils/ast-utils.js +5 -1
- package/dist/utils/ast-utils.js.map +1 -1
- package/docs/rules/cred-class-name-suffix.md +46 -0
- package/docs/rules/cred-class-oauth2-naming.md +68 -0
- package/docs/rules/n8n-object-validation.md +93 -0
- package/docs/rules/no-overrides-field.md +5 -5
- package/docs/rules/no-runtime-dependencies.md +58 -0
- package/docs/rules/no-template-placeholders.md +51 -0
- package/docs/rules/node-operation-error-itemindex.md +81 -0
- package/docs/rules/valid-credential-references.md +78 -0
- package/package.json +3 -3
- package/src/plugin.ts +16 -0
- package/src/rules/cred-class-name-suffix.test.ts +74 -0
- package/src/rules/cred-class-name-suffix.ts +57 -0
- package/src/rules/cred-class-oauth2-naming.test.ts +197 -0
- package/src/rules/cred-class-oauth2-naming.ts +118 -0
- package/src/rules/index.ts +16 -0
- package/src/rules/n8n-object-validation.test.ts +202 -0
- package/src/rules/n8n-object-validation.ts +200 -0
- package/src/rules/no-builder-hint-leakage.test.ts +84 -0
- package/src/rules/no-builder-hint-leakage.ts +112 -0
- package/src/rules/no-overrides-field.ts +1 -1
- package/src/rules/no-runtime-dependencies.test.ts +50 -0
- package/src/rules/no-runtime-dependencies.ts +50 -0
- package/src/rules/no-template-placeholders.test.ts +135 -0
- package/src/rules/no-template-placeholders.ts +68 -0
- package/src/rules/node-operation-error-itemindex.test.ts +280 -0
- package/src/rules/node-operation-error-itemindex.ts +223 -0
- package/src/rules/valid-credential-references.test.ts +230 -0
- package/src/rules/valid-credential-references.ts +105 -0
- package/src/rules/webhook-lifecycle-complete.test.ts +5 -0
- package/src/rules/webhook-lifecycle-complete.ts +10 -0
- package/src/utils/ast-utils.ts +5 -1
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
import { afterEach, beforeEach, describe, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { ValidCredentialReferencesRule } from './valid-credential-references.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
|
+
readPackageJsonCredentials: vi.fn(),
|
|
12
|
+
findPackageJson: vi.fn(),
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const mockReadPackageJsonCredentials = vi.mocked(fileUtils.readPackageJsonCredentials);
|
|
17
|
+
const mockFindPackageJson = vi.mocked(fileUtils.findPackageJson);
|
|
18
|
+
|
|
19
|
+
const ruleTester = new RuleTester();
|
|
20
|
+
|
|
21
|
+
const nodeFilePath = '/tmp/TestNode.node.ts';
|
|
22
|
+
|
|
23
|
+
function createNodeCode(
|
|
24
|
+
credentials: Array<string | { name: string; required?: boolean }> = [],
|
|
25
|
+
): string {
|
|
26
|
+
const credentialsArray =
|
|
27
|
+
credentials.length > 0
|
|
28
|
+
? credentials
|
|
29
|
+
.map((cred) => {
|
|
30
|
+
if (typeof cred === 'string') {
|
|
31
|
+
return `'${cred}'`;
|
|
32
|
+
} else {
|
|
33
|
+
const required =
|
|
34
|
+
cred.required !== undefined ? `,\n\t\t\t\trequired: ${cred.required}` : '';
|
|
35
|
+
return `{\n\t\t\t\tname: '${cred.name}'${required},\n\t\t\t}`;
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
.join(',\n\t\t\t')
|
|
39
|
+
: '';
|
|
40
|
+
|
|
41
|
+
const credentialsProperty =
|
|
42
|
+
credentials.length > 0 ? `credentials: [\n\t\t\t${credentialsArray}\n\t\t],` : '';
|
|
43
|
+
|
|
44
|
+
return `
|
|
45
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
46
|
+
|
|
47
|
+
export class TestNode implements INodeType {
|
|
48
|
+
description: INodeTypeDescription = {
|
|
49
|
+
displayName: 'Test Node',
|
|
50
|
+
name: 'testNode',
|
|
51
|
+
group: ['output'],
|
|
52
|
+
version: 1,
|
|
53
|
+
inputs: ['main'],
|
|
54
|
+
outputs: ['main'],
|
|
55
|
+
${credentialsProperty}
|
|
56
|
+
properties: [],
|
|
57
|
+
};
|
|
58
|
+
}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Same as createNodeCode but uses double quotes for the credential name — matches fixer output */
|
|
62
|
+
function createExpectedNodeCode(
|
|
63
|
+
credentials: Array<string | { name: string; required?: boolean }> = [],
|
|
64
|
+
): string {
|
|
65
|
+
const credentialsArray =
|
|
66
|
+
credentials.length > 0
|
|
67
|
+
? credentials
|
|
68
|
+
.map((cred) => {
|
|
69
|
+
if (typeof cred === 'string') {
|
|
70
|
+
return `"${cred}"`;
|
|
71
|
+
} else {
|
|
72
|
+
const required =
|
|
73
|
+
cred.required !== undefined ? `,\n\t\t\t\trequired: ${cred.required}` : '';
|
|
74
|
+
return `{\n\t\t\t\tname: "${cred.name}"${required},\n\t\t\t}`;
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
.join(',\n\t\t\t')
|
|
78
|
+
: '';
|
|
79
|
+
|
|
80
|
+
const credentialsProperty =
|
|
81
|
+
credentials.length > 0 ? `credentials: [\n\t\t\t${credentialsArray}\n\t\t],` : '';
|
|
82
|
+
|
|
83
|
+
return `
|
|
84
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
85
|
+
|
|
86
|
+
export class TestNode implements INodeType {
|
|
87
|
+
description: INodeTypeDescription = {
|
|
88
|
+
displayName: 'Test Node',
|
|
89
|
+
name: 'testNode',
|
|
90
|
+
group: ['output'],
|
|
91
|
+
version: 1,
|
|
92
|
+
inputs: ['main'],
|
|
93
|
+
outputs: ['main'],
|
|
94
|
+
${credentialsProperty}
|
|
95
|
+
properties: [],
|
|
96
|
+
};
|
|
97
|
+
}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function createNonNodeClass(): string {
|
|
101
|
+
return `
|
|
102
|
+
export class RegularClass {
|
|
103
|
+
credentials = [
|
|
104
|
+
{ name: 'ExternalApi', required: true }
|
|
105
|
+
];
|
|
106
|
+
}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function createNonINodeTypeClass(): string {
|
|
110
|
+
return `
|
|
111
|
+
export class NotANode {
|
|
112
|
+
description = {
|
|
113
|
+
displayName: 'Not A Node',
|
|
114
|
+
credentials: [
|
|
115
|
+
{ name: 'ExternalApi', required: true }
|
|
116
|
+
]
|
|
117
|
+
};
|
|
118
|
+
}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
mockFindPackageJson.mockReturnValue('/tmp/package.json');
|
|
122
|
+
mockReadPackageJsonCredentials.mockReturnValue(new Set(['myApiCredential', 'oauthApi']));
|
|
123
|
+
|
|
124
|
+
ruleTester.run('valid-credential-references', ValidCredentialReferencesRule, {
|
|
125
|
+
valid: [
|
|
126
|
+
{
|
|
127
|
+
name: 'node referencing a credential that exists (object form)',
|
|
128
|
+
filename: nodeFilePath,
|
|
129
|
+
code: createNodeCode([{ name: 'myApiCredential', required: true }]),
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'node referencing a credential that exists (string form)',
|
|
133
|
+
filename: nodeFilePath,
|
|
134
|
+
code: createNodeCode(['myApiCredential']),
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: 'node referencing multiple credentials that all exist',
|
|
138
|
+
filename: nodeFilePath,
|
|
139
|
+
code: createNodeCode(['myApiCredential', { name: 'oauthApi', required: false }]),
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'node without credentials array',
|
|
143
|
+
filename: nodeFilePath,
|
|
144
|
+
code: createNodeCode(),
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'non-node file ignored',
|
|
148
|
+
filename: '/tmp/regular-file.ts',
|
|
149
|
+
code: createNonNodeClass(),
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: 'non-INodeType class ignored',
|
|
153
|
+
filename: nodeFilePath,
|
|
154
|
+
code: createNonINodeTypeClass(),
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
invalid: [
|
|
158
|
+
{
|
|
159
|
+
name: 'credential name does not exist in package (object form)',
|
|
160
|
+
filename: nodeFilePath,
|
|
161
|
+
code: createNodeCode([{ name: 'brokenReference', required: true }]),
|
|
162
|
+
errors: [
|
|
163
|
+
{
|
|
164
|
+
messageId: 'credentialNotFound',
|
|
165
|
+
data: { credentialName: 'brokenReference' },
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: 'credential name does not exist in package (string form)',
|
|
171
|
+
filename: nodeFilePath,
|
|
172
|
+
code: createNodeCode(['unknownCredential']),
|
|
173
|
+
errors: [
|
|
174
|
+
{
|
|
175
|
+
messageId: 'credentialNotFound',
|
|
176
|
+
data: { credentialName: 'unknownCredential' },
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: 'credential name is a typo close to an existing credential — suggestion provided',
|
|
182
|
+
filename: nodeFilePath,
|
|
183
|
+
code: createNodeCode([{ name: 'myApiCredentail', required: true }]),
|
|
184
|
+
errors: [
|
|
185
|
+
{
|
|
186
|
+
messageId: 'credentialNotFound',
|
|
187
|
+
data: { credentialName: 'myApiCredentail' },
|
|
188
|
+
suggestions: [
|
|
189
|
+
{
|
|
190
|
+
messageId: 'didYouMean',
|
|
191
|
+
data: { suggestedName: 'myApiCredential' },
|
|
192
|
+
output: createExpectedNodeCode([{ name: 'myApiCredential', required: true }]),
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: 'mix of valid and invalid credentials — only invalid reported',
|
|
200
|
+
filename: nodeFilePath,
|
|
201
|
+
code: createNodeCode(['myApiCredential', { name: 'brokenRef', required: true }]),
|
|
202
|
+
errors: [
|
|
203
|
+
{
|
|
204
|
+
messageId: 'credentialNotFound',
|
|
205
|
+
data: { credentialName: 'brokenRef' },
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('valid-credential-references — no package.json found', () => {
|
|
213
|
+
beforeEach(() => {
|
|
214
|
+
mockFindPackageJson.mockReturnValue(null);
|
|
215
|
+
});
|
|
216
|
+
afterEach(() => {
|
|
217
|
+
mockFindPackageJson.mockReturnValue('/tmp/package.json');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
ruleTester.run('valid-credential-references (no package.json)', ValidCredentialReferencesRule, {
|
|
221
|
+
valid: [
|
|
222
|
+
{
|
|
223
|
+
name: 'check is skipped when package.json cannot be found',
|
|
224
|
+
filename: nodeFilePath,
|
|
225
|
+
code: createNodeCode([{ name: 'anyCredential', required: true }]),
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
invalid: [],
|
|
229
|
+
});
|
|
230
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { TSESTree } from '@typescript-eslint/types';
|
|
2
|
+
import type { ReportSuggestionArray } from '@typescript-eslint/utils/ts-eslint';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
isNodeTypeClass,
|
|
6
|
+
findClassProperty,
|
|
7
|
+
findArrayLiteralProperty,
|
|
8
|
+
extractCredentialNameFromArray,
|
|
9
|
+
findPackageJson,
|
|
10
|
+
readPackageJsonCredentials,
|
|
11
|
+
isFileType,
|
|
12
|
+
findSimilarStrings,
|
|
13
|
+
createRule,
|
|
14
|
+
} from '../utils/index.js';
|
|
15
|
+
|
|
16
|
+
export const ValidCredentialReferencesRule = createRule({
|
|
17
|
+
name: 'valid-credential-references',
|
|
18
|
+
meta: {
|
|
19
|
+
type: 'problem',
|
|
20
|
+
docs: {
|
|
21
|
+
description:
|
|
22
|
+
'Ensure credentials referenced in node descriptions exist as credential classes in the package',
|
|
23
|
+
},
|
|
24
|
+
messages: {
|
|
25
|
+
credentialNotFound:
|
|
26
|
+
'Credential "{{ credentialName }}" does not exist in this package. Check for typos or ensure the credential class is declared and listed in package.json.',
|
|
27
|
+
didYouMean: "Did you mean '{{ suggestedName }}'?",
|
|
28
|
+
},
|
|
29
|
+
schema: [],
|
|
30
|
+
hasSuggestions: true,
|
|
31
|
+
},
|
|
32
|
+
defaultOptions: [],
|
|
33
|
+
create(context) {
|
|
34
|
+
if (!isFileType(context.filename, '.node.ts')) {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let packageCredentials: Set<string> | null = null;
|
|
39
|
+
|
|
40
|
+
const loadPackageCredentials = (): Set<string> => {
|
|
41
|
+
if (packageCredentials !== null) {
|
|
42
|
+
return packageCredentials;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const packageJsonPath = findPackageJson(context.filename);
|
|
46
|
+
if (!packageJsonPath) {
|
|
47
|
+
packageCredentials = new Set();
|
|
48
|
+
return packageCredentials;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
packageCredentials = readPackageJsonCredentials(packageJsonPath);
|
|
52
|
+
return packageCredentials;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
ClassDeclaration(node) {
|
|
57
|
+
if (!isNodeTypeClass(node)) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const descriptionProperty = findClassProperty(node, 'description');
|
|
62
|
+
if (
|
|
63
|
+
!descriptionProperty?.value ||
|
|
64
|
+
descriptionProperty.value.type !== TSESTree.AST_NODE_TYPES.ObjectExpression
|
|
65
|
+
) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const credentialsArray = findArrayLiteralProperty(descriptionProperty.value, 'credentials');
|
|
70
|
+
if (!credentialsArray) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const knownCredentials = loadPackageCredentials();
|
|
75
|
+
if (knownCredentials.size === 0) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
credentialsArray.elements.forEach((element) => {
|
|
80
|
+
const credentialInfo = extractCredentialNameFromArray(element);
|
|
81
|
+
if (!credentialInfo || knownCredentials.has(credentialInfo.name)) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const similar = findSimilarStrings(credentialInfo.name, knownCredentials);
|
|
86
|
+
const suggestions: ReportSuggestionArray<'credentialNotFound' | 'didYouMean'> =
|
|
87
|
+
similar.map((suggestedName) => ({
|
|
88
|
+
messageId: 'didYouMean' as const,
|
|
89
|
+
data: { suggestedName },
|
|
90
|
+
fix(fixer) {
|
|
91
|
+
return fixer.replaceText(credentialInfo.node, `"${suggestedName}"`);
|
|
92
|
+
},
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
context.report({
|
|
96
|
+
node: credentialInfo.node,
|
|
97
|
+
messageId: 'credentialNotFound',
|
|
98
|
+
data: { credentialName: credentialInfo.name },
|
|
99
|
+
suggest: suggestions,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
});
|
|
@@ -107,6 +107,11 @@ export class RegularClass {
|
|
|
107
107
|
code: createTriggerNode({ webhookMethods: null }),
|
|
108
108
|
errors: [{ messageId: 'missingWebhookMethods' }],
|
|
109
109
|
},
|
|
110
|
+
{
|
|
111
|
+
name: 'trigger node with empty webhookMethods object (no lifecycle groups)',
|
|
112
|
+
code: createTriggerNode({ webhookMethods: '{}' }),
|
|
113
|
+
errors: [{ messageId: 'emptyWebhookMethods' }],
|
|
114
|
+
},
|
|
110
115
|
{
|
|
111
116
|
name: 'trigger node with empty webhookMethods group (all three missing)',
|
|
112
117
|
code: createTriggerNode({
|
|
@@ -56,6 +56,8 @@ export const WebhookLifecycleCompleteRule = createRule({
|
|
|
56
56
|
messages: {
|
|
57
57
|
missingWebhookMethods:
|
|
58
58
|
'Webhook trigger node is missing the `webhookMethods` property. Implement `checkExists`, `create`, and `delete` to register, verify, and clean up the webhook on the third-party service.',
|
|
59
|
+
emptyWebhookMethods:
|
|
60
|
+
'Webhook trigger node has an empty `webhookMethods` object. Define at least one lifecycle group with `checkExists`, `create`, and `delete` methods.',
|
|
59
61
|
missingLifecycleMethod:
|
|
60
62
|
'Webhook trigger lifecycle is incomplete. `webhookMethods.{{group}}` is missing: {{missing}}. All of `checkExists`, `create`, and `delete` must be implemented.',
|
|
61
63
|
},
|
|
@@ -91,6 +93,14 @@ export const WebhookLifecycleCompleteRule = createRule({
|
|
|
91
93
|
return;
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
if (webhookMethodsProperty.value.properties.length === 0) {
|
|
97
|
+
context.report({
|
|
98
|
+
node: webhookMethodsProperty.key,
|
|
99
|
+
messageId: 'emptyWebhookMethods',
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
94
104
|
for (const groupProperty of webhookMethodsProperty.value.properties) {
|
|
95
105
|
if (groupProperty.type !== AST_NODE_TYPES.Property) continue;
|
|
96
106
|
if (groupProperty.value.type !== AST_NODE_TYPES.ObjectExpression) continue;
|
package/src/utils/ast-utils.ts
CHANGED
|
@@ -130,7 +130,11 @@ export function hasArrayLiteralValue(
|
|
|
130
130
|
export function getTopLevelObjectInJson(
|
|
131
131
|
node: TSESTree.ObjectExpression,
|
|
132
132
|
): TSESTree.ObjectExpression | null {
|
|
133
|
-
|
|
133
|
+
// In a JSON file parsed as JS, the root object is the sole expression of
|
|
134
|
+
// the program, so its parent is an ExpressionStatement. Anything else
|
|
135
|
+
// (Property, ArrayExpression, etc.) is nested and must not be treated as
|
|
136
|
+
// the package root.
|
|
137
|
+
if (node.parent?.type !== AST_NODE_TYPES.ExpressionStatement) {
|
|
134
138
|
return null;
|
|
135
139
|
}
|
|
136
140
|
return node;
|